Skip to documentation

Module references

Warrant — authority, lifecycle & gated change

Warrant is the module that answers who vouches for this, and who is allowed to change it. Adopt it alone if you already have concepts or documents somewhere and you need a review queue, an authority tier, an owner, and a trail you can defend.

What it does

Warrant tracks four things about a concept: its authority tier (machine_extractedasserted curatedcertified), its lifecycle (draft, pending_review, published, deprecated, superseded), its owner and stewards, and its review clock. Destructive or schema-level edits do not happen in place: they are proposed as change requests that a reviewer approves, rejects, or that the proposer withdraws. Every accepted change appends an immutable revision carrying a full snapshot and a field-level diff. Documents get a parallel, simpler treatment — a verification stamp with a re-verify due date.

Nothing in this module calls a model. All of it is transactional database work inside a workspace transaction.

Use it standalone

  1. Issue a key with the scopes Warrant actually checks

    Reads (the change-request queue, the revision trail) require kb:read. Every mutation — propose, approve, reject, withdraw, certify, review, set authority, set owner, deprecate, verify a document — requires kb:write. There is no governance:* scope; the scope taxonomy is agents:run, runs:read, kb:read, kb:write, data:read, data:run, reports:read, reports:write.

    Delegated user keys (eli_uk_…) are additionally narrowed by the owner's live workspace role: these routes name the capability governance:read or governance:write, and a 403 insufficient_capability comes back if the owner has lost it. Workspace keys (eli_sk_…) carry no role, so the capability check does not apply to them.

  2. Install two packages and import one entry

    Headless: @eli-ai/client/governance. React: @eli-ai/react/governance. Neither pulls in query, agents, data, reports, connectors or evals code.

  3. Call it

    The client is a thin typed wrapper over /api/v1. The transport takes an origin, a credential resolver and an optional workspace id; it makes no authorization decision of its own.

install (headless only)bash
npm install @eli-ai/client @eli-ai/contracts
propose → approve → read the trailts
import { createEliTransport } from "@eli-ai/client/core";
import { createGovernanceClient } from "@eli-ai/client/governance";

const transport = createEliTransport({
  baseUrl: "https://eli.ai",
  apiKey: () => process.env.ELI_API_KEY!, // eli_sk_… or eli_uk_…
  // workspaceId is only needed for a user key that can reach several workspaces
});

const governance = createGovernanceClient(transport);

// 1 — propose a gated edit instead of writing it directly
const proposal = await governance.proposeChangeRequest({
  kind: "update",
  entityId: "01KZ40AAAAAAAAAAAAAAAAAAAA",
  payload: { description: "Recurring revenue retained from existing customers over 12 months." },
  note: "Aligning the definition with the FY26 board deck",
});

// 2 — a reviewer applies it. Approve returns the request AND the entity it landed on.
const { request, appliedEntityId } = await governance.approveChangeRequest(proposal.id, {
  note: "Matches the finance definition",
});

// 3 — the append-only trail, newest first
const { revisions } = await governance.listEntityRevisions(appliedEntityId!, { limit: 10 });
console.log(request.status, revisions[0]?.changeKind, revisions[0]?.diff);

What Warrant does NOT require

No agent runtime, no retrieval or reranker configuration, no data connector, no report or eval setup, and no model provider — none of these code paths perform an LLM, network or file call. It does need somewhere for the subject to exist: a change request of kind update, deprecate, supersede or delete must name an entity that is live in the workspace (404 otherwise), and POST /documents/{id}/verify needs a live document. Kind create is the one path that needs no pre-existing row — it proposes the concept and creates it on approval.

React components

The React package exports exactly one component for this module, plus its two types. EliGovernance is a client component ("use client") that renders the open change-request queue and, optionally, a proposal form. It uses scoped eli-* class names and ships no styling of its own — import @eli-ai/react/styles.css or provide your own rules for those classes.

EliGovernance

What it renders, top to bottom:

  • a titled surface with a Propose change / Cancel toggle button (only when canPropose);
  • when toggled open, a form with a kind select (create, update, deprecate, supersede, delete), an Entity ID input (required for every kind except create), a Review note textarea, and a Proposed fields (JSON) textarea that must parse to a JSON object — an array, a scalar or invalid JSON is rejected inline before any request is sent;
  • one card per open request: a kind badge (critical tone for delete, warning otherwise), the entity name — falling back to the entity id, then to "New concept" — the created date, the request note, the names of the keys in payload, and the proposer;
  • when canReview, a per-request note input plus Approve and Reject buttons; a non-empty note is sent as the request body, an empty one is omitted;
  • loading, error (with a retry button) and empty states.

It calls four operations only: listChangeRequests({ status: "open" }) on mount and after every successful mutation, proposeChangeRequest, approveChangeRequest and rejectChangeRequest. The status filter is fixed to open and is not a prop.

EliGovernance props

labelsPartial<EliGovernanceLabels>Per-string overrides merged over the defaults. See the labels table below for the full key set.
canReviewbooleanRenders the per-request note input and the Approve/Reject buttons. Defaults to true.
canProposebooleanRenders the Propose change toggle and its form. Defaults to true.
onRequestReviewed(requestId: string, decision: "approved" | "rejected") => voidFired after an approve or reject resolves successfully, once the queue has been refetched.
onRequestProposed(request: ChangeRequest) => voidFired after a proposal resolves, with the created request. The form is reset and closed first.
titlestringOverrides the surface heading. Falls back to labels.title.
descriptionstringOverrides the sub-heading. Falls back to labels.description.
loadingLabelstringText next to the spinner. Defaults to "Loading governance queue…".
errorLabelstringText in the queue error state. Defaults to "The governance queue could not be loaded.".
onError(error: unknown) => voidCalled for the queue read and for every mutation failure.
classNamestringAppended to the root element, which always carries "eli-root".

emptyLabel is deliberately absent

EliGovernanceProps is Omit<CommonSurfaceProps, "emptyLabel"> — the empty state is driven by labels.empty instead. Passing emptyLabel is a type error.

EliGovernanceLabels — every key, with its default

titlestring"Governance queue"
descriptionstring"Review proposed knowledge changes before they alter the canonical graph."
openstring"Open"
proposedBystring"Proposed by"
approvestring"Approve"
rejectstring"Reject"
proposestring"Propose change"
cancelstring"Cancel"
kindstring"Change type"
entityIdstring"Entity ID"
notestring"Review note" — used for both the proposal note and the per-request review note
payloadstring"Proposed fields (JSON)"
submitstring"Submit proposal"
emptystring"No open change requests"
provider wiring + the componenttsx
import { createEliTransport } from "@eli-ai/client/core";
import { EliProvider } from "@eli-ai/react/provider";
import { EliGovernance } from "@eli-ai/react/governance";
import "@eli-ai/react/styles.css";

// EliProvider has no API-key prop by design. In the browser, point the transport at a
// same-origin route of your own that injects the Bearer key server-side.
const transport = createEliTransport({ fetch: sameOriginProxyFetch });

export function ReviewQueue({ workspaceId, isSteward }: { workspaceId: string; isSteward: boolean }) {
  return (
    <EliProvider transport={transport} workspaceId={workspaceId}>
      <EliGovernance
        canReview={isSteward}
        labels={{ title: "Change review", empty: "Nothing waiting on you" }}
        onRequestReviewed={(id, decision) => track("cr_reviewed", { id, decision })}
        onError={(error) => report(error)}
      />
    </EliProvider>
  );
}

Never ship a workspace key to the browser

eli_sk_ keys are workspace-wide. EliProvider takes a transport, not a credential, precisely so the key stays server-side — see the SDK page for the proxy pattern.

What the React surface does not cover yet

EliGovernance has no UI for withdraw, for certify / review / set authority / set owner / deprecate, for the revision trail, or for document verification, and it cannot list anything other than open requests. All of those are available on the headless client (createGovernanceClient) and over HTTP — build your own markup with useEliQuery / useEliMutation from @eli-ai/react/hooks if you need them in React.

HTTP API

Twelve operations. Every one authenticates with a Bearer key; the scope column below is what the route enforces, and delegated user keys additionally need the named capability. All examples use https://eli.ai with -L on non-GET calls, because the apex issues a 308 redirect that curl will not follow for a POST without it.

The change-request state machine. Approve is claim-then-apply across transactions: the row flips to approved first, the edit runs second, and applied_at is stamped only after that commits. A failed apply reverts the row to open; a crash between the two leaves approved with a null applied_at, which a later approve resumes idempotently.

Change requests

GET/api/v1/change-requestsBearer · kb:read · governance:read

The change-request queue, newest first (created_at desc, then id desc). Rows are joined to the target entity so entityName comes back with them.

Query parameters

statusstringFilter to one of open | approved | rejected | withdrawn. Any other value is a 400 invalid-params. Omit for all.
Example requestbash
curl -s https://eli.ai/api/v1/change-requests?status=open \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "requests": [
    {
      "id": "01KZ41BBBBBBBBBBBBBBBBBBBB",
      "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
      "kind": "update",
      "payload": { "description": "Recurring revenue retained from existing customers over 12 months." },
      "status": "open",
      "proposedBy": "usr_01H8…",
      "note": "Aligning the definition with the FY26 board deck",
      "reviewedBy": null,
      "reviewNote": null,
      "createdAt": "2026-07-21T09:00:00.000Z",
      "reviewedAt": null,
      "appliedAt": null,
      "entityName": "Net Revenue Retention"
    }
  ]
}
POST/api/v1/change-requestsBearer · kb:write · governance:write

Propose a change. Returns 201 with the created request. The payload is validated strictly per kind, so an unknown field is a 400 rather than a silent drop.

Request body

kindrequired"create" | "update" | "deprecate" | "supersede" | "delete"What is being proposed.
entityIdstringTarget concept. Required for every kind except create — omitting it is a 400, and an unknown or soft-deleted id is a 404.
payloadobjectKind-specific and strict. create: {name, type, aliases?, description?, lifecycle?: draft|published} — name and type required. update: {name?, type?, aliases?, description?} — at least one. supersede: {supersededBy, note?}. deprecate: {note?}. delete: {}. Defaults to an empty object, which only satisfies deprecate and delete.
notestringWhy you are proposing this, shown to reviewers. Max 4000 characters.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/change-requests \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "update",
    "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
    "payload": { "description": "Recurring revenue retained from existing customers over 12 months." },
    "note": "Aligning the definition with the FY26 board deck"
  }'
Responsejson
{
  "id": "01KZ41BBBBBBBBBBBBBBBBBBBB",
  "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
  "kind": "update",
  "payload": { "description": "Recurring revenue retained from existing customers over 12 months." },
  "status": "open",
  "proposedBy": "usr_01H8…",
  "note": "Aligning the definition with the FY26 board deck",
  "reviewedBy": null,
  "reviewNote": null,
  "createdAt": "2026-07-21T09:00:00.000Z",
  "reviewedAt": null,
  "appliedAt": null,
  "entityName": "Net Revenue Retention"
}
POST/api/v1/change-requests/{id}/approveBearer · kb:write · governance:write

Approve and apply. The response carries both the closed request and the entity the change landed on — for kind create that id did not exist before the call.

Request body

notestringReview note stored on the request as reviewNote. Max 4000 characters. This field is named note, not reviewNote.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/change-requests/01KZ41BBBBBBBBBBBBBBBBBBBB/approve \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note": "Matches the finance definition"}'
Responsejson
{
  "request": {
    "id": "01KZ41BBBBBBBBBBBBBBBBBBBB",
    "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
    "kind": "update",
    "payload": { "description": "Recurring revenue retained from existing customers over 12 months." },
    "status": "approved",
    "proposedBy": "usr_01H8…",
    "note": "Aligning the definition with the FY26 board deck",
    "reviewedBy": "usr_01J2…",
    "reviewNote": "Matches the finance definition",
    "createdAt": "2026-07-21T09:00:00.000Z",
    "reviewedAt": "2026-07-21T09:05:00.000Z",
    "appliedAt": "2026-07-21T09:05:00.000Z",
    "entityName": "Net Revenue Retention"
  },
  "appliedEntityId": "01KZ40AAAAAAAAAAAAAAAAAAAA"
}

The review-note field is `note`

Approve and reject both validate { note?: string }. Body validation strips unknown keys, so sending reviewNote is accepted with a 200 and the note is silently discarded. Send note; read it back as reviewNote on the request object.

Who may approve, and what a 409 means here

If the target entity has an ownerId or a non-empty stewardIds, only those identities may approve — anyone else gets 409 conflict with "Not a reviewer". With neither set, any caller who passes the scope check may approve.

The acting identity is the user id for a delegated eli_uk_ key, and the API key id for a shared eli_sk_ workspace key. A workspace key therefore can never match an owner or steward: owned concepts must be approved with a user key belonging to a reviewer, or through the workspace UI.

A 409 also comes back when the request is no longer open — first reviewer wins, and a double apply is impossible.

POST/api/v1/change-requests/{id}/rejectBearer · kb:write · governance:write

Close an open request as rejected without applying it. 409 when the request is not open, 404 when it does not exist.

Request body

notestringStored as reviewNote. Max 4000 characters.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/change-requests/01KZ41BBBBBBBBBBBBBBBBBBBB/reject \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note": "Superseded by the ontology rework"}'
Responsejson
{
  "id": "01KZ41BBBBBBBBBBBBBBBBBBBB",
  "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
  "kind": "update",
  "status": "rejected",
  "reviewedBy": "usr_01J2…",
  "reviewNote": "Superseded by the ontology rework",
  "reviewedAt": "2026-07-21T09:06:00.000Z",
  "appliedAt": null,
  "entityName": "Net Revenue Retention"
}
POST/api/v1/change-requests/{id}/withdrawBearer · kb:write · governance:write

Close an open request as withdrawn. Takes no request body. 409 when the request is not open.

Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/change-requests/01KZ41BBBBBBBBBBBBBBBBBBBB/withdraw \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "id": "01KZ41BBBBBBBBBBBBBBBBBBBB",
  "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
  "kind": "update",
  "status": "withdrawn",
  "reviewedBy": "usr_01H8…",
  "reviewNote": null,
  "reviewedAt": "2026-07-21T09:07:00.000Z",
  "appliedAt": null,
  "entityName": "Net Revenue Retention"
}

What approval actually applies

create inserts the concept at authority curated and lifecycle published (or draft when the payload asks for it); a name collision with a live concept is a 409. update writes the fields and promotes authority to at least curated in one transaction — a promotion only, never a demotion of a certified concept. deprecate and supersede route through the same code as the deprecate endpoint below, and delete soft-deletes. Every one of them records the revision with activity change_request:<id>, which is how you trace an edit back to its proposal.

Concept authority & lifecycle

These five mutations all return the same CurationMeta object and all append a revision. They are the direct path — use them for the states a change request does not cover (authority, owner, review stamps), and use change requests for edits you want reviewed.

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

Owner/admin sign-off. Sets authority to 'certified', refreshes lastReviewedAt and nextReviewAt (now + the workspace reviewIntervalDays, default 180), bumps version, and writes a revision with changeKind 'certify'. Lifecycle is untouched.

Request body

notestringRecorded on the revision. Max 4000 characters.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/certify \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note": "Signed off by the finance data owner"}'
Responsejson
{
  "authority": "certified",
  "lifecycleStatus": "published",
  "ownerId": "usr_01J2…",
  "stewardIds": ["usr_01K4…"],
  "version": 8,
  "lastReviewedAt": "2026-07-21T09:10:00.000Z",
  "nextReviewAt": "2027-01-17T09:10:00.000Z",
  "supersededBy": null,
  "sourceRefs": [{ "label": "FY26 board deck", "url": "https://example.com/fy26" }],
  "editorialNote": null
}
POST/api/v1/entities/{id}/reviewBearer · kb:write · governance:write

Stamp a review without certifying. Refreshes lastReviewedAt and nextReviewAt; a machine_extracted concept is promoted to 'curated', higher tiers are left alone. Takes no request body.

Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/review \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "authority": "curated",
  "lifecycleStatus": "published",
  "ownerId": null,
  "stewardIds": [],
  "version": 4,
  "lastReviewedAt": "2026-07-21T09:12:00.000Z",
  "nextReviewAt": "2027-01-17T09:12:00.000Z",
  "supersededBy": null,
  "sourceRefs": [],
  "editorialNote": null
}
POST/api/v1/entities/{id}/authorityBearer · kb:write · governance:write

Set the authority tier explicitly, including a demotion. Setting the tier it already has is an idempotent no-op: no version bump, no revision, no audit row.

Request body

authorityrequired"machine_extracted" | "asserted" | "curated" | "certified"The new tier. Any other value is a 400.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/authority \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"authority": "curated"}'
Responsejson
{
  "authority": "curated",
  "lifecycleStatus": "published",
  "ownerId": "usr_01J2…",
  "stewardIds": [],
  "version": 9,
  "lastReviewedAt": "2026-07-21T09:10:00.000Z",
  "nextReviewAt": "2027-01-17T09:10:00.000Z",
  "supersededBy": null,
  "sourceRefs": [],
  "editorialNote": null
}
POST/api/v1/entities/{id}/ownerBearer · kb:write · governance:write

Assign or clear the owner and optionally replace the whole steward list. This is what arms the reviewer gate on change requests for this concept.

Request body

ownerIdrequiredstring | nullThe owning identity, 1–200 characters, or null to clear. The key must be present; the reviewer gate compares this value against the acting identity, so use the same id space your user keys resolve to.
stewardIdsstring[]Replaces the steward list wholesale when present, de-duplicated. Up to 50 entries, each 1–200 characters. Omit to leave the existing list untouched.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/owner \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ownerId": "usr_01J2…", "stewardIds": ["usr_01K4…"]}'
Responsejson
{
  "authority": "curated",
  "lifecycleStatus": "published",
  "ownerId": "usr_01J2…",
  "stewardIds": ["usr_01K4…"],
  "version": 10,
  "lastReviewedAt": "2026-07-21T09:10:00.000Z",
  "nextReviewAt": "2027-01-17T09:10:00.000Z",
  "supersededBy": null,
  "sourceRefs": [],
  "editorialNote": null
}
POST/api/v1/entities/{id}/deprecateBearer · kb:write · governance:write

Retire a concept. Without supersededBy the lifecycle becomes 'deprecated'; with it the lifecycle becomes 'superseded' and supersededBy points at the successor.

Request body

supersededBystringSuccessor concept id. It must be live, unmerged and published: 404 when it does not exist, 400 when it was merged away, is not published, or equals the target itself.
notestringRecorded on the revision. Max 4000 characters.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/deprecate \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"supersededBy": "01KZ44CCCCCCCCCCCCCCCCCCCC", "note": "Replaced by the FY26 definition"}'
Responsejson
{
  "authority": "curated",
  "lifecycleStatus": "superseded",
  "ownerId": "usr_01J2…",
  "stewardIds": [],
  "version": 11,
  "lastReviewedAt": "2026-07-21T09:10:00.000Z",
  "nextReviewAt": "2027-01-17T09:10:00.000Z",
  "supersededBy": "01KZ44CCCCCCCCCCCCCCCCCCCC",
  "sourceRefs": [],
  "editorialNote": null
}

Only a published concept can be retired

Deprecate and supersede both require the target to be in lifecycle published. A draft or pending-review concept gets a 400 telling you to publish or delete it instead; a concept that is already deprecated or superseded gets a 409. There is no draft → deprecated edge in the state machine.

Revision trail

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

The append-only history for one concept, newest first. Returns 404 for an unknown or deleted entity, so revisions of purged ids stay private.

Query parameters

limitinteger1 to 200, default 50. Anything else is a 400 invalid-params.
Example requestbash
curl -s "https://eli.ai/api/v1/entities/01KZ40AAAAAAAAAAAAAAAAAAAA/revisions?limit=2" \
  -H "Authorization: Bearer $ELI_KEY"
Responsejson
{
  "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
  "revisions": [
    {
      "id": "01KZ45DDDDDDDDDDDDDDDDDDDD",
      "workspaceId": "ws_01H7…",
      "entityId": "01KZ40AAAAAAAAAAAAAAAAAAAA",
      "version": 8,
      "changeKind": "certify",
      "snapshot": {
        "name": "Net Revenue Retention",
        "type": "metric",
        "attrs": {},
        "authority": "certified",
        "lifecycleStatus": "published",
        "ownerId": "usr_01J2…",
        "stewardIds": ["usr_01K4…"],
        "aliases": ["NRR"],
        "sourceRefs": [],
        "supersededBy": null
      },
      "diff": { "authority": { "from": "curated", "to": "certified" } },
      "activity": null,
      "changedBy": "usr_01J2…",
      "note": "Signed off by the finance data owner",
      "createdAt": "2026-07-21T09:10:00.000Z"
    }
  ]
}

changeKind is one of create, update, review, certify, set_authority, set_owner, deprecate, supersede, delete, change_request, verify. activity records where the change came from — api for the authority and deprecate endpoints, and change_request:<id> for anything applied by an approval.

Document verification

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

Stamp a document. verified and certified record who verified it, when, and a re-verify due date (now + the workspace reviewIntervalDays); unverified and deprecated clear all three stamps. Audited as document.verify.

Request body

verificationrequired"unverified" | "verified" | "certified" | "deprecated"The new state. Any other value is a 400.
Example requestbash
curl -s -L -X POST https://eli.ai/api/v1/documents/01KZ50EEEEEEEEEEEEEEEEEEEE/verify \
  -H "Authorization: Bearer $ELI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"verification": "certified"}'
Responsejson
{
  "docId": "01KZ50EEEEEEEEEEEEEEEEEEEE",
  "verification": "certified",
  "verifiedBy": "usr_01J2…",
  "verifiedAt": "2026-07-21T09:20:00.000Z",
  "nextVerifyAt": "2027-01-17T09:20:00.000Z"
}

Error envelope

Failures come back as { error, message } — plus issues on a body-validation 400. Across this module: 401 unauthorized (missing, invalid or revoked key), 403 insufficient_scope or insufficient_capability, 400 invalid_body / invalid_json / invalid-params / invalid, 404 not_found, and 409 conflict. One exception worth coding around: the document-verify route's own not-found reply is { "error": "Document not found" } with no message key. That branch also applies the caller's content ACL, so a document restricted away from the key looks identical to one that does not exist. There is no pagination envelope, idempotency key or rate limiter on these routes.

Data elements

Warrant owns two tables outright, owns a column group on two more, and appends to the audit log. Every one of the five carries a workspace_id and a <table>_tenant_isolation row-level-security policy whose USING and WITH CHECK clauses both require workspace_id to equal the workspace of the current transaction. There is no cross-workspace read path: a wrong-workspace id is simply invisible, which is why several routes return 404 where you might expect 403.

TableWarrant's roleColumns that matterWhat a row means
concept_change_requestsownsid · workspace_id · entity_id · kind · payload · status · proposed_by · note · reviewed_by · review_note · created_at · reviewed_at · applied_atOne proposed change. entity_id is null when kind is 'create' (a brand-new concept). status is open | approved | rejected | withdrawn. applied_at is stamped only after the apply commits, so approved + null applied_at is a detectable, resumable wedge.
entity_revisionsowns (append-only)id · workspace_id · entity_id · version · change_kind · snapshot · diff · activity · changed_by · note · created_atOne immutable revision per accepted change. snapshot holds name, type, attrs, authority, lifecycleStatus, ownerId, stewardIds, aliases, sourceRefs, supersededBy; diff is {field: {from, to}} against the previous snapshot. entity_id deliberately carries NO foreign key so history survives hard removal of the entity. Never UPDATEd or DELETEd.
entitiesreads + writes governance columnsauthority · lifecycle_status · owner_id · steward_ids · version · last_reviewed_at · next_review_at · superseded_by · source_refs · editorial_noteThe row itself belongs to the graph module; Warrant owns this column group. authority defaults to machine_extracted, lifecycle_status defaults to published, version defaults to 1. editorial_note is steward-only and is excluded from revision snapshots.
documentsreads + writes verification columnsverification · verified_by · verified_at · next_verify_atDocument-level verification. verification defaults to 'unverified'; setting verified or certified stamps the other three, setting unverified or deprecated clears them.
audit_logwritesid · workspace_id · actor_id · event · target · meta · created_atWarrant appends change_request.create / .approve / .reject / .withdraw, entity.certify / .review / .authority / .owner / .deprecate / .supersede / .lifecycle / .refs / .editorial_note, and document.verify.

Two details worth knowing before you build on these rows. First, editorial_note is steward-only: it is stored on the entity but deliberately excluded from the revision snapshot and diff, so it never travels into a history view. Second, entity_revisions.version is bumped with an atomic UPDATE … RETURNING that takes the entity row lock, so two concurrent mutations can never mint the same version number.

How it composes

Warrant is usable on its own — the state it records is meaningful whether or not anything reads it. What follows is what each neighbour adds, and what genuinely breaks without it.

With Atlas — the concepts being governed

Atlas owns the entities row; Warrant owns its governance columns. This is the one real dependency: a change request of kind update, deprecate, supersede or delete, and every one of the five authority/lifecycle endpoints, needs a live entity to point at. Approving a create request is how you get one without touching Atlas's write API at all — the entity is created by the approval, at authority curated.

With Lens — where the state changes answers

This is where governance stops being paperwork. Retrieval's graph-fusion stage excludes entities whose lifecycle_status is draft or pending_review, so drafting a concept keeps it out of answer context. On the document side, documents.verification is a direct input to the answer policy's confidence features — ranked unverified 0, verified 1, certified 2, and deprecated −1, so a deprecated source counts against the answer — and when no cited document is verified or certified the policy raises a no_verified_source caveat. Entity authority participates too: the feature set tracks whether any detected concept is certified. Without Lens, all of this is recorded and simply not consumed.

With Lineage — the trail, assembled

entity_revisions and the audit rows are Warrant's, and GET /api/v1/entities/{id}/revisions reads them without any Lineage code. Lineage is what stitches them together with document revisions, extraction runs and citations into a single provenance view. Note that createGovernanceClient also exposes documentLineage(id), which calls GET /api/v1/documents/{id}/lineage— that endpoint is Lineage's, not Warrant's, and is documented there.

With Intake — verification on ingested content

Connector-ingested documents arrive at the default verification of unverified. Warrant's verify endpoint is the human stamp on top of that. No connector is required to use it — any document row will do.

With Crucible — the policy decision log

GET /api/v1/policy/decisions and GET /api/v1/policy/stats are guarded by the same governance:read capability as this module's reads (accepting either kb:read or runs:read), and their wire types PolicyDecision / PolicyStats live in @eli-ai/contracts/governance. Their client methods, however, are on the quality entry — createQualityClient(transport).listPolicyDecisions() — so they are documented with Crucible. Adopt Warrant alone and you still get the write-side governance state; you do not get the decision log unless you also take that entry.

With Ports — two approvals that are not the same thing

Agent runs have their own human-in-the-loop approval at POST /api/v1/runs/{id}/approve, gated on agents:run. It has no relationship to the change-request queue: different table, different scope, different lifecycle. Do not wire one to the other expecting shared state. Separately, the SKOS/PROV-O export at GET /api/v1/export/skos is reachable from createGovernanceClient(transport).exportSkos() but guards on graph:read — it is a graph export whose contents are lifecycle-shaped (published, deprecated and superseded concepts only; drafts never export).

With Conduit

No coupling. Live-data connectors and governed queries neither read nor write any Warrant table, and Warrant needs nothing from them.

Going deeper

The reasoning behind the tiers, the two-track gating model and the review SLA is in Concept governance. For a curl-only walkthrough of the same loop see Curate & govern concepts, and for the in-product review queue see Curation & change management.

Extending Warrant

A plugin can attach a read-only policy observer policyObservers runs post-hoc on a recorded decision and can never change a verdict — and can read and write governance through the facade: staleConcepts, getCurationMeta and curationSummary under governance:read, and markReviewed, setAuthority and deprecateEntity under governance:write, each appending the same revision rows a human action would. Warrant emits entity.certified, entity.authority_changed, change_request.* and policy.decision.recorded. See the extension model for the contract and the trust boundary, and Build a plugin for a worked example.