Building a Vertical on Purple8
Purple8 is delivered as a platform and a set of vertical products built on it. The platform is Purple8 Graph and Purple8 DocIntel. A vertical is configuration and a front-end. This page defines what a vertical consists of, what it must not contain, and the order in which to build one.
Platform and verticals
Vertical products AML Platform Customer Onboarding Supplier Risk …
(thin: config + UI) │ │ │
└────────────┬────┴──────────────────┘
│ REST API · MCP
Platform Purple8 Graph Purple8 DocIntel
(all logic lives here) graph · vector · full-text · Journey Engine · RBAC
audit · encryption · replication · algorithms · egress
│
Delivery Self-Hosted · Customer VPC · Managed Service
(identical for every vertical)A vertical inherits, without additional work: authentication and RBAC, encryption at rest, audit trail, replication and failover, backup, multi-tenancy, observability, MCP access for AI agents, and all four deployment models.
Definition of a vertical
A vertical consists of exactly six artifacts.
| # | Artifact | Lives in | Mechanism |
|---|---|---|---|
| 1 | Schema | Purple8 Graph | Node types, edge types, property constraints registered via /schema or schema.* MCP tools |
| 2 | Journey templates | Purple8 Graph | JourneyDefinition specs: stages, SLA policies, HITL gates, AI-assist hooks. Loaded via journey.define or journey_templates.py |
| 3 | Extraction profile | Purple8 DocIntel | ExtractionProfile with domain ontology (entity and relationship types), chunking strategy, confidence threshold, GLiNER or LLM backend |
| 4 | Detection configuration | Purple8 Graph | Which built-in algorithms run, on which labels and edge types, with which thresholds. Optionally an evolutionary tuning objective |
| 5 | Domain vocabulary | Front-end | Labels, edge types, prompt framing, display text, role descriptions. One typed configuration object per tenant or market |
| 6 | Front-end | Static SPA | React application calling the Purple8 Graph REST API and MCP endpoint directly. No intermediate server |
Anything not in this list belongs in the platform, not the vertical.
What a vertical must not contain
| Prohibited | Reason | Where it belongs |
|---|---|---|
| A backend service or API layer | Breaks the single-process delivery model; adds an operational component to every deployment | Purple8 Graph router or MCP tool |
| A database, cache or queue | Purple8 Graph is the store | Purple8 Graph |
| Custom authentication or authorization | Security controls must be uniform across products | Purple8 Graph RBAC, OIDC, SAML |
| Graph algorithms | Algorithms are domain-neutral and must be tested once | core/algorithms.py |
| Document parsing or extraction code | DocIntel owns the parse-chunk-extract pipeline | DocIntel parser or profile |
| Direct storage access | Bypasses audit, encryption and tenancy | REST or MCP |
If a vertical needs a capability the platform lacks, add it to the platform. Every other vertical then receives it.
Build order
1. Schema
Define the node and edge types for the domain. Use the platform's schema registry so that validation, the LCNC graph browser and DocIntel emit all agree.
POST /schema/node-types
{
"type_name": "Account",
"data_classification": "confidential",
"properties": {
"account_id": { "type": "str", "required": true },
"opened_at": { "type": "str" },
"risk_tier": { "type": "str", "enum": ["low", "medium", "high"] }
},
"immutable_properties": ["account_id"]
}
POST /schema/edge-types
{
"type_name": "TRANSFERRED_TO",
"source_node_types": ["Account"],
"target_node_types": ["Account"],
"properties": {
"amount": { "type": "float", "required": true },
"currency": { "type": "str", "required": true },
"ts": { "type": "str", "required": true }
}
}AI agents perform the same step with schema.define_node_type and schema.define_edge_type.
2. Journey templates
Model the lifecycle the vertical manages. Each stage carries an SLA policy and, where a human decision is required, a HITL gate.
TEMPLATE = {
"journey_type": "aml_case",
"description": "AML investigation: triage → investigation → review → filed.",
"stages": [
{"name": "triage", "owner_system": "alerting",
"sla_warn_seconds": 7_200, "sla_breach_seconds": 14_400},
{"name": "investigation", "owner_system": "investigations_team",
"sla_warn_seconds": 86_400, "sla_breach_seconds": 259_200,
"ai_step": {"trigger": "on_enter", "auto_execute": False,
"system_prompt": "Summarise the evidence graph for this case."}},
{"name": "review", "owner_system": "compliance_officer",
"sla_warn_seconds": 43_200, "sla_breach_seconds": 86_400,
"hitl": {"required": True, "assigned_role": "admin",
"title": "Approve SAR filing", "expires_in_seconds": 86_400}},
{"name": "filed", "owner_system": "regulatory_reporting", "terminal": True},
],
}The spec shape matches TEMPLATE_SPECS in journey_templates.py. Load with journey.define (MCP) or a load_*() function following the same module. Every transition, SLA breach and HITL decision is written to the graph as an immutable edge; the vertical writes no audit code.
3. Extraction profile
Create extraction/profiles/<domain>.py in Purple8 DocIntel. The ontology's entity and relationship types must match the schema from step 1 so that emitted entities land on the correct node types.
PROFILE = ExtractionProfile(
name="aml",
entity_backend="gliner",
gliner_model="urchade/gliner_medium-v2.1",
confidence_threshold=0.55,
chunking=ChunkingStrategy(mode="table_aware", size=1200, overlap=150),
ontology=ExtractionOntology(
entity_types=["Person", "Account", "Company", "Transaction", "Amount", "Date", "Jurisdiction"],
relationship_types=["OWNS", "TRANSFERRED_TO", "CONTROLS", "LOCATED_IN"],
two_pass=True,
),
)Register in extraction/router.py. Documents processed with profile_hint=aml are emitted to Purple8 Graph with deterministic entity IDs, so the same real-world entity always resolves to the same node.
4. Detection configuration
Select platform algorithms and bind them to the domain's labels and edge types. No new algorithm code.
| Detection | Platform primitive | Vertical supplies |
|---|---|---|
| Ring or mule network | detect_communities | Edge types that carry value; minimum density |
| Hub or facilitator | betweenness_centrality, pagerank | Labels to rank; top-k |
| Sanctions or PEP exposure | shortest_path | Target label; maximum hops |
| Structuring | Query DSL over transaction edges | Threshold amount; window |
| Threshold tuning | Evolutionary optimisation (core/evolution.py) | Objective: precision, recall, alert capacity |
Store configuration as graph nodes or in the tenant's settings so it is versioned, audited and per-tenant.
5. Domain vocabulary
One typed configuration object per tenant or market. The AML platform's tenantConfig.ts is the reference:
export interface TenantConfig {
displayName: string;
pivotLabels: Set<string>; // shared-infrastructure node types
entityLabels: Set<string>; // match targets
ownershipEdgeTypes: string[];
transactionEdgeTypes: string[];
analystRole: string; // injected into LLM prompts
typologyQuestion: (entity: string) => string;
}All API and algorithm calls are identical across tenants. Only labels, edge types, prompt framing and display text differ. This is what makes the One-to-Many and Many-to-One topologies configuration rather than code.
6. Front-end
A static React application. It calls Purple8 Graph directly.
| Concern | Implementation |
|---|---|
| Authentication | OIDC redirect to the customer IdP, or Purple8 JWT login; token in Authorization: Bearer |
| Data | GET /nodes, POST /graph/traverse, POST /algorithms/*, POST /rag/hybrid-query |
| Workflow | journey.* endpoints for case status, advancement, HITL queue |
| AI assistance | POST /mcp (SSE) for streamed, evidence-cited explanations |
| Configuration | Build-time VITE_API_BASE_URL; runtime TenantConfig |
| Hosting | S3 + CloudFront, or any static host. See AWS Deployment Patterns |
There is no server-side rendering, session store or API gateway in the vertical.
Acceptance criteria
A vertical is complete when all of the following hold.
- [ ] Schema registered;
schema.validatepasses on seeded data. - [ ] Journey template loads; an instance can be started, advanced through every stage, blocked at each HITL gate, and audited via
journey.audit. - [ ] Extraction profile registered; a representative document set emits entities whose types all exist in the schema.
- [ ] Detection configuration produces expected results on the seeded data set; thresholds are stored per tenant, not hard-coded.
- [ ] Front-end runs against an unmodified Purple8 Graph instance with only
VITE_API_BASE_URLand aTenantConfigchanged. - [ ] Repository contains no server code, no database client, no authentication logic.
- [ ] Deploys unchanged on the reference AWS stack.
Reference: Purple8 AML Platform
| Artifact | Location |
|---|---|
| Schema and seed data | generate_demo_data.py, ingest_ibm_aml.py |
| Journey templates | compliance_review (platform), AML case template (vertical) |
| Extraction profile | Purple8-DocIntel/extraction/profiles/financial.py |
| Detection configuration | TypologyManager.tsx, dimensional_ablation_aml.py |
| Domain vocabulary | src/tenantConfig.ts (banking, asset_mgmt, prime_brokerage) |
| Front-end | src/App.tsx, AmlGraph.tsx, ClaudePanel.tsx, api.ts |
Three tenant vocabularies share one code base and one platform instance. Adding a fourth is a TenantConfig entry.
Reference: Hybrid Human–Agent Orchestration
A vertical in which AI agents and humans work the same cases, in the same state machine, with the same audit trail. No orchestration framework is added; the Journey Engine is the orchestrator and the graph is the shared state.
Why this is native rather than integrated
| Orchestration concern | Conventional stack | Purple8 |
|---|---|---|
| Shared state between agents | Message bus + state store | The graph. Agents read and write nodes and edges; every write is visible to every other agent and every human on the next read. |
| Workflow definition | Separate DAG or graph DSL | JourneyDefinition. One definition serves humans and agents. |
| Agent step | Framework node with tool bindings | AIStepConfig on a stage: trigger (on_enter, on_sla_warn, on_sla_breach, manual), auto_execute, allowed_actions, system_prompt. |
| Human step | Custom approval service | hitl on a stage: required, assigned_role, title, expires_in_seconds. Surfaced via journey.list_hitl; cleared via journey.resolve_hitl. |
| Hand-off agent → human | Callback or webhook | Agent returns suggest_advance, flag or hold. Instance waits at the HITL gate. |
| Hand-off human → agent | Trigger call | Human resolves the HITL task; the next stage's on_enter AI step fires. |
| Wake-up on change | Polling or pub/sub infrastructure | graph.wait_for_changes (long-poll) or graph.poll_changes (cursor). |
| Scheduled work | Cron service | scheduler.add_trigger. |
| Tool access for agents | Per-agent tool wiring | MCP server, 125 MCP tools across 16 namespaces, RBAC per tool. External tools via registry.register_tool and registry.register_mcp_server. |
| Guardrails | Middleware | guardrails.evaluate before any agent-proposed action is applied. |
| Tracing and audit | Separate tracing service | Every transition, AI recommendation, HITL decision and SLA event is an immutable graph edge. journey.audit returns the full trail. |
| Learning | Fine-tuning or prompt store | feedback.open / feedback.record / feedback.resolve; memory.history / memory.search. |
Agents and humans are peers. Both are principals with roles; both act through the same API; both leave the same audit records.
Roles in the state machine
| Principal | Role | Acts via | Allowed actions |
|---|---|---|---|
| Triage agent | editor (API key) | MCP journey.*, graph.*, rag.* | annotate, suggest_advance, flag |
| Research agent | editor (API key) | MCP rag.hybrid_query, graph.traverse, data.ingest_* | annotate |
| Supervisor agent | editor (API key) | MCP journey.sla_alerts, journey.advance | force_advance where auto_execute: true, hold |
| Case officer | editor (OIDC) | Front-end | Advance, annotate, resolve HITL tasks assigned to their role |
| Approver | admin (OIDC) | Front-end or LCNC | Resolve admin HITL gates |
Which AIActionType values an agent may return is set per stage in allowed_actions. force_advance requires auto_execute: true on that stage; every other action leaves the decision with a human.
The six artifacts
| # | Artifact | Content for this vertical |
|---|---|---|
| 1 | Schema | Case, Evidence, Recommendation, Decision node types; SUPPORTS, CONTRADICTS, RECOMMENDED_BY, DECIDED_BY edge types. Recommendation and Decision carry principal_id and principal_type (agent or human). |
| 2 | Journey template | Below. |
| 3 | Extraction profile | Domain profile for incoming evidence documents. |
| 4 | Detection configuration | Not applicable, or the domain's own (e.g. AML detections if this orchestration wraps AML). |
| 5 | Domain vocabulary | Stage names, role names, agent system prompts per tenant. |
| 6 | Front-end | Case board showing current stage, pending HITL tasks for the signed-in role, agent recommendations with evidence links, full audit timeline. |
Journey template
TEMPLATE = {
"journey_type": "hybrid_case",
"description": "Agents triage and research; humans decide; supervisor agent enforces SLA.",
"stages": [
{"name": "received", "owner_system": "intake",
"sla_warn_seconds": 900, "sla_breach_seconds": 3_600,
"ai_step": {"trigger": "on_enter", "auto_execute": True,
"allowed_actions": ["force_advance", "flag"],
"system_prompt": "Classify the case. Advance to 'research' if in scope; flag if not."}},
{"name": "research", "owner_system": "research_agent",
"sla_warn_seconds": 7_200, "sla_breach_seconds": 14_400,
"ai_step": {"trigger": "on_enter", "auto_execute": False,
"allowed_actions": ["annotate", "suggest_advance"],
"system_prompt": "Gather evidence via rag.hybrid_query and graph.traverse. "
"Write Evidence nodes. Recommend a decision with citations."}},
{"name": "decision", "owner_system": "case_officer",
"sla_warn_seconds": 43_200, "sla_breach_seconds": 86_400,
"hitl": {"required": True, "assigned_role": "editor",
"title": "Review agent recommendation and decide", "expires_in_seconds": 86_400},
"ai_step": {"trigger": "on_sla_warn", "auto_execute": False,
"allowed_actions": ["annotate"],
"system_prompt": "Summarise what is blocking the decision."}},
{"name": "approval", "owner_system": "approver",
"sla_warn_seconds": 43_200, "sla_breach_seconds": 86_400,
"hitl": {"required": True, "assigned_role": "admin",
"title": "Approve decision", "expires_in_seconds": 86_400}},
{"name": "closed", "owner_system": "records", "terminal": True},
],
}Execution sequence
Intake (webhook / data.ingest_file)
│ journey.start
▼
received ──on_enter──► Triage agent ──force_advance──► research
└─flag──────────► held for human
│
research ──on_enter──► Research agent
│ rag.hybrid_query · graph.traverse · graph.add_node(Evidence)
│ suggest_advance("decision") + annotate(citations)
▼
decision [HITL: editor] Case officer reads recommendation + evidence, decides
│ └─on_sla_warn──► Supervisor agent annotates blocker; journey.sla_alerts raised
│ journey.resolve_hitl
▼
approval [HITL: admin] Approver confirms
│ journey.resolve_hitl
▼
closedEvery arrow is an ADVANCED_TO, SLA_BREACHED or HITL edge in the graph. journey.audit returns the sequence with principal, timestamp, action and evidence references.
Agent runtime
Agents are external processes holding an API key. A minimal loop:
graph.wait_for_changesfiltered toJourneyInstancenodes entering the agent's stage, orscheduler.add_triggerfor periodic sweeps.journey.statusto load the instance;graph.traverseandrag.hybrid_queryto gather context.guardrails.evaluateon the proposed action.journey.advance(only whereauto_executepermits) or write aRecommendationnode and annotate.feedback.recordwhen a human later overrides the recommendation.
Any MCP-capable agent runtime (Claude, Copilot, a custom loop) can play any agent role. Purple8 does not prescribe the runtime.
Acceptance additions
- [ ] An agent-initiated
force_advanceis rejected on stages whereauto_executeis false. - [ ] A HITL gate cannot be passed by an agent principal.
- [ ]
journey.auditdistinguishes agent and human principals for every transition. - [ ] Overriding an agent recommendation records a
feedbackentry linked to the instance.
Pattern catalogue
Use cases published on purple8.ai and in Use Cases, expressed as the six artifacts. Each row is a vertical; none requires platform code.
| Vertical | Schema (key types) | Journey | Extraction profile | Detection / analytics | Vocabulary axis |
|---|---|---|---|---|---|
| AML / financial crime | Person, Account, Transaction, Company | aml_case | financial | communities, centrality, shortest path, evolutionary tuning | banking / asset management / prime brokerage |
| Customer onboarding | Customer, Document, Check, Product | customer_onboarding | general, contract | duplicate detection (vector), link prediction | retail / commercial / wealth |
| Supplier and vendor risk | Supplier, Contract, Site, Certification | supplier_approval | contract | PageRank (criticality), bridges (single points of failure) | industry |
| Insurance claims | Claim, Policy, Party, Provider | claims_lifecycle | general, financial | communities (fraud rings), triangles | line of business |
| Healthcare care coordination | Patient, Encounter, Provider, Order | patient_journey | scientific | shortest path (referral), temporal PageRank | specialty |
| Manufacturing traceability | Order, Batch, Machine, Inspection | production_order | general | BFS impact propagation, MST | plant |
| Government service delivery | Citizen, Application, Agency, Document | citizen_service | general | duplicate detection, SLA analytics | programme |
| Pharma clinical trials | Trial, Site, Patient, Submission | compliance_review | scientific | link prediction (recruitment), lineage | therapeutic area |
| Regulatory compliance graph | Regulation, Control, Evidence, Owner | compliance_review | contract | coverage gaps (traversal), betweenness | jurisdiction |
| CRM deal intelligence | Account, Contact, Opportunity, Interaction | deal_lifecycle | general | community (buying group), temporal PageRank (momentum) | segment |
| Hybrid human–agent orchestration | Case, Evidence, Recommendation, Decision | hybrid_case | domain-specific | as per wrapped domain | agent roles / prompts |
| HR org-chart assistant | Person, Team, Role, Skill | none | general | shortest path, centrality | business unit |
Adding a vertical from this table is: register schema, load template, register extraction profile, set detection configuration, write TenantConfig, build front-end. Deployment is the same stack in every case.