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 $TOKEN is the access_token from login (15-minute lifetime --- re-login or refresh if it expires).
  • You never pass tenant_id in 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.
TutorialWhat you learnReference
1. Ingest knowledge & query itThe human-gated ingestion lifecycle and epistemic grounding in conversationsKnowledge Graph
2. Build & run an SPO persona pipeline flowWiring Scarlet -> Ayana -> Eliza -> Reagan with a claim-validation checkpointAPI Reference -> Flows
3. Trace a memo claim for sign-offWalking a claim back to its supporting triples and source documentsKnowledge 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.

  1. 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..."
    
  2. 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" }
    
  3. Approve the proposal (the human gate). Approval stamps approved_by/approved_at and 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"
    }
    
  4. Poll until ingestion completes. Status moves approved -> processing -> completed. The ingestion_result carries 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 to doc-2222 for provenance.

  5. 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" }
    
  6. 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" }
    
  7. 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 verification block 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 under ungrounded_claims instead of hiding them.

    To watch the gates and node steps in real time, POST to /conversations/cnv-4444/messages/stream instead and read the text/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:

PersonaRoleNode behavior
ScarletResearch --- writes triplesAgent then SpoWrite (persists her output as SPO triples with provenance)
AyanaAnalysis --- filters triplesAgent then SpoFilter (queries SPO triples into state, optionally by asserter)
ElizaDue diligence --- drafts the DDAgent
ReaganReview --- approves the outputAgent
---Cross-stage grounding checkClaimValidation (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.

  1. Create the flow definition. The graph is a ReactFlow {nodes, edges, viewport} object. Each node's data carries its type-specific config. Reuse $TOKEN from 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/generate builds one from a natural-language prompt instead (see API Reference -> AI Flow Generation).

  2. Execute the flow. The input object seeds the flow state --- here, the company under review. The executor walks the graph from Start, 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" }
    
  3. Poll the execution until it finishes. Status follows pending -> running -> completed (or failed/cancelled). If you had inserted a HumanInput node, it would pause at paused here and wait for POST /flows/executions/exe-7777/input.

    curl http://localhost:4000/flows/executions/exe-7777 \
      -H "Authorization: Bearer $TOKEN"
    
  4. Inspect the results. A completed execution exposes the per-node node_states (each with status, input, output) and the shared state blackboard. The ClaimValidation node'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_MENTIONED label 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).

  1. Trace the claim. Paste the memo statement verbatim. max_results caps 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
      }'
    
  2. Read the trace. Each match groups a supporting triple (with its asserter and assertion_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 by scarlet, grounded in doc-2222 with the exact excerpt. duration_ms confirms it came in under the sub-500 ms IC budget.

  3. Handle an unsupported claim. If the memo asserts something the graph cannot back up, matches comes 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_MENTIONED label from Tutorial 2's ClaimValidation node 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.