POST /v1/recall
Read a stratified pack — layers, context block, provenance, and diagnostics.
Authorization
bearer Authorization: Bearer . Four credentials authenticate: (1) the operator key, the static CORTEX_API_KEY: the trusted operator, acting as the principal X-Cortex-Actor names (default user:local), in billing tenant default; (2) a PASETO v4 public token this server's minter signed (POST /v1/auth/tokens, POST /v1/auth/signup, the mint_offline tool), of audience CORTEX_V1_AUDIENCE or, with CORTEX_V1_TENANT_AUDIENCES=1, a billing tenant's own cortexdb:tenant:; (3) since 0.14.0 (principal_chain_v1), a JWT (RS256 or ES256) or PASETO v4 public token of an issuers-file entry (CORTEX_AUTH_ISSUERS_FILE; [auth] issuers_file on a cluster data node), whose subject, audience and token id (:) the entry maps; (4) a commitment transport token, on /v1/memory/commitments/* only. HS256, none and every symmetric JWT algorithm are refused. With auth disabled (no operator key: CORTEX_API_KEY unset on a loopback bind, or CORTEX_INSECURE_NO_AUTH) no token is verified and every request is served as the trusted operator, as the X-Cortex-Actor it names or user:local. On the operator key, with auth disabled and in the unsigned development mode, an X-Cortex-Actor that is not a principal id is 400 INVALID_HEADER, details {part: header, field: X-Cortex-Actor, reason: the parse error} (since 0.14.0; before, 401 INVALID_BODY). X-Cortex-Actor must equal the token's sub; a token of an issuers-file entry in prefix, hash or directory mode and a call of /mcp may leave it out (when present it must match). Any other token sent without it is verified first: a bearer of no known token form (a mistyped operator key) is 401 MISSING_TOKEN, any other bad token gets its own refusal, as with the header, and only a token that verifies answers 401 ACTOR_MISMATCH, the header is required. Every authenticated response carries X-Cortex-Token-Expires-In and X-Cortex-Token-Expires-At (and a Warning header within 72 h of expiry); every error of the auth layer carries X-Cortex-Request-ID.
Authentication refusals, the same on every authenticated route, none retriable unless marked. A token that carries none of the principal-chain claims and was not signed by an issuers-file entry gets each code with no details, as in 0.13.x; an entry's token, and a token that carries a chain claim other than minter, gets details.issuer (the entry's id, minter, commitment or unknown) and the details named here. 401 MISSING_TOKEN: no Authorization: Bearer header, or a bearer that is neither the operator key nor a PASETO v4 public or JWT token (a wrong operator key), with or without X-Cortex-Actor. 401 INVALID_TOKEN: a PASETO token no registered key verifies, a token signed by a key other than the issuer its iss names included. 401 INVALID_TOKEN_SIGNATURE: a bad signature, an iss no issuer is registered under, a JWT algorithm other than RS256 or ES256, a malformed token, or, on a token without chain claims, an iat ahead of the clock; for an issuers-file entry's token details.reason issuer_disabled, token_type, critical_header, algorithm, kid_revoked or unknown_kid (none for a bad signature). 401 EXPIRED_TOKEN. 401 REVOKED_TOKEN: the token's id is revoked (an entry's token goes by :). 401 WRONG_TENANT: an aud the token's issuer may not name, or one that names no tenant. 401 INVALID_TOKEN_CLAIM (since 0.14.0), details.claim and details.reason: a principal-chain claim malformed (malformed, not_an_agent, same_as_sub, not_a_service, person_with_agent, run_start_without_run, after_iat, run_too_old, out_of_range; unknown_critical_claim, with details.critical); an issuers-file entry's token without exp, iat or jti (missing), with iat or nbf ahead of the clock (ahead), or living longer than the entry's max_lifetime_s (claim exp, lifetime_over_maximum, details.lifetime_s, details.ceiling_s); a token this server's minter signed whose exp - iat is above the ceiling for its subject, 604,800 s for a user: or agent:, 2,592,000 s for a service: or system: (claim exp, lifetime_over_ceiling, details.issuer minter, details.lifetime_s, details.ceiling_s; checked before the revocation, and counted instead while CORTEX_MINT_LIFETIME_ENFORCE=off); and, whatever issuer signed it, a token whose act names an AI employee of its tenant, the self of an employee record that is not delegable (claim act, act_not_delegable). 401 CLAIM_NOT_PERMITTED (since 0.14.0), details.claim: a chain claim the token's issuer is not bound to assert (an issuers-file entry's [issuer.claims]; minter from any issuer but this server's minter; the minter's own chain claims without the chain assertion), or an issuers-file entry's scopes entry outside its token's tenant root org: (claim scopes, details.entry). 401 SUBJECT_UNMAPPED (since 0.14.0), details.reason: an issuers-file entry's subject mapping produced no principal (missing, not_a_principal, kind, service, foreign_namespace, personal_data_in_id, malformed, too_long, not_in_directory, directory_not_here); and, on every route whatever issuer signed the credential, a principal whose entry in its tenant's directory (org:) is deactivated (deactivated; never the trusted operator). 410 TENANT_DELETED (since 0.14.0): the credential's tenant root, org:, is being deleted or was (POST /v1/erasures/tenant); never the trusted operator. 503 REVOCATION_UNAVAILABLE (retriable): the revocation list cannot answer, so every token is refused as an outage, never 401 REVOKED_TOKEN, and the list still fails closed: on a cluster data node while the node's revocation lease has expired (the metadata group has not confirmed its revocation set for 10 s, or the node has just started), and on a single node once the list's journal failed (a refused write latches the list). 503 ISSUER_KEYS_UNAVAILABLE (retriable): an issuers-file entry's JWKS cannot be fetched and no pinned or cached key verifies the token; Retry-After is the whole seconds until the key cache's next fetch (its backoff, 3 s doubling to 60 s; 3 s before the first fetch), at least 1, also in details.retry_after_seconds. 503 WAL_UNAVAILABLE (retriable): the directory did not read, so a directory-mode subject's principal, or a principal's deactivation, is unknown.
Authorization refusals any route gives where it checks a capability or a roster, none retriable. 403 POLICY_DENIED, details.capability and details.denied_by_tier (deployment, tenant, actor or scope): the deployment's or the tenant's policy denies the capability, the token's caps leave it out, a scopes entry names it but not at this scope, or the scope's roster does not admit the caller. Since 0.14.0 a token that carries scopes holds only the capabilities an entry names (: its subtree, ! that one scope, :* every scope and the checks made without one); any other is refused with details.link confinement, and counted instead while CORTEX_TOKEN_CONFINEMENT_ENFORCE=off. An agent chain token (one that carries act or run, or app or client resolving to agent) is confined so whatever the switch, and without scopes holds no capability. A tenant-audience token's :* entry at a scope outside its own org: root is details.link tenant. A token another principal minted (minter a principal other than its sub) also needs that minter to hold the capability under the deployment's and the tenant's policy and the role on the scope's roster: details.link minter, denied_by_tier scope, counted instead while CORTEX_MINTER_LINK_ENFORCE=off. A read whose credential stops being current while it runs (expired or revoked, or its issuers-file entry disabled or reloaded without its audience) is refused 403 POLICY_DENIED before it delivers. 403 APP_NOT_ADMITTED (since 0.14.0): in a tenant whose root sets apps_enforced, a chain whose application (app, or unlisted) is not admitted at the scope. 403 RUN_REQUIRED (since 0.14.0), details.scope: on the write routes (POST /v1/experience, each item of /v1/experience/bulk and /v1/experiences, each JSONL import line, POST /v1/beliefs), a token with act and no run.
The operator key under CORTEX_OPERATOR_DATA_READS (since 0.14.0, operator_break_glass_v1): allow, audit or deny; unset, deny on the cloud_shared_saas preset and allow on the other presets and with auth disabled; a tenant's own mode (PUT /v1/admin/access/tenant-settings) overrides it in that tenant. 403 BREAK_GLASS_REQUIRED, details {mode, class, route}: under deny, without a well-formed X-Cortex-Break-Glass, an operator-key action of class stored_rows (a route that returns stored rows, or a forget or erasure preview whose selector names about_subject, about_entity or predicate), admin_content (a content-returning admin route), private_region (a read whose scope or extra_scopes lie in a private region) or in_band (a mint of a token that carries a data capability, can mint, carries a registry-write capability or is for an Owner of a private region; a registration, an auto-provisioning first write included; a roster or tree edit that widens a private region or adds a service: principal; a write into a private region or a shared org-tree scope; a migration out of a private region; lowering a tenant's mode from deny; a deployment minter's mint of a token that carries a data capability). 400 INVALID_HEADER, in every mode: that header malformed, sent twice or longer than 512 bytes, details {part: header, field: X-Cortex-Break-Glass, reason: what is wrong with it}. Both refusals write the request's audit row as decision deny, each with its denial (check operator_access, path request#x-cortex-break-glass, decided_by_tier request; reason the 403's class, or break_glass_invalid for the 400, which names no scope), within the operator key's bound (its billing tenant and X-Cortex-Actor) (AuditRow). Its form: reason=incident|legal|support|security|migration|restore|other; ticket=1 to 64 characters of [A-Za-z0-9._:-]; optionally note=percent-encoded UTF-8 of at most 256 bytes; no other key, separated by semicolons. It is read only on the operator key's requests and on a deployment minter's POST /v1/auth/tokens, and ignored on every other credential; a cross-origin browser page cannot send it. 503 NOTICE_UNAVAILABLE: under deny, a header-bearing read of a private region whose notice to the region's person did not commit, so nothing was delivered: details.reason moving, limit, members_behind (Retry-After: 30) or refused (details.refusal), each retriable; arming_disabled and reserved_path_in_use, not retriable. A by-id read's notice is its row's region's, committed once the row is read (since 0.14.0; scope_unnamed is gone).
Before authentication, a build that enforces licenses answers every data route 402 LICENSE_EXPIRED, or 402 LICENSE_REQUIRED when it never held a license, not retriable; health, version, ready, /metrics, the published contract (the OpenAPI document, GET /v1/schemas and /v1/schemas/{name}), the console, every GET /v1/admin/* route, GET /v1/auth/whoami, POST /v1/export and, since 0.14.0, deleting data (POST /v1/forget and its preview, POST /v1/erasures and its preview, /v1/erasures/person, /v1/erasures/tenant, /v1/erasures/{id}/cancel, and GET /v1/erasures/{id}, /verify and /receipt) pass. Not an authentication refusal, and answered only where a write needs a newer shard apply format than the node's shard is armed for (change sets, application records, directory and SCIM writes, a person's erasure and a tenant's deletion, writes that carry atoms or provenance, rehomes and watermarks, a declared belief with provenance): 503 FORMAT_UPGRADE_PENDING, details {reason, format, node, paths, count} (reason, format and node alone on the directory, SCIM, rehome and watermark writes; reason alone on an application record or a reference root), retriable with Retry-After: 30 for members_behind only (arming_disabled and reserved_path_in_use wait for the operator). A plain node arms for format 10 at its first boot of 0.14.0 under the default ceiling, one way, and then never answers it.
In: header
Header Parameters
operator_break_glass_v1 (API reference §8.15.3): read only on a request the operator key (CORTEX_API_KEY) authenticates, and on a deployment minter's POST /v1/auth/tokens; ignored on every other credential. reason=<incident|legal|support|security|migration|restore|other>; ticket=<1 to 64 of [A-Za-z0-9._:-]>[; note=<percent-encoded UTF-8, at most 256 bytes>], exactly one header of at most 512 bytes; any other form is 400 INVALID_HEADER in every mode (details {part: header, field: X-Cortex-Break-Glass, reason}), and the request's audit row a deny. Under CORTEX_OPERATOR_DATA_READS=deny (the deployment's, or the tenant's own) an operator-key request that reads stored rows, a content-returning admin route, a private region's read, or an in-band path needs it, else 403 BREAK_GLASS_REQUIRED (details {mode, class, route}). A well-formed header is written to the audit row as {reason, ticket}, never the note.
length <= 512subject_token_shadow_v1: on a request the operator key authenticates, the token of the principal the operator acts for, verified by the same verifier (without waiting on the network and without the X-Cortex-Actor step) and evaluated in shadow by the access report (GET /v1/admin/access/report). It never changes the response: a repeated, oversized (over 8,192 bytes) or invalid header, or the operator key itself, is recorded as the shadow's invalid_token and the request runs as sent.
length <= 8192Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Body of POST /v1/recall and POST /v1/recall/stream. Strict: a field not listed here is 422 INVALID_BODY (unknown field), and so are the pagination keys limit, offset, page, cursor and page_size on /v1/recall (bound the pack with budgets; page raw records with GET /v1/events). consistency and purpose are read off the body before it is parsed. A field whose capability GET /v1/admin/version does not list is refused with 422 INVALID_BODY (details.unsupported_fields, details.required_capabilities), its key's presence counting (language: null, scripts: [], temporal.timezone: null); subtree is flag-gated instead (409 SUBTREE_RECALL_DISABLED).
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/v1/recall" \ -H "Content-Type: application/json" \ -d '{ "scope": "string" }'{ "pack_id": "string", "scope": "string", "view": "raw", "context_block": "string", "layers": { "events": [ { "matched_fragments": [ { "chunk_id": "55e808e1-8ddc-49f1-92c9-6bbdcdff1c83", "text_source": "event_text", "byte_range": { "start": 0, "end": 1 }, "locator": { "kind": "conversation", "session_id": "string", "turn_index": 0, "speaker": "string", "message_id": "string" }, "language": "string", "matched_via": "vector" } ], "extract": [ "facts" ] } ], "facts": [], "beliefs": [], "episodes": [], "understanding": [], "code": [ { "repo": "string", "path": "string", "citation": "string", "snippet": "string", "generation": 0, "score": 0 } ], "rollups": [ { "id": "string", "home": "string", "unit": "string", "level": "team", "kind": "totals", "topic": "string", "part": 0, "parts": 0, "summary_safe": true, "built_at": "2019-08-24T14:15:22Z", "window": { "from": "2019-08-24T14:15:22Z", "through": "2019-08-24T14:15:22Z" }, "inputs": [ { "scope": "string", "read_at": "reader" } ], "totals": { "events": 0, "intents": [ { "key": "string", "count": 0, "latest_at": "2019-08-24T14:15:22Z" } ], "label_families": [ { "key": "string", "count": 0, "latest_at": "2019-08-24T14:15:22Z" } ], "measures": [ { "predicate": "string", "count": 0, "sum": 0, "latest": { "value": 0, "at": "2019-08-24T14:15:22Z" } } ], "inputs": [ { "scope": "string", "events": 0 } ], "other": 0 }, "text": "string", "positions": [ { "text": "string", "inputs": [ "string" ] } ], "citations": [ { "kind": "event", "id": "string", "scope": "string" } ] } ] }, "knowledge_subgraph": {}, "provenance": {}, "diagnostics": { "time_ms": { "property1": 0, "property2": 0 }, "policy": { "tier_attribution": "string", "capability": "string" }, "filters_applied": [ "string" ], "knapsack_evictions": 0, "_partial": true, "degraded_sources": [ "string" ], "items": [ { "event_id": "string", "final_rank": 0, "fused_rank": 0, "variant_fused_rank": 0, "channels": { "property1": { "rank": 0, "score": 0 }, "property2": { "rank": 0, "score": 0 } }, "variants": [ { "index": 0, "vector_rank": 0, "bm25_rank": 0, "fused_rank": 0 } ], "matched_via": [ "string" ], "reorder": { "property1": { "from": 0, "to": 0 }, "property2": { "from": 0, "to": 0 } } } ], "drops": [ { "event_id": "string", "reason": "time_range" } ], "channel_lists": [ { "channel": "vector", "variant": 0, "len": 0 } ], "items_truncated": 1, "filtered_results_not_in_ranked_pool": 0, "scopes_traversed": [ {} ], "code_stage": { "property1": "string", "property2": "string" } }, "warnings": [ "string" ], "subtree": { "mode": "ranked", "complete": false, "subjects_authorized": 0, "subjects_with_candidates": 0, "subjects_in_pack": 0, "events_in_pack": 0, "vector_strategy": "exact_scan", "subjects": [ { "alias": "string", "scope": "string", "events": 0, "best_rank": 0 } ] }, "coverage": { "searched": { "scopes": [ "string" ], "scopes_total": 0, "partitions": 0, "truncated": true }, "authorized": { "scopes": [ "string" ], "scopes_total": 0, "truncated": true }, "gaps": [ { "scope": "string", "reason": "bound", "left_out": 0 } ], "gaps_truncated": true, "gaps_total": 0, "watermarks": [ { "source": "string", "at": "2019-08-24T14:15:22Z", "through": "string" } ] }, "consistency_token": "string", "consistency_reset": true, "derived_through": 0, "access_restored_at": "2019-08-24T14:15:22Z"}Views
| view | traversal | notes |
|---|---|---|
raw | single scope | full event payloads |
granular (alias local) | single scope | all layers, exact scope only |
holistic (default) | scope + ancestors | requires scope.read.holistic |
descend | scope + descendants | requires scope.read.descend |
lineage | — | supported (was undocumented in v1) |
structured | single scope | facts + beliefs only |
local is accepted
view: "local" is accepted on recall and behaves as granular.
Behavior notes
Recall is not cached by idempotency_key
Recall always runs fresh: repeated calls return a new pack_id each time, whatever idempotency_key
you send. Keep a pack_id yourself if you need to refer back to a pack.
- Filters take exactly one field,
metadata, with attributesobserved_actor,caller,modality,labels,intent(ANDed; values ORed). Unknown attributes →422. No confidence filter. exclude_content: truenulls largecontent/summarystrings.budgets.per_layer_limitshard-caps each layer (events: 0drops the layer).diagnostics=none(default) /summary/full. Timings are reported only forsummaryandfull:time_msholds about 47 phase-named keys (coordinator_recall,wal_hydrate,recall_total, …); withnoneit is an empty object. With capabilityrecall_diagnostics_v1,fullalso returns per-item ranks,items[] = { event_id, final_rank, fused_rank, channels: { bm25: {rank, score}, vector: {rank, score} }, matched_via }, andchannel_lists[] = { channel, len }, plusknapsack_evictions.provenance.trailentries are{ phase, elapsed_ms }(+ acontext_contributorslist; see the context receipt).- A body field easy to miss:
prefer_exhaustive(exhaustive candidate sweep). Theorderfield has its own section below. - Streaming:
POST /v1/recall/streamemitsplan → layer(×N) → context_block → provenance → diagnostics → done(thedoneevent carriespack_id). pack_idcan be reused byPOST /v1/answerviause_pack_id(same-scope only; cross-scope →404).
Context block, budgets and long events
context_block is the text an LLM would see. With capability budget_excerpt_v1, no entry takes
it past budgets.max_tokens. Events are placed whole in rank order while they fit; an event that doesn't fit
is skipped, and each skipped event then gets the room still free at its rank position — whole if it now
fits, otherwise as an excerpt of its best-matching chunks. The default budget is 4,000 tokens, about
14,000 bytes of context (CORTEX_RECALL_DEFAULT_MAX_TOKENS changes it).
For example, a 15,637-byte document queried for its second section gives a 3,320-byte block at
max_tokens: 1000, with the excerpt starting at the matched section, and a 13,992-byte block at the
default budget. An
excerpted event's layers.events item carries the same excerpt, marked as a partial view:
"content": {
"kind": "text",
"text": "[…]
# Section 2: VPN troubleshooting …
[…]",
"_partial": true,
"_partial_reason": "budget_excerpt",
"_original_bytes": 15637,
"_excerpt_ranges": [{ "start": 3013, "end": 6219 }, { "start": 15564, "end": 15636 }]
}Treat content.text as the whole text only when _partial is absent; GET /v1/events/{id} always returns
the whole event. Slices are joined with […] markers; _excerpt_ranges are byte ranges into the stored text.
CORTEX_RECALL_BUDGET_EXCERPTS=0 (or off, false) turns excerpts off and restores the pre-0.10.1 render.
The server then leaves budget_excerpt_v1 out of
capabilities[] and logs
RECALL_BUDGET_EXCERPTS_DISABLED at startup, so a client that gates on the name knows excerpts are off.
For every language, English included: an assistant turn's In reply to user: … echo is dropped when
that user turn is rendered in the same block; an event too large for the remaining budget does not end the
block; and the first-ranked event gets no exception: it is excerpted like any other when it does not fit.
Matched fragments
For a long (chunked) event, layers.events[].matched_fragments[] (capability matched_fragments_v1) says
which chunks matched: { chunk_id, byte_range: {start, end}, matched_via: "bm25" | "vector", locator: { kind: "document", section_path: [title, heading] }, language, text_source }. Byte ranges index the stored
text, not an excerpt. section_path holds the Markdown headings in force where the chunk starts: the first
# heading acts as the document title and later headings follow it. GET /v1/admin/events/{id}/chunks
shows how any event was chunked.
The context receipt: which events the block renders
The provenance entry { "phase": "context_contributors", "context_contributors": [{ "event_id", "rank" }] }
names every event context_block renders, including one the cross-layer knapsack evicted from
layers.events (that row carries evicted_from_layers: true and is the only place the response names it).
Converting receipt ids
context_contributors[].event_id is a hyphenated UUID (01a0e70c-81fd-7cc2-b687-4e42ed600f50), while
layers.events[].id and the event routes use evt_01a0e70c81fd7cc2b6874e42ed600f50. To match an entry
or fetch the event, strip the hyphens and prefix evt_.
Who said it: recalled event identity
Every layers.events item carries the identity its event was captured with: caller,
observed_actor, subject, scope_key, scope and wal_offset are the values
GET /v1/events/{id} returns, in every view. What that changes for a client:
- A
filters.metadata.observed_actorrecall now keeps events captured on someone else's behalf that those paths used to drop. scope_keyis the billing partition the event was captured under (defaultfor the static operator key), not its scope. Comparescopewhen you need the scope.context_blockis unchanged.
A capture cancelled with
POST /v1/lifecycle/memory-event/{id}/cancel
is left out of every view: granular, raw, holistic, descend, prefer_exhaustive and
order: "recency".
Ordering: relevance vs recency
The order field switches how the events layer is selected and rendered:
order | Selection | Rendering |
|---|---|---|
relevance (default) | ranked retrieval (BM25 + vector + RRF), scored against the query | ranked order |
recency | the scope's newest eligible events by observed_at — a timeline tail | newest-first |
recency replaces ranked retrieval for the events layer with a timeline tail: the newest eligible
events by observed_at, capped by budgets.per_layer_limits.events (else CORTEX_RECENCY_TAIL_EVENTS,
default 100). Eligibility (scope/tenant, filters, temporal) is identical to ranked mode — only
selection changes. It's built for "recent/latest X" questions, where relevance ranking surfaces the
most-repeated items and can drop the newest ones. An unknown order value → 422.
Use the holistic view for recency
order: "recency" applies in the holistic view (the recall default): holistic + recency +
{ events: 3 } returns the three newest events, newest first. The granular and raw views keep
relevance order (ascending observed_at for events, and a per_layer_limits.events cap keeps the
oldest). Use the default holistic view for "latest / most recent" reads.
/v1/answer forwards order and auto-infers recency for recency-shaped questions — see
POST /v1/answer. The tuning knobs
(CORTEX_RECENCY_TAIL_EVENTS / _ORDER / _INFERENCE) live in
Recall Tuning.
Current values and corrections: newest_of_kinds
Ranked recall returns every past statement that matches the query. For something with one current value — a preference, a setting, a status — that means a late correction comes back beside the statements it corrects, and they can outnumber it:
[2026-01-05] Nigel prefers morning meetings, ideally 9am.
[2026-09-20] Correction from Nigel: he now prefers afternoon meetings after 2pm.
[2026-03-01] Nigel said again he likes early morning meetings at 9am.Give each such thing a stable kind in context.intent when you write it, corrections included, and
ask recall for the newest of that kind (v0.12.4+, capability memory_kinds_v1):
# Every statement of the preference, the correction too, carries the same intent
curl -fsS -X POST "$CORTEX_URL/v1/experience?wait=indexed" -H "$AUTH" -H 'Content-Type: application/json' -d '{
"scope": "org:acme/user:nigel", "modality": "observation",
"content": { "kind": "text", "text": "Correction from Nigel: he now prefers afternoon meetings after 2pm." },
"context": { "intent": "preference.meeting_time" }
}'
curl -fsS -X POST "$CORTEX_URL/v1/recall" -H "$AUTH" -H 'Content-Type: application/json' -d '{
"scope": "org:acme/user:nigel",
"query": "What meeting time does Nigel prefer?",
"newest_of_kinds": ["preference.meeting_time", "preference.seat"]
}'For each kind listed, the pack holds the newest eligible event of that kind, first, whether or not
the query matched it, and none of the older ones: the example returns only the correction. Older
events stay listable (GET /v1/events?intent=preference.meeting_time) and erasable. Blank and repeated
entries are dropped; more than 16 kinds is 422 INVALID_BODY; it can't be combined with subtree.
When the scope can't be read whole in one bounded listing, the newest of each kind among the recalled
events is kept and warnings carries newest_of_kinds_partial.
Use one kind per thing that has a single current value (preference.meeting_time, not
preference): a kind serves exactly one event. Leave history questions ("what did Nigel prefer
before?") on ranked recall.
Reading several scopes: extra_scopes
extra_scopes (v0.12.4+, capability extra_scopes_v1) adds up to 8 more scopes to the same ranked
pass as scope: for example an application's own scope plus a shared source:slack and source:jira.
/v1/recall/stream, /v1/answer and /v1/compose take the same field for every recall they make.
curl -fsS -X POST "$CORTEX_URL/v1/recall" -H "$AUTH" -H 'Content-Type: application/json' -d '{
"scope": "org:acme/app:support",
"extra_scopes": ["org:acme/source:slack", "org:acme/source:jira"],
"query": "What did we promise Globex about the export bug?"
}'- Each extra scope is read exactly (its own events, no ancestors or descendants) and authorized as a
direct
granularrecall of it would be: one the caller can't read refuses the whole request with that recall's 403. - Each contributes its own vector and BM25 candidates, and all of them meet in one fusion and one rerank. Their facts, beliefs, episodes and concepts join too.
- Events keep their real scope in
layers.events[].scope, andcontext_blocklines from an extra scope carry its last segment after the time bracket:[2026-06-15 08:30 UTC] [source:slack] .... - Every extra scope must lie in the tenant subtree of
scope(its root by default;CORTEX_TENANT_DEPTH=2makes it the first two segments). One outside it, or more than 8, answers422 INVALID_EXTRA_SCOPES. Repeats andscopeitself are dropped; it can't be combined withsubtree. Answers that use it are not cached.
On CortexDB Cloud the tenant subtree is your account, so extra_scopes can name
any of your tenants and none of another account's.
Self-hosted default retrieval
On a default self-hosted instance recall runs BM25 + vector + RRF fusion only. Graph traversal
needs a knowledge graph (CORTEX_ENTITY_GRAPH=1 + enrichment); cross-encoder rerank needs a Cohere key.
See Self-hosting defaults.