03 — Identifiers & Naming Conventions
Purpose. This document is the single normative reference for every identifier the Knoll
Analyzer backend creates in or exchanges with the GRAG platform: the charset rules GRAG
enforces, the reserved ids we must avoid, the Knoll ID scheme (tenants, workspaces, KBs,
conversations, KG entities), the exact German-aware slugification algorithm, the grag_refs
mapping-table contract, idempotency semantics for provisioning, the canonical success-lever
slug list, and the X-Pipeline-Id convention for cost attribution. Provisioning code
(plan 3.1, 3.5), the KG writer (plan 3.6), and the cost rollup (plan 1.6/7.3) MUST follow
this document. Siblings: 01-architecture.md for the resource mapping
rationale, 04-provisioning-runbook.md for operator steps,
06-knoll-db-schema.md for the full DB schema around grag_refs,
07-kg-schema-knoll-advisory.md for the KG schema the
entity ids live in, 14-cost-model.md for the spend rollup that consumes
pipeline ids.
Status / verified against: 2026-07-06, repo document-processing-pipelines @ db63a95,
adversarial-review corrections applied (no Idempotency-Key support; ledger paths
/ledger/api/v1/ledger/*; kg-service has no entity delete route).
1. The DNS-label charset rule
GRAG validates almost every resource id against one regex:
^[a-z0-9][a-z0-9-]{0,62}$
Lowercase ASCII letters, digits, hyphens; must start with a letter or digit; 1–63 chars. No umlauts, no uppercase, no underscores, no dots, no colons. German names must be slugified by Knoll before provisioning (section 4).
Where the rule is enforced (verified in source):
| Identifier | Enforced by | Notes |
|---|---|---|
Tenant id (X-Tenant-ID header + POST /workspaces/api/v1/tenants body) | pipeline-common/src/pipeline_common/tenant_context.py:50; workspaces/src/workspaces/schemas.py:39-45 | Invalid header → 400. Missing header → silent fallback to tenant default (allow_default_tenant=true platform-wide) — packages/grag-client hard-fails instead (plan 2.5, 7.7) |
Workspace id (X-Workspace-ID header + workspace create body) | workspace_context.py:67; schemas.py | kg-service and entity-linking require the header on every non-public route (400 without) |
| Project id | scope_context.py:68; schemas.py | Not used by Knoll — no project layer (plan 0.1 / D1) |
| KB id | schemas.py | The KB id doubles as the voyager collection name — another reason it must be a clean slug |
Conversation id (client-chosen id in POST /workspaces/api/v1/conversations) | schemas.py:224-240 (_validate_id) | Server auto-generates conv-<8 urlsafe chars> when omitted (routers/conversations.py:46-50,146) — Knoll always chooses its own (section 3) |
| Document id | not regex-validated (DocumentCreate.id has no validator, schemas.py:310-326); server auto-generates doc-<8 chars> | Knoll never chooses document ids — the upload BFF creates them and returns documentId; we only store what we get back (invariant I3, section 5) |
| KG entity id | kg-service/src/kg_service/api/schemas/entities.py:13 — free string, 1–256 chars | NOT a DNS label: colons and unicode are allowed (extraction derives {type}:{slug} with umlauts preserved, e.g. location:münchen). Knoll follows the same extraction slug rule for label-derived entity ids (sections 3, 7) |
X-Pipeline-Id value | not validated anywhere (free string; read at ai-gateway/src/ai_gateway/api/routes/completion.py:78) | Keep it DNS-label-charset anyway for log/Grafana hygiene (section 8) |
Rule of thumb: everything Knoll sends as a resource id or scope header on the
workspaces plane (tenant, workspace, KB, conversation) is a DNS label produced by the
section-4 slugifier. KG entity ids are the one exception: {type}:{slug} in the
platform's extraction slug charset (unicode alphanumerics incl. umlauts, plus -) —
never the section-4 DNS-label slugifier (sections 3, 7). Nothing else.
2. Reserved ids — never create, never reuse
| Reserved id | Level | Why |
|---|---|---|
default | tenant | Platform fallback tenant — every request that forgets X-Tenant-ID lands here (allow_default_tenant=true). Data in it is unowned. |
system | tenant | Reserved for internal platform jobs (ADR 0001 §Tenant ID format). |
canary | tenant | Live synthetic-traffic tenant (24/7 canary probes); dashboards filter tenant!="canary". |
general | workspace | Auto-created in every tenant on POST /tenants, undeletable (409 on DELETE). Knoll uses it (it hosts kb-methodology) but must never try to create or delete it. |
Plan 1.3 codifies this for tenant creation. Additionally, treat every id prefix in
section 3 as reserved within the Knoll tenant: do not hand-create resources matching
client-*, kb-analysis-*, conv-* outside the provisioning module, or the
already-provisioned detection (section 6) breaks.
3. The Knoll ID scheme
<shortid> = the first 8 hex characters of the owning Knoll-DB row's UUID primary key
(lowercase, hyphens removed). It is derived once at provisioning time and stored in
grag_refs — never re-derived. On a grag_refs unique-constraint collision (≈2⁻³² per
pair, effectively never at pilot scale) extend to the first 12 hex chars and retry.
<slug> = output of the algorithm in section 4, capped at 32 chars.
| Knoll concept | GRAG resource | ID pattern | Example | Plan |
|---|---|---|---|---|
| Firm (platform customer) | tenant | knoll (pilot single tenant); later firm-<slug>; dev/staging knoll-dev | knoll, firm-mueller, knoll-dev | 0.1, 1.3, 2.8 |
| Client | workspace | client-<slug>-<shortid> | client-hartmann-maschinenbau-9f3ab2c1 | 3.1 |
| Analysis (engagement) | KB in the client workspace | kb-analysis-<shortid> | kb-analysis-4b0c77de | 3.1 |
| Methodenhandbuch / firm knowledge | KB in general | kb-methodology (fixed singleton) | kb-methodology | 3.3 |
| Chat thread | conversation | conv-<purpose>-<shortid> with purpose ∈ file | acquisition | iu | conv-file-c41f09aa | 5.1a, 5.1b, 5.2 |
| Uploaded document | workspaces document | GRAG-assigned (doc-<8> from the upload BFF response documentId) — never composed by Knoll | doc-x1y2z3a4 | 4.1 |
| Ingest job | orchestrator job | GRAG-assigned (jobId from POST /next/api/ingest/start) | — | 4.1, 4.2 |
| Success lever (11, closed taxonomy) | KG entity | lever:<slug> | lever:market-position | 3.5 |
| Service provider (catalog) | KG entity | service-provider:<slug> — extraction slug of name (section 7), derived once at first sync, persisted, never re-derived (07 §4.3); collision suffix per section 4 | service-provider:digital-wings-kg | 3.5 |
| Client (graph node) | KG entity | client:<slug>-<shortid> — same <slug>-<shortid> suffix as the workspace id | client:hartmann-maschinenbau-9f3ab2c1 | 3.6 |
| Recommendation | KG entity | recommendation:<analysis-shortid>-<n>, n = 1-based position in the approved expert-report version | recommendation:4b0c77de-3 | 3.6 |
| Project (graph node) | KG entity | project:<project-shortid> | project:77aa41b0 | 3.6, 5.5 |
| Metric | KG entity | metric:<client-shortid>-<metric>-<year> — per-client fact instance (value/unit/year ride in properties), not a closed taxonomy; instance-id rule in 07-kg-schema-knoll-advisory.md §5.1 | metric:9f3ab2c1-revenue-2025 | 3.6 |
| Cost-attribution run | X-Pipeline-Id header value | knoll-<scope>-<step>-<runid> (section 8) | knoll-4b0c77de-scoring-4e21ac03 | 1.6, 7.3 |
Notes:
- KG entity ids use
:as the type separator only (one colon, immediately after the lowercase entity-type prefix). Everything after the colon obeys the extraction slug charset (unicode alphanumerics — umlauts survive — plus-; section 7), not the section-4 DNS-label charset. Knoll caps entity ids at 120 chars (= the extraction slugifier's own cap,SLUG_MAX_CHARS; the platform hard limit is 256). - The
lever:/service-provider:/client:/recommendation:/project:/metric:prefixes match the lowercaseentity_typevalues of theknoll-advisoryschema v1 (SuccessLever,ServiceProvider,Client,Recommendation,Project,Metric— plan 3.4), mirroring the platform's own extraction convention{entity_type_lower}:{slug}(kg-service/src/kg_service/workers/extraction.py:85-89). - Two slugifiers — do not mix them. Label-derived dictionary entities
(
lever:<slug>,service-provider:<slug>) use GRAG's own extraction slugifier (extraction.py:70-82: NFKC → lowercase → whitespace→-→ strip non-alphanumerics → strip outer-, cap 120; umlauts survive, e.g.München→münchen) — precisely so auto-extracted document mentions (D7 allows entity-only extraction) dedupe onto the seeded entities instead of minting duplicates (07 §4.1). The section-4 DNS-label slugifier applies only to workspaces-plane ids (tenant/workspace/KB/conversation) — never to KG entity ids. The shortid-embedding graph nodes (client:,recommendation:,project:,metric:) reuse Knoll-composed suffixes (theclient:slug is copied verbatim from the workspace id, section-4 transliteration included) and can never match an extraction-derived id — auto-extraction may therefore still create a stray for the same surface form (client:mullervs. Knoll'sclient:hartmann-maschinenbau-9f3ab2c1). Consumers treat Knoll DB +grag_refsas canonical and the reconcile job (plan 3.6) tombstones strays viaproperties.status="inactive"— there is no entity delete route (plan 3.5). - Because the KG has no delete route, entity ids are forever: never recycle a slug for a different real-world object.
4. Slugification algorithm (normative)
Scope: this DNS-label slugifier applies only to workspaces-plane ids — tenant,
workspace, KB, conversation — and, by extension, to the <slug>-<shortid> suffix that
client: graph nodes copy verbatim from the workspace id. It is never used to derive
label-based KG entity ids (lever:, service-provider:): those use the platform's
extraction slugifier (section 7), which preserves umlauts and produces different output.
Input: any German (or other) display name. Output: a DNS-label-safe slug. The step order
is load-bearing (umlaut transliteration must precede accent stripping, otherwise
ä → a instead of ae).
- Unicode-normalize NFKC.
- Lowercase.
- Transliterate German characters:
ä→ae,ö→oe,ü→ue,ß→ss. - Unicode-normalize NFD and strip combining marks (handles
é→e,à→a,ç→c, …). - Replace every run of characters outside
[a-z0-9]with a single-. - Trim leading/trailing
-. - Truncate to the max length (default 32 chars), then trim trailing
-again. - If the result is empty (nothing alphanumeric survived), the caller MUST fall back to
the
<shortid>alone (id families that have one) or reject with a validation error (families that don't).
Collision handling:
- Id families that embed a
<shortid>(client-…,recommendation:…,project:…,metric:…) are unique by construction — no suffix needed. - Slug-only families (
firm-<slug>,service-provider:<slug>): before provisioning, checkgrag_refsfor the candidate id; on collision append-2,-3, … (before truncation-safety: the suffix counts toward the length cap — truncate the slug first, then append). Note: the candidate slug forservice-provider:comes from the extraction rule (section 7), not from this section's algorithm — only the collision protocol is shared.
Length budgets (full id ≤ 63 for DNS-label resources):
| Family | Fixed parts | Slug budget |
|---|---|---|
firm-<slug> | 5 | 32 (37 available; 32 by convention) |
client-<slug>-<shortid> | 7 + 1 + 8 = 16 | 32 (total ≤ 48) |
kb-analysis-<shortid> | 17 total | — (no slug) |
conv-<purpose>-<shortid> | ≤ 16 total | — (no slug) |
KG entity <type>:<slug…> | type prefix + : | up to 120 total |
Reference implementation (TypeScript — lives in packages/grag-client, plan 2.5):
const DE_MAP: Record<string, string> = { "ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss" };
export function slugify(input: string, maxLen = 32): string {
let s = input.normalize("NFKC").toLowerCase();
s = s.replace(/[äöüß]/g, (c) => DE_MAP[c]); // ä ö ü ß
s = s.normalize("NFD").replace(/[̀-ͯ]/g, ""); // strip accents: é → e
s = s.replace(/[^a-z0-9]+/g, "-"); // replace + collapse
s = s.replace(/^-+|-+$/g, "");
s = s.slice(0, maxLen).replace(/-+$/g, "");
return s; // "" → caller falls back to shortid or rejects
}
Worked examples (the acceptance fixtures for the unit test):
| Input | Slug |
|---|---|
Kanzlei Müller | kanzlei-mueller → tenant firm-mueller (drop the redundant word if you prefer: slugify only the proper name) |
Hartmann Maschinenbau GmbH | hartmann-maschinenbau-gmbh |
Bäckerei Großmann-Söhne GmbH & Co. KG | baeckerei-grossmann-soehne-gmbh (truncated at 32, trailing - trimmed) |
Produkte & Dienstleistungen | produkte-dienstleistungen — DNS slug only; the KG entity id is lever:products--services (extraction rule, section 7) |
Planung & Controlling | planung-controlling — DNS slug only; the KG entity id is lever:planning--controlling (extraction rule, section 7) |
Café René | cafe-rene |
Straßenbau Weiß | strassenbau-weiss |
株式会社 (nothing survives) | "" → shortid fallback / reject |
Slugs are frozen at provisioning: renaming a client in Knoll DB updates display names
(Knoll DB + the GRAG resource's name field via PATCH) but never the id — GRAG ids are
immutable, and slug drift is accepted (invariant I2 below).
5. The grag_refs table contract
grag_refs is the only authoritative mapping between Knoll rows and GRAG resources
(plan 2.1). Full schema context in 06-knoll-db-schema.md; the
contract:
CREATE TABLE grag_refs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
knoll_type text NOT NULL, -- 'firm'|'client'|'analysis'|'methodology'|'file'
-- |'questionnaire_doc'|'expert_report_doc'|'conversation'
-- |'kg_entity'|'kg_schema'|'ingest_job'
knoll_id uuid, -- PK of the owning Knoll row (NULL for singletons
-- like kb-methodology)
grag_kind text NOT NULL, -- 'tenant'|'workspace'|'kb'|'document'
-- |'conversation'|'entity'|'schema'|'job'
grag_tenant text NOT NULL, -- 'knoll' | 'knoll-dev' | 'firm-<slug>'
grag_workspace_id text, -- set for kb/document/conversation/entity refs
grag_kb_id text, -- set for document/conversation refs
grag_id text NOT NULL, -- the resource id in GRAG — NEVER parsed
sync_status text NOT NULL DEFAULT 'pending',
last_error text,
payload_sha256 text, -- hash of last-pushed projection content
-- (KG/markdown re-push short-circuit)
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (grag_tenant, grag_kind, grag_id),
UNIQUE (knoll_type, knoll_id, grag_kind)
);
sync_status lifecycle:
pending ──► creating ──► ready ──► deleting ──► deleted
│ │
▼ ▼
failed superseded (documents only: replaced via
│ POST /documents/{id}/supersede, plan 4.4/4.5)
└──► creating (retry)
pending— row written in the same Knoll-DB transaction as the domain row; no GRAG call made yet.creating— GRAG call in flight (crash-safe: the reconcile sweep re-drives any row stuck inpending/creating/failed).ready— GRAG confirmed (2xx, or 409 = already provisioned, section 6).failed— non-retryable error captured inlast_error; operator surface in 11-resilience-and-errors.md.superseded— the GRAG document was replaced; the successor has its own row.deleting/deleted— de-provisioning fan-out (plan 3.2) in flight / confirmed.
Invariants:
- I1 — Write-ahead: every GRAG write is preceded by a
grag_refsrow (pending) in the same transaction as the Knoll domain write. A crash between DB commit and GRAG call leaves a re-drivablependingrow, never an untracked GRAG resource. - I2 — GRAG ids are never parsed for meaning. The embedded slugs and shortids exist
for operator grep-ability only. All joins Knoll→GRAG and GRAG→Knoll go through
grag_refs(knoll_type+knoll_id→grag_id, orgrag_id→knoll_id). Never extract a shortid from agrag_id, never reconstruct agrag_idfrom a Knoll row outside the provisioning module. - I3 — GRAG-assigned ids are stored verbatim.
documentIdfromPOST /next/api/upload/startandjobIdfromPOST /next/api/ingest/startare opaque strings; record them (grag_kind='document'|'job') and pass them back unchanged. - I4 — Single mapping point. Other Knoll tables (e.g.
files.grag_document_id,ai_runs.pipeline_id) may hold denormalized copies for query convenience, but sync state lives only ingrag_refs, and the reconcile job (plan 3.6) reads onlygrag_refs. - I5 — No cross-tenant refs:
grag_tenanton every row; every GRAG call resolves itsX-Tenant-IDfrom the ref row (dev rows carryknoll-dev, prod rowsknoll), never from global config alone.
6. Idempotency rules
The workspaces service offers exactly one idempotency mechanism: client-chosen ids +
409 Conflict on duplicate (tenant, id).
⚠ There is NO
Idempotency-Keyheader.workspaces/CLAUDE.mdmentions accepting one, but a grep over the workspaces and pipeline-common source trees finds zero implementation — the header is silently ignored (adversarial review finding [5], overriding the exploration reports). Do not send it, do not rely on it.
Provisioning protocol (plan 3.1):
- Derive the deterministic id (section 3), write the
grag_refsrow (pending). POSTthe create with the client-chosenid.201→sync_status='ready'.409→ already provisioned (a previous attempt succeeded).GETthe resource to confirm it matches expectations (name, parent workspace), thensync_status='ready'.- Any other error →
failed+last_error; the reconcile sweep retries from step 2 — safe because the id never changes between attempts.
# Create the client workspace — idempotent by client-chosen id (dev tenant shown)
curl -sS -X POST "$GRAG_URL/workspaces/api/v1/workspaces" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: knoll-dev" \
-H "Content-Type: application/json" \
-d '{"id": "client-hartmann-maschinenbau-9f3ab2c1",
"name": "Hartmann Maschinenbau GmbH"}'
# 201 Created -> grag_refs.sync_status = ready
# 409 Conflict -> already provisioned: GET it, verify, then ready
curl -sS "$GRAG_URL/workspaces/api/v1/workspaces/client-hartmann-maschinenbau-9f3ab2c1" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: knoll-dev"
The same protocol applies to KBs (POST /workspaces/api/v1/workspaces/{ws}/kbs with
id: "kb-analysis-<shortid>") and conversations. Conversations MUST be created explicitly
before the first chat turn — the chat BFF does not create them; a made-up
conversation_id streams fine but silently never persists (plan 5.2, review finding [6]):
curl -sS -X POST "$GRAG_URL/workspaces/api/v1/conversations" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: knoll-dev" \
-H "Content-Type: application/json" \
-d '{"id": "conv-file-c41f09aa",
"kb_id": "kb-analysis-4b0c77de",
"workspace_id": "client-hartmann-maschinenbau-9f3ab2c1",
"title": "Frag die Akte — Hartmann Maschinenbau"}'
KG entity writes are idempotent by design: POST /kg-service/api/v1/entities/upsert is an
upsert (response reports {inserted, updated}), and the AGE mirror uses MERGE. Re-running
the deterministic KG writer (plan 3.6) with the same ids is always safe — that is what the
reconcile job exploits.
7. Canonical success-lever slugs & KG entity-id derivation
Source of truth for labels and order: SUCCESS_LEVERS in
next-monorepo/apps/web/lib/types.ts (11 success levers of the Knoll methodology, fixed order).
Derivation rule: entity_id = "lever:" + extraction_slugify(label) — the
platform extraction slugifier, not the section-4 DNS slugifier: NFKC-normalize →
lowercase → join whitespace runs with - → strip every char that is not a unicode
alphanumeric or - → strip outer - → cap 120 chars
(kg-service/src/kg_service/workers/extraction.py::slugify;
07 §4.1). Umlauts survive, and because & is stripped
after whitespace joining, "Products & Services" yields a double hyphen.
Seeds MUST use this rule so auto-extracted document mentions dedupe onto the seeded
entities instead of minting duplicates. The section-4 DNS-label slugifier applies only to
workspaces-plane ids (tenant/workspace/KB/conversation), never to KG entity ids.
The English label (with &, umlauts) is preserved verbatim in the entity's label field.
| # | Success lever (label) | Slug | KG entity id |
|---|---|---|---|
| 1 | MarketPosition | market-position | lever:market-position |
| 2 | Strategy | strategy | lever:strategy |
| 3 | Brand | brand | lever:brand |
| 4 | Competition | competition | lever:competition |
| 5 | ProductsAndServices | products--services | lever:products--services |
| 6 | Pricing | pricing | lever:pricing |
| 7 | Customers | customers | lever:customers |
| 8 | Sales | sales | lever:sales |
| 9 | PlanningAndControlling | planning--controlling | lever:planning--controlling |
| 10 | MarketingImplementation | marketing-implementation | lever:marketing-implementation |
| 11 | Personnel | personnel | lever:personnel |
This is a closed taxonomy: exactly these 11 ids are seeded once via
POST /kg-service/api/v1/entities/upsert (plan 3.5) and referenced by every SCORES /
ADDRESSES edge (plan 3.6). New levers do not appear without a methodology change; retired
ones are tombstoned (properties.status="inactive"), never deleted (no delete route exists).
Seeding example (kg-service requires X-Workspace-ID; entities are tenant-wide per
ADR 0025 Amendment, so seed once via the general workspace):
curl -sS -X POST "$GRAG_URL/kg-service/api/v1/entities/upsert" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: $GRAG_TENANT" \
-H "X-Workspace-ID: general" \
-H "Content-Type: application/json" \
-d '{"entities": [{
"entity_id": "lever:market-position",
"entity_type": "SuccessLever",
"label": "MarketPosition",
"aliases": [],
"description": "Success lever 1 of the Knoll methodology: the company's position in the market.",
"properties": {"order": 1, "status": "active", "source": "knoll-methodik"}
}]}'
# → {"inserted": 1, "updated": 0}
Service-provider entities follow the same derivation rule (service-provider: + extraction slug
of name, e.g. "Digital Wings KG" → service-provider:digital-wings-kg — 07 §4.3;
derived once at first sync, persisted in the Knoll DB row, never re-derived; on a rare
collision append -2, -3, … per the section-4 collision protocol); Knoll DB remains the
master, the KG is a projection (plan 3.5).
8. X-Pipeline-Id — cost-attribution convention
The ledger attributes spend per run via the X-Pipeline-Id request header: ai-gateway's
completion routes read it (ai-gateway/src/ai_gateway/api/routes/completion.py:78,122,
responses.py:111) and stamp it into every cost entry
(pipeline_common/ledger.py:30,131). Per-run totals are then queryable at
GET /ledger/api/v1/ledger/totals?pipeline_id=… — note the doubled ledger path segment
(Traefik prefix /ledger + internal /api/v1/ledger/totals; review finding [4]).
GET /ledger/api/v1/ledger/spend?group_by=service|provider is tenant-level only — the
only two groupings that exist; per-analysis attribution is impossible without pipeline ids.
Format (normative):
pipeline_id := "knoll-" <scope> "-" <step> "-" <runid>
scope := <analysis-shortid> ; analysis-scoped runs (expert-report pipeline, KIU, …)
| "agent-" <nn> ; agent runs without an analysis (nn = AI-agent
; number from the ai_agents registry, e.g. "02")
step := "interview" | "document" | "swot" | "scoring" | "report" | "report"
| "classification" ; checklist classification (plan 4.3)
| "leadscore" ; acquisition lead scoring (plan 5.1a)
| "matching" ; service-provider rationale (plan 5.4)
| "project" ; project brief (plan 5.5)
runid := 8 hex chars = shortid of the ai_runs row
Charset stays within [a-z0-9-], total length ≤ 63 (not enforced by GRAG — kept for
log/metric hygiene). Examples:
knoll-4b0c77de-scoring-4e21ac03— expert-report step 4 (Scoring) of analysis4b0c77deknoll-agent-02-leadscore-b7d2e910— acquisition lead scoring, no analysis
ai_runs.step → <step> token mapping (normative). The well-known ai_runs.step
values (06-knoll-db-schema.md §3.12) use underscores, which are
illegal in the [a-z0-9-] pipeline-id charset — never embed them directly; map them:
ai_runs.step (06 §3.12) | pipeline-id <step> token |
|---|---|
interview, document, swot, scoring, report, report | identical (already charset-clean) |
checklist_classification | classification |
lead_scoring | leadscore |
service_provider_rationale | matching |
project_brief | project |
Rules:
- One pipeline id per
ai_runsrow, stored inai_runs.pipeline_idat run start. Stamp it on every direct GRAG call the run makes (ai-gatewaychat/completions, groundednessscore, kg-service writes) — services that don't read it simply ignore it. - Per-analysis rollup is a Knoll-side sum (plan 7.3 / 14-cost-model.md):
select all
ai_runs.pipeline_idfor the analysis, call/ledger/totalsonce per id, sum./totalstakes exactly onepipeline_id— there is no prefix or wildcard query. - BFF-lane traffic carries no Knoll pipeline id. The chat/upload/ingest BFF routes do
not forward
X-Pipeline-Id(grep overfrontend-next/apps/web/app/api: zero matches) — chat spend lands in tenant-level/spendonly. - Ingest jobs use the GRAG-assigned
jobIdfromPOST /next/api/ingest/startas their pipeline id (the orchestrator stamps it downstream; the ledger client's own docstring pairs the budget poller withtotals(pipeline_id=…),pipeline-common/src/pipeline_common/client/ledger.py:6). Record thejobIdingrag_refs(grag_kind='job') and query/totalswith it verbatim — do NOT compose aknoll-…id for ingest. TODO-VERIFY: run one live ingest againstknoll-devand confirmGET /ledger/api/v1/ledger?pipeline_id=<jobId>returns the chunking/embedding cost rows for that job (i.e. that the orchestrator's downstream cost entries really carry the jobId aspipeline_id). - ai-gateway's
/embedand/rerankroutes do not readX-Pipeline-Id(grep overai-gateway/src/ai_gateway/api/routes: onlycompletion.py/responses.py/ledger.pymatch) — retrieval/embedding spend inside a run is attributable per tenant and service, not per run. TODO-VERIFY: whether per-run retrieval attribution matters for 14-cost-model.md; if yes, file a platform gap alongside plan 1.14 (small, additive: read the header in the embed/rerank routes).
Query examples:
# Per-run totals (expert-report Scoring step of analysis 4b0c77de)
curl -sS "$GRAG_URL/ledger/api/v1/ledger/totals?pipeline_id=knoll-4b0c77de-scoring-4e21ac03" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: $GRAG_TENANT"
# Tenant-level budget display (Settings → AI, plan 6.6)
curl -sS "$GRAG_URL/ledger/api/v1/ledger/spend?group_by=service" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: $GRAG_TENANT"
Stamping example (expert-report pipeline step, direct ai-gateway lane):
curl -sS -X POST "$GRAG_URL/ai-gateway/api/v1/chat/completions" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: $GRAG_TENANT" \
-H "X-Workspace-ID: client-hartmann-maschinenbau-9f3ab2c1" \
-H "X-Pipeline-Id: knoll-4b0c77de-scoring-4e21ac03" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "…"}]}'
(X-Workspace-ID is optional on ai-gateway but enables keyvault workspace-scoping and
attribution — send it whenever the run is client-scoped; see
02-grag-api-cookbook.md.)
Quick checklist for reviewers of provisioning PRs
- Every new id matches its section-3 pattern and passes
^[a-z0-9][a-z0-9-]{0,62}$(KG entities exempt: one:, extraction-slug charset — umlauts allowed — ≤120 chars). - No id in {
default,system,canary,general} is created or deleted. - Workspaces-plane slugs produced only by the section-4 function (single shared
implementation in
packages/grag-client); label-derived KG entity slugs only by the extraction-rule function (section 7 / 07 §4.1) — never cross-wired; umlaut and&(double-hyphen) fixtures in the unit tests for both. -
grag_refsrow written before the GRAG call (I1); 409 handled as already-provisioned; noIdempotency-Keyheader anywhere. - No code parses a
grag_id(I2); GRAG-assigneddocumentId/jobIdstored verbatim (I3). - Every direct GRAG call from an
ai_runsstep stampsX-Pipeline-Idper section 8.