Tutorials
These walkthroughs take you end-to-end through the three workflows that most onboarding teams need first: getting knowledge into the graph and querying it, wiring an SPO persona pipeline as a flow, and tracing a memo claim back to its sources for sign-off.
Every step is a real HTTP call against the Node.js Gateway (http://localhost:4000). If you have not started the stack yet, follow the Development Guide first --- you need Rust Core, the Gateway, PostgreSQL, and NATS running.
A few conventions used throughout:
- All authenticated calls send
Authorization: Bearer $TOKEN. The$TOKENis theaccess_tokenfrom login (15-minute lifetime --- re-login or refresh if it expires). - You never pass
tenant_idin a request body. It is derived from your JWT (see API Reference -> Authentication). - Responses are abbreviated to the fields each step needs. The examples are illustrative --- UUIDs and tokens are placeholders.
| Tutorial | What you learn | Reference |
|---|---|---|
| 1. Ingest knowledge & query it | The human-gated ingestion lifecycle and epistemic grounding in conversations | Knowledge Graph |
| 2. Build & run an SPO persona pipeline flow | Wiring Scarlet -> Ayana -> Eliza -> Reagan with a claim-validation checkpoint | API Reference -> Flows |
| 3. Trace a memo claim for sign-off | Walking a claim back to its supporting triples and source documents | Knowledge Graph -> Claim Trace |
Tutorial 1: Ingest knowledge & query it
Goal: register an account, propose a document, approve it through the human gate so it ingests, then ask a question and see the answer grounded in the freshly ingested facts.
This tutorial exercises the human-gated ingestion lifecycle described in Knowledge Graph -> Document Ingestion Pipeline. Nothing enters the SPO graph or vector store until a human approves it.
-
Register a user and tenant. Registration creates the user, a default workspace tenant, and returns a token pair in one call.
curl -X POST http://localhost:4000/auth/register \ -H 'Content-Type: application/json' \ -d '{ "email": "analyst@acme.test", "password": "supersecret", "name": "Ada Analyst" }'{ "access_token": "eyJ0eXAiOiJKV1Q...", "refresh_token": "eyJ0eXAiOiJKV1Q...", "active_tenant": { "tenant_id": "tnt-1111", "tenant_name": "Ada Analyst's Workspace", "role": "owner" } }If the account already exists, log in instead --- the response shape is identical:
curl -X POST http://localhost:4000/auth/login \ -H 'Content-Type: application/json' \ -d '{ "email": "analyst@acme.test", "password": "supersecret" }'Capture the access token for the rest of the tutorial:
export TOKEN="eyJ0eXAiOiJKV1Q..." -
Propose a document for review. Proposing does not ingest --- it places the document in the review queue with status
pending_approval.curl -X POST http://localhost:4000/knowledge/propose \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "title": "Acme Corp Overview", "content": "Acme Corporation was founded in 1990 by John Smith. The company is headquartered in San Francisco and reported 2024 revenue of $391B. Acme acquired DataCo in 2015 for $50 million.", "content_type": "text/plain", "source_url": "https://example.com/acme", "source_type": "web" }'{ "document_id": "doc-2222" } -
Approve the proposal (the human gate). Approval stamps
approved_by/approved_atand kicks off ingestion asynchronously on the Rust side. It returns immediately --- ingestion (analyze -> extract triples -> chunk -> embed) runs in the background.curl -X POST http://localhost:4000/knowledge/documents/doc-2222/approve \ -H "Authorization: Bearer $TOKEN"{ "document_id": "doc-2222", "status": "approved", "message": "Ingestion started -- poll document status for progress" } -
Poll until ingestion completes. Status moves
approved -> processing -> completed. Theingestion_resultcarries the routing decision and counts.curl http://localhost:4000/knowledge/documents/doc-2222 \ -H "Authorization: Bearer $TOKEN"{ "document_id": "doc-2222", "title": "Acme Corp Overview", "ingestion_status": "completed", "ingestion_result": { "routing": "both", "triples_extracted": 4, "chunks_created": 2 } }Four triples are now in the SPO index --- e.g.
(Acme Corporation, founded_by, John Smith)and(Acme Corporation, revenue_is, $391B)--- each linked back todoc-2222for provenance. -
Create an agent to talk to. Conversations route through an agent persona (see API Reference -> Agents).
curl -X POST http://localhost:4000/agents \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "research-analyst", "display_name": "Research Analyst", "description": "Answers questions grounded in the knowledge graph", "system_prompt": "You are a research analyst. Answer using only grounded facts." }'{ "id": "agt-3333", "name": "research-analyst" } -
Open a conversation.
curl -X POST http://localhost:4000/conversations \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "title": "Acme research" }'{ "id": "cnv-4444", "title": "Acme research" } -
Ask a question and see epistemic grounding. Sending a message runs the epistemic query pipeline on your message before synthesis: it pulls matching SPO triples, runs the competence gate, and injects the grounding context into the LLM call. After synthesis, the verification gate records whether the answer stayed grounded --- both results land in the message metadata.
curl -X POST http://localhost:4000/conversations/cnv-4444/messages \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "agent_id": "agt-3333", "content": "Who founded Acme Corporation and what was its 2024 revenue?" }'{ "id": "msg-5555", "role": "assistant", "content": "Acme Corporation was founded by John Smith and reported 2024 revenue of $391B.", "metadata": { "verification": { "passed": true, "faithfulness": 1.0, "attributions": [ { "subject": "Acme Corporation", "predicate": "founded_by", "object": "John Smith", "score": 0.62 }, { "subject": "Acme Corporation", "predicate": "revenue_is", "object": "$391B", "score": 0.41 } ], "ungrounded_claims": [] } } }The answer is drawn from the triples you just ingested. The
verificationblock confirms the response stayed grounded --- if you ask about something not in the graph, the competence gate coverage drops and the verification gate surfaces the unsupported claims underungrounded_claimsinstead of hiding them.To watch the gates and node steps in real time, POST to
/conversations/cnv-4444/messages/streaminstead and read thetext/event-stream. See API Reference -> Stream Message.
Tutorial 2: Build & run an SPO persona pipeline flow
Goal: wire a four-persona research pipeline as a flow graph, run it, and inspect the execution results --- including the claim-validation checkpoint that blocks unverified claims from flowing downstream.
The personas map to flow node behaviors described in API Reference -> Flow Node Types:
| Persona | Role | Node behavior |
|---|---|---|
| Scarlet | Research --- writes triples | Agent then SpoWrite (persists her output as SPO triples with provenance) |
| Ayana | Analysis --- filters triples | Agent then SpoFilter (queries SPO triples into state, optionally by asserter) |
| Eliza | Due diligence --- drafts the DD | Agent |
| Reagan | Review --- approves the output | Agent |
| --- | Cross-stage grounding check | ClaimValidation (grounds Scarlet's and Ayana's claims against the SPO graph) |
The ClaimValidation node is the hard checkpoint: it labels every prior-stage claim SUPPORTED, CONTRADICTED, or NOT_MENTIONED before Eliza builds on them. See Knowledge Graph -> Claim Validation Node.
-
Create the flow definition. The graph is a ReactFlow
{nodes, edges, viewport}object. Each node'sdatacarries its type-specific config. Reuse$TOKENfrom Tutorial 1.curl -X POST http://localhost:4000/flows \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "spo-persona-dd-pipeline", "description": "Scarlet writes triples, Ayana filters, ClaimValidation checks, Eliza drafts DD, Reagan reviews", "flow_data": { "nodes": [ { "id": "start", "type": "Start", "data": {}, "position": { "x": 0, "y": 0 } }, { "id": "scarlet", "type": "Agent", "data": { "label": "Scarlet (Research)", "agent_name": "scarlet", "prompt": "Research {{target_company}} and state concrete facts.", "output_key": "scarlet_output" }, "position": { "x": 200, "y": 0 } }, { "id": "scarlet-write", "type": "SpoWrite", "data": { "label": "Persist Scarlet triples", "content_key": "scarlet_output", "asserter": "scarlet", "source": "spo-persona-dd-pipeline", "output_key": "scarlet_triples" }, "position": { "x": 400, "y": 0 } }, { "id": "ayana", "type": "Agent", "data": { "label": "Ayana (Analysis)", "agent_name": "ayana", "prompt": "Analyze the research for {{target_company}}.", "output_key": "ayana_output" }, "position": { "x": 600, "y": 0 } }, { "id": "ayana-filter", "type": "SpoFilter", "data": { "label": "Filter Scarlet triples", "asserter": "scarlet", "output_key": "scarlet_facts" }, "position": { "x": 800, "y": 0 } }, { "id": "validate", "type": "ClaimValidation", "data": { "label": "Ground prior claims", "input_keys": "scarlet_output,ayana_output", "output_key": "claim_validation" }, "position": { "x": 1000, "y": 0 } }, { "id": "eliza", "type": "Agent", "data": { "label": "Eliza (Due Diligence)", "agent_name": "eliza", "prompt": "Write a due-diligence summary using only SUPPORTED claims from {{claim_validation}}.", "output_key": "eliza_output" }, "position": { "x": 1200, "y": 0 } }, { "id": "reagan", "type": "Agent", "data": { "label": "Reagan (Review)", "agent_name": "reagan", "prompt": "Review Eliza's DD for sign-off readiness.", "output_key": "reagan_output" }, "position": { "x": 1400, "y": 0 } }, { "id": "end", "type": "End", "data": {}, "position": { "x": 1600, "y": 0 } } ], "edges": [ { "id": "e1", "source": "start", "target": "scarlet" }, { "id": "e2", "source": "scarlet", "target": "scarlet-write" }, { "id": "e3", "source": "scarlet-write","target": "ayana" }, { "id": "e4", "source": "ayana", "target": "ayana-filter" }, { "id": "e5", "source": "ayana-filter", "target": "validate" }, { "id": "e6", "source": "validate", "target": "eliza" }, { "id": "e7", "source": "eliza", "target": "reagan" }, { "id": "e8", "source": "reagan", "target": "end" } ], "viewport": { "x": 0, "y": 0, "zoom": 1 } } }'{ "id": "flw-6666", "name": "spo-persona-dd-pipeline", "node_count": 9 }Don't want to hand-author the graph?
POST /flows/generatebuilds one from a natural-language prompt instead (see API Reference -> AI Flow Generation). -
Execute the flow. The
inputobject seeds the flow state --- here, the company under review. The executor walks the graph fromStart, running each node and threading outputs through shared state.curl -X POST http://localhost:4000/flows/flw-6666/execute \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "input": { "target_company": "Acme Corporation" } }'{ "execution_id": "exe-7777", "status": "running" } -
Poll the execution until it finishes. Status follows
pending -> running -> completed(orfailed/cancelled). If you had inserted aHumanInputnode, it would pause atpausedhere and wait forPOST /flows/executions/exe-7777/input.curl http://localhost:4000/flows/executions/exe-7777 \ -H "Authorization: Bearer $TOKEN" -
Inspect the results. A completed execution exposes the per-node
node_states(each withstatus,input,output) and the sharedstateblackboard. TheClaimValidationnode's output is the checkpoint you care about for sign-off.{ "execution_id": "exe-7777", "status": "completed", "state": { "target_company": "Acme Corporation", "scarlet_output": "Acme was founded by John Smith; 2024 revenue $391B.", "scarlet_triples": [ { "subject": "Acme Corporation", "predicate": "founded_by", "object": "John Smith" } ], "claim_validation": { "summary": { "supported": 2, "contradicted": 0, "not_mentioned": 1 }, "claims": [ { "claim": "Acme was founded by John Smith", "label": "SUPPORTED" }, { "claim": "2024 revenue was $391B", "label": "SUPPORTED" }, { "claim": "Acme operates in 40 countries", "label": "NOT_MENTIONED" } ] }, "eliza_output": "Due-diligence summary (grounded claims only)...", "reagan_output": "Approved for sign-off." }, "node_states": { "scarlet": { "status": "completed" }, "scarlet-write":{ "status": "completed", "output": { "triples_written": 4 } }, "ayana": { "status": "completed" }, "ayana-filter": { "status": "completed" }, "validate": { "status": "completed" }, "eliza": { "status": "completed" }, "reagan": { "status": "completed" } } }The
NOT_MENTIONEDlabel on "Acme operates in 40 countries" is the system telling you Scarlet asserted something the graph cannot back up --- Eliza is instructed to drop it, and Reagan signs off only on the grounded remainder.List every run of a flow with
GET /flows/flw-6666/executions.
Tutorial 3: Trace a memo claim for sign-off
Goal: an investment-committee reviewer reads a claim in a memo and needs to verify it against the underlying knowledge in one round trip --- which triples support it, who asserted them, and from which source documents.
This uses the claim-trace path documented in Knowledge Graph -> Claim Trace. It runs the inverse of ingestion: free-text claim in, supporting triples + source documents out, under a 500 ms budget (a 450 ms internal timeout returns partial results rather than failing the review).
-
Trace the claim. Paste the memo statement verbatim.
max_resultscaps how many supporting matches come back (1--20). Reuse$TOKEN.curl -X POST http://localhost:4000/knowledge/trace \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "claim": "Acme Corporation was founded by John Smith", "max_results": 5 }' -
Read the trace. Each match groups a supporting triple (with its
asserterandassertion_type) under the claim and links it to the source documents that ground it --- each carrying the excerpt and the full source content.{ "claim": "Acme Corporation was founded by John Smith", "matches": [ { "triple": { "subject": "Acme Corporation", "predicate": "founded_by", "object": "John Smith", "confidence": 0.95, "source": "Acme Corp Overview", "asserter": "scarlet", "assertion_type": "fact" }, "documents": [ { "document_id": "doc-2222", "title": "Acme Corp Overview", "source_url": "https://example.com/acme", "source_type": "web", "excerpt": "Acme Corporation was founded in 1990 by John Smith", "content": "Acme Corporation was founded in 1990 by John Smith. The company is headquartered in San Francisco...", "confidence": 0.95 } ] } ], "duration_ms": 312 }The reviewer now has everything to sign off: the claim resolves to a
fact-type triple asserted byscarlet, grounded indoc-2222with the exact excerpt.duration_msconfirms it came in under the sub-500 ms IC budget. -
Handle an unsupported claim. If the memo asserts something the graph cannot back up,
matchescomes back empty --- a clear signal the statement is not grounded and should not be signed off:curl -X POST http://localhost:4000/knowledge/trace \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "claim": "Acme Corporation operates in 40 countries", "max_results": 5 }'{ "claim": "Acme Corporation operates in 40 countries", "matches": [], "duration_ms": 88 }An empty trace and a
NOT_MENTIONEDlabel from Tutorial 2'sClaimValidationnode are two views of the same gap --- the pipeline catches it before the memo is written, and the trace endpoint catches it again at sign-off. Together they close the loop from ingestion to investment-committee review.