Skip to documentation

Use cases by industry

Use case: content & growth

A streaming service lives or dies on two questions no single screen answers: which content is actually retaining subscribers, and which cohort is quietly churning? At Lumina Media the answers are split across a catalog of title pages, a warehouse of viewership data, and a strategy nobody wrote down in one place. This guide builds that content-and-growth analyst end to end across seven module slices: Intake to ingest the catalog, Atlas to type titles and cohorts, Warrant to certify what's quotable and abstain on hype, Conduit to pull live churn from the analytics warehouse, Lens to answer in plain language with citations, Lineage to trace any answer to its source, and Crucible to keep it accurate as the slate changes.

One knowledge base, seven slices. The dashed edge is the loop that keeps it honest: a regression the golden set catches steers the next round of Warrant curation.

Follow along in the seeded workspace

Everything below runs against real content. Seed it with yarn demo:seed media and it appears as the Lumina Media (Streaming) workspace — 16 densely-wikilinked documents, a typed ontology, a read-only analytics connector, the content-growth-analyst agent, a golden set, sample answer-policy decisions, and alerts. The seed is deterministic and idempotent (no LLM calls). See Demo workspaces for the full catalog.

Prerequisites

  • A workspace key with kb:write, kb:read, data:read, data:run, agents:run, and runs:read, exported as $ELI_KEY. See Getting started.
  • Owner/admin on the workspace, to certify concepts and verify sources.
  • An enabled chat model, so /query can generate — otherwise 409 no-model-configured.

1 · Intake — ingest the catalog

Lumina's knowledge already exists as markdown: a company overview, a page per title, cohort and campaign docs, licensing memos, creator interviews. Post them as documents and each commits as searchable, citable text before detached graph enrichment begins. The demo:seed command does this for all 16 files; here is the shape of one call:

POST/api/v1/documentsBearer · kb:write

Save markdown through the vault. The file, chunks, and full-text index commit before 201; embedding and graph enrichment continue as detached work, so the response contains stored-document metadata only.

Example requestbash
curl -s -X POST https://your-host/api/v1/documents \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "titles/aurora-chronicles.md",
    "title": "Title — Aurora Chronicles",
    "content": "# [[Aurora Chronicles]]\n\nThe flagship original and cornerstone of the [[Aurora Universe]] franchise, created and run by [[Vera Ostrom]]. Owned IP, governed by [[Aurora Universe Rights]]. Highest [[Watch Time]] on the service; anchors [[Annual Plan Cohort]] retention."
  }'
Responsejson
{
  "docId": "01KY10AAAAAAAAAAAAAAAAAAAA",
  "path": "titles/aurora-chronicles.md",
  "contentHash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
  "title": "Title — Aurora Chronicles"
}

The bracketed names remain part of the source text; the 201 does not claim they have become graph rows. Detached enrichment can later connect them. As the other documents land — growth/subscriber-cohorts.md, growth/churn-analysis-q2.md, licensing/gilded-hour-license.md — the same links stitch Aurora Chronicles, the Winback Cohort, and The Gilded Hour License into one graph that Atlas then types.

2 · Atlas — type titles, franchises & cohorts

Wikilinks give you nodes; the ontology gives them meaning. Lumina's graph is typed with six entity types — Title, Franchise, Cohort, Campaign, LicensingRight, and Creator — connected by directed relations like part_of, created_by, targets, and licensed_under. That structure is what turns "which franchise does this title belong to?" into a graph walk rather than a keyword guess. Expand the neighborhood around the flagship title to see it:

GET/api/v1/entities/{id}/neighborhoodBearer · kb:read

The knowledge-graph frontier around one entity: its typed card plus directed edges out to depth hops. Here, Aurora Chronicles and everything one hop away.

Example requestbash
curl -s "https://your-host/api/v1/entities/01KZTITLEAURORACHRON00000/neighborhood?depth=1" \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "entity": { "id": "01KZTITLEAURORACHRON00000", "name": "Aurora Chronicles", "type": "Title" },
  "nodes": [
    { "id": "01KZFRANCHISEAURORAUNIV00", "name": "Aurora Universe", "type": "Franchise" },
    { "id": "01KZCREATORVERAOSTROM0000", "name": "Vera Ostrom", "type": "Creator" },
    { "id": "01KZCOHORTANNUALPLAN00000", "name": "Annual Plan Cohort", "type": "Cohort" }
  ],
  "edges": [
    { "srcEntityId": "01KZTITLEAURORACHRON00000", "dstEntityId": "01KZFRANCHISEAURORAUNIV00", "type": "part_of" },
    { "srcEntityId": "01KZTITLEAURORACHRON00000", "dstEntityId": "01KZCREATORVERAOSTROM0000", "type": "created_by" }
  ],
  "truncated": false
}

The ontology is the retrieval spine

Because Aurora Chronicles is a Title that is part_of the Aurora Universe and created_by Vera Ostrom, a later question about "the Aurora franchise" retrieves the flagship, the spinoff Skybound, and their shared creator together — not three unrelated string matches. Curate the vocabulary in Manage entities & relations.

3 · Warrant — certify the metric, abstain on the hype

Extraction lands concepts at authority machine_extracted — a starting point, not a source of truth. A renewal decision or a churn number has to be vouched for. In Warrant, certify the concept the growth answers key on — Subscriber Churn — so the assistant can state it without hedging:

POST/api/v1/entities/{id}/certifyBearer · kb:write

Owner/admin sign-off — authority certified. Also starts the review clock: nextReviewAt lands at now + the workspace's reviewIntervalDays, so a certified metric definition can't quietly go stale.

Example requestbash
curl -s -X POST https://your-host/api/v1/entities/01KZCONCEPTSUBCHURN000000/certify \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note": "Subscriber Churn = monthly churn rate by cohort from the Content Analytics Platform. Signed off by Theo Lindgren + Camille Roy."}'
Responsejson
{ "authority": "certified", "lifecycleStatus": "published", "version": 3, "nextReviewAt": "2027-01-19T09:10:00.000Z" }

Do the same for the source document that carries the finding — verify the Q2 churn analysis so its provenance carries a governance tier:

POST/api/v1/documents/{id}/verifyBearer · kb:write

Glean-style document verification. Stamps the verification tier, the verifier, and the next re-verify date — the governance block Lineage reports and the manifest counts.

Example requestbash
curl -s -X POST https://your-host/api/v1/documents/01KY2CHURNFINDING000000AA/verify \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"verification":"certified","note":"Q2 churn analysis reviewed with Camille Roy; current as of the latest cohort model"}'
Responsejson
{ "id": "01KY2CHURNFINDING000000AA", "verification": "certified", "verifiedAt": "2026-07-22T09:12:00.000Z", "nextVerifyAt": "2026-12-29T00:00:00.000Z" }

Governance is also what the assistant refusesto say. Lumina's answer policy is tuned to abstain on unbacked speculation, and the content-growth-analyst system prompt is explicit: never predict whether an unreleased title will succeed. Ask "Will the next Aurora Universe title be a hit?" and the seeded policy engine returns a real pre-generation decision rather than a guess.

The verdict on hype: abstain, with pointers

The seeded policy decision for that question is { verdict: "abstain_with_pointers", stage: "pre", confidence: 0.1, reasons: ["no_relevant_chunks"] }. No verified chunk supports a forecast about an unreleased title, so the policy stops before generation and points the reader at what the corpus canground. That is the behavior a content strategy demands — a confident wrong prediction is worse than a clear "I can't back that."

4 · Conduit — pull live churn from the warehouse

Documents say what the strategy is; the warehouse says what is happening right now. Conduit exposes the read-only lumina-analytics connector as governed named queries— reusable, parameterized SQL a human authored and bound onto entities. List what's available, then run the churn query for a specific cohort. Every execution writes an append-only data_calls row and hands back a dataCallId — the [Dn] anchor you can cite later.

GET/api/v1/data/queriesBearer · data:read

List named queries (domain components) available to run, with their parameter specs. Secrets never appear — you see slugs, titles, and params.

Example requestbash
curl -s https://your-host/api/v1/data/queries \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "queries": [
    {
      "slug": "churn-by-cohort",
      "title": "Churn rate by cohort",
      "description": "Monthly churn rate and subscriber count for a given cohort, broken out by plan tier, worst churn first.",
      "connectorName": "lumina-analytics",
      "params": [ { "name": "cohort", "type": "string", "required": true, "description": "Cohort to check, e.g. Winback Cohort" } ]
    },
    { "slug": "top-titles-by-hours", "title": "Top titles by streaming hours", "connectorName": "lumina-analytics", "params": [] },
    { "slug": "campaign-performance", "title": "Campaign performance by return on ad spend", "connectorName": "lumina-analytics", "params": [] }
  ]
}
POST/api/v1/data/queries/{slug}/runBearer · data:run

Execute a named query with validated params. Read-only, row-capped (maxRows 200), timed, and provenance-logged. Params are bound, never interpolated. Returns a dataCallId — the [Dn] provenance anchor.

Example requestbash
curl -s -X POST https://your-host/api/v1/data/queries/churn-by-cohort/run \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"params":{"cohort":"Winback Cohort"}}'
Responsejson
{
  "dataCallId": "01KY10CCCCCCCCCCCCCCCCCCCC",
  "connectorName": "lumina-analytics",
  "queryTitle": "Churn rate by cohort",
  "columns": ["cohort", "plan_tier", "churn_rate_pct", "subscribers"],
  "rows": [
    { "cohort": "Winback Cohort", "plan_tier": "Monthly", "churn_rate_pct": 11.4, "subscribers": 20431 },
    { "cohort": "Winback Cohort", "plan_tier": "Annual", "churn_rate_pct": 3.2, "subscribers": 4870 }
  ],
  "rowCount": 2,
  "truncated": false,
  "durationMs": 38,
  "executedAt": "2026-07-22T09:14:03.412Z"
}

Queries are bound to entities

churn-by-cohort is bound onto the Subscriber Churn concept (its cohort param sourced from the detected entity), top-titles-by-hours onto the Content Analytics Platform, and campaign-performance onto Ad Campaign Performance. That binding is what lets the next slice cross from a natural-language question into live SQL automatically. A slow or broken source becomes a logged data_calls status, never a hung request — see Query live data.

5 · Lens — the analyst asks, and gets a cited answer

Now the payoff. Ask the question a growth lead actually types; Lens detects the cohorts in the question, runs the bound churn-by-cohort query as a governed [Dn] lookup, and grounds the narrative in the certified Q2 finding as [Sn] — the data and document layers fused in one response. Pass includeData: true to cross the bridge:

POST/api/v1/queryBearer · agents:run

Run the grounded query pipeline: retrieval + entity detection + bound live-data lookups + one generation + a groundedness check + the answer-policy engine. Returns a dual-cited answer with a policy decision. The workspace comes from the key.

Example requestbash
curl -s -X POST https://your-host/api/v1/query \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question":"Why does the Winback Cohort churn faster than the Annual Plan Cohort?","includeData":true}'
Responsejson
{
  "answer": "The Winback Cohort is churning at 11.4% on the monthly tier [D1], well above the Annual Plan Cohort. It is reactivated by premiere-driven pushes like the Aurora Premiere Campaign, and those signups lapse again within 60 days of the Aurora Chronicles premiere week [S1]. The Annual Plan Cohort is on annual billing and anchored by the Aurora Universe, giving it the lowest churn on the service [S1].",
  "claims": [
    { "text": "Winback Cohort monthly churn is 11.4%", "citations": ["D1"] },
    { "text": "Reactivated winback signups lapse within 60 days of the Aurora Chronicles premiere", "citations": ["S1"] },
    { "text": "The Annual Plan Cohort has the lowest churn, anchored by the Aurora Universe", "citations": ["S1"] }
  ],
  "sources": [
    { "id": "S1", "docId": "01KY2CHURNFINDING000000AA", "docTitle": "Finding — Q2 Churn Analysis", "docPath": "growth/churn-analysis-q2.md", "chunkId": "01KY2CHURNCHUNK00000000BB" }
  ],
  "dataCalls": [
    { "id": "D1", "dataCallId": "01KY10CCCCCCCCCCCCCCCCCCCC", "queryTitle": "Churn rate by cohort", "connectorName": "lumina-analytics", "rowCount": 2, "executedAt": "2026-07-22T09:14:03.412Z" }
  ],
  "entities": [
    { "id": "01KZCOHORTWINBACK00000000", "name": "Winback Cohort", "type": "Cohort" },
    { "id": "01KZCONCEPTSUBCHURN000000", "name": "Subscriber Churn", "type": "concept", "authority": "certified" }
  ],
  "policy": { "decision": "answer", "confidence": "medium", "reason": "verified live data plus a certified Q2 finding, no conflicts" },
  "groundedness": { "verified": 3, "total": 3 },
  "model": { "chat": "claude-sonnet-4-5", "judge": "claude-haiku-4-5" }
}

The policy block is the trust signal, and it is grounded in the seeded decisions: the narrower "What is the churn rate for the Winback Cohort?" resolves to { verdict: "answer", confidence: 0.74 } because a verified live number backs it, while "Will the next Aurora Universe title be a hit?" still returns abstain_with_pointers. Certified metrics are stated plainly; unbacked forecasts are refused. The same pipeline is a native MCP tool, so it drops straight into the growth team's Claude or BI assistant.

6 · Lineage — trace the answer back to its source

A VP reviewing that answer asks the fair question: where is this coming from, and can I trust it? The answer carried two anchors — D1 (a logged query) and S1 → docId 01KY2C… (a document). Pull that document's lineage and read the whole trail in one call — origin, governance, and the downstream answers it is grounding right now:

GET/api/v1/documents/{id}/lineageBearer · kb:read or runs:read

The unified per-document lineage trail: origin provenance, governance, the audit trail, derived knowledge, and downstream impact. This is how you prove an answer's pedigree end to end.

Example requestbash
curl -s https://your-host/api/v1/documents/01KY2CHURNFINDING000000AA/lineage \
  -H "Authorization: Bearer $ELI_KEY" | jq '{origin: .origin, verification: .governance.verification, citedBy: .downstream.citedByAnswers}'
Responsejson
{
  "origin": {
    "kind": "upload",
    "path": "growth/churn-analysis-q2.md",
    "author": "Camille Roy",
    "ingestedAt": "2026-07-22T09:05:12.008Z"
  },
  "verification": "certified",
  "citedBy": 9
}

Closed loop: the narrative half of the answer traces to an uploaded finding authored by Camille Roy, now certified (the verify from slice 3), and it is currently grounding 9 answers — so an edit to it has a known blast radius. The numeric half has its own pedigree: D1 resolves to the append-only data_calls row from slice 4, recording the exact SQL, the cohortparam, and the row count. Both halves of the dual-grounded answer are auditable, and the VP didn't have to trust the model — they read the receipt.

Same trail, three surfaces

The workspace UI renders this exact DocumentLineage as a visual trail on the document page (via the BFF at GET /api/w/{workspaceId}/documents/{docId}/lineage), and an agent can pull it mid-run with the kb_document_lineage MCP tool. One shape, three surfaces.

7 · Crucible & Ports — lock the accuracy in, ship it as an agent

Content changes constantly — new premieres, shifting campaigns, expiring licenses — and a wrong churn number in a board deck is expensive. Capture the questions the team actually asks as a golden set so Crucible can prove the assistant still answers them after every content change. The seed ships a Content & growth Q&A baseline config anchored on three items — "Which franchise does Aurora Chronicles belong to?" (Aurora Universe), "Who created Little Comet?" (Mika Tran), and the medium "Why does the Winback Cohort churn faster than the Annual Plan Cohort?" — the last seeded with a source: "provenance" qrel derived from the Q2 finding chunk. Run the set and read the metrics:

GET/api/v1/evals/runs/{id}/metricsBearer · kb:read or runs:read

Retrieval, RAGAS, and abstention metrics for one run — overall and by facet. The ruler for 'did the assistant get worse after a content change?' Retrieval and abstention are deterministic; RAGAS carries the judge model that produced it.

Example requestbash
curl -s https://your-host/api/v1/evals/runs/01KZRUNA00000000000000000/metrics \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "runId": "01KZRUNA00000000000000000",
  "n": 24,
  "retrieval": { "recallAtK": 0.78, "ndcgAt10": 0.75, "mrr": 0.81, "hitAtK": 0.92 },
  "ragas": { "faithfulness": 0.92, "responseRelevancy": 0.85, "contextPrecision": 0.69 },
  "abstention": { "truthfulness": 0.64, "abstainRate": 0.13, "aurc": 0.071 }
}

The same grounded pipeline ships as a durable agent. The seeded content-growth-analyst (data-enabled, always cited, abstains without backing) drives the exact harness the UI uses, leaving a saved run trace behind every answer — the Ports surface a webhook, queue, or schedule can call:

POST/api/v1/agents/{slug}/runsBearer · agents:run

Start a run of the content-growth-analyst. With stream:false the request blocks and returns the final RunResult — text plus the citations the answer was grounded on — and persists an ordered, cost-accounted step trace.

Example requestbash
curl -s -X POST https://your-host/api/v1/agents/content-growth-analyst/runs \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input":"Why does the Winback Cohort churn faster than the Annual Plan Cohort?","stream":false}'
Responsejson
{
  "runId": "01KZAGENTRUNCONTENT0000AA",
  "status": "succeeded",
  "text": "The Winback Cohort is churning at 11.4% monthly [D1] — reactivated by premiere-driven pushes that lapse within 60 days [S1] — while the Annual Plan Cohort stays sticky on annual billing anchored by the Aurora Universe [S1].",
  "citations": [ { "docId": "01KY2CHURNFINDING000000AA", "chunkId": "01KY2CHURNCHUNK00000000BB" } ]
}

When a metric drops after a content update, the golden set names exactly which question regressed — and promoting that miss back into the set turns it into a permanent test. That is the dashed loop in the diagram: Crucible's regressions steer the next round of Warrant curation. And the workspace doesn't only answer when asked — the seed also ships a live churn.spike alert (Winback Cohort churn exceeded the 8% monthly threshold after the Aurora Premiere Campaign) and a license.expiring alert on The Gilded Hour License, so the number you just traced surfaces as an operational signal, not just a query result.

The leverage

Seven slices, one knowledge base, one audit trail. The growth lead gets a fast, dual-cited churn answer; the VP can trace it to a certified finding anda logged query, and see its blast radius; the policy refuses to guess whether an unreleased title will land; and the whole thing is measured, so accuracy is a number you watch rather than a hope. Swap streaming for support, curriculum, or claims and the shape is identical — the platform doesn't care what the documents are about, only that every answer is grounded, governed, traceable, and measured.

Keep going

Turn this into a scheduled deliverable with Automated reports, end to end, compare a retrieval or prompt change behind a significance test in Measure & compare, or browse the other starter domains in Demo workspaces.