The Experience Envelope
The single structured payload for everything CortexDB ingests — messages, observations, tool results, documents, and triples.
The Experience Envelope is one structured shape used to ingest all forms of agent experience — messages, observations, tool results, documents, and raw triples — into lossless event-sourced memory. One shape covers every data type, so ingestion isn't fragmented across formats.
CortexDB separates the payload into functional slots: identity (who), content (the data), context (temporal/spatial grounding), and operational directives (how to process it).
Minimal envelope
{
"scope": "org:acme/dept:eng/user:alice",
"modality": "conversation",
"content": { "kind": "message", "role": "user", "text": "Just got off a call with Priya at Acme." }
}Required fields
Only scope, modality, and content are required. context (and context.observed_at) and
idempotency_key are optional — a write without them returns 202. (Older docs marked observed_at
and idempotency_key as required; they are not.)
Identity slots
Up to three identities; each defaults to the previous for the common "narrating yourself" case.
| Slot | Default | Capability needed when set explicitly |
|---|---|---|
| Caller | Implicit (from token) | always |
| Observed actor | = caller | scope.write.on_behalf_of when ≠ caller |
| Subject | = observed_actor | scope.write.about_other when ≠ observed_actor |
This supports bots writing on behalf of a user (observed_actor) and writing memories about other
entities (subject).
Content kinds
content is a discriminated union — pick the shape that matches your data:
| Kind | Shape | Use for |
|---|---|---|
message | { role, text, media[] } | Conversational turns |
text | { text } | Free-form text observations |
json | { data } | Structured tool outputs, sensor readings |
blob_ref | { blob_id } | Reference an already-uploaded blob |
triple | { triple: { subject, predicate, object } } | Direct fact insertion (object needs type+datatype+value) |
The role enum is enforced: user, assistant, tool, system (anything else → 422).
Modalities
| Modality | Typical use |
|---|---|
conversation | Chat turns, meeting transcripts |
document | Documents and long text |
tool_result | Structured tool output |
observation | Sensor readings, generic observations |
feedback | Ratings, corrections, thumbs up/down |
imported | Records written by /v1/import/* |
The value is an open-set label: it is stored verbatim and echoed by reads, and values outside the
list above are accepted too (banana writes 202 and reads back unchanged). Modality does not
gate extraction: with enrichment configured, every event is enriched the same way, feedback and
other values included. Extraction into Facts/Beliefs requires
enrichment to be configured at all — see Self-hosting defaults.
Per-event opt-out: send directives.extract: [] through bulk
To skip enrichment for one event, send directives: { "extract": [] } via POST /v1/experience/bulk
(a one-item bulk request works). The single POST /v1/experience route runs enrichment regardless of
the directive.
Context
{
"observed_at": "2026-05-15T10:42:00Z",
"source_recorded_at": "2026-05-15T10:41:58Z",
"location": { "city": "Bangalore" },
"preceded_by": ["evt_01HX..."],
"intent": "deal_status_update",
"labels": ["acme", "renewal"]
}All context fields are optional. preceded_by chains experiences by CortexDB event id (evt_…) so the
episode builder recognizes a sequence.
Language, script and time zone (v0.10.1)
Three optional hints, each gated on a capability in GET /v1/admin/version → capabilities[] (a server
that doesn't list it refuses the field with 422 INVALID_BODY):
| Field | Where | Example | Capability |
|---|---|---|---|
language | top level | "hi-Latn" (BCP-47) | capture_language_v1 |
scripts | top level | ["Deva"] (ISO 15924, at most 8) | capture_language_v1 |
context.timezone | context | "Asia/Kolkata" (exact IANA name) | capture_language_v1 |
context.refers_to | context | [{ "from": "2026-09-26", "precision": "day" }] | refers_to_v1 |
Without hints the server still tags non-English text by detection (und-Deva, hi-Latn); English
stores no tag. None of these is part of the write identity. The full rules are on
Languages & Multilingual Memory.
Source descriptor (optional; added after v0.9.9)
source is an optional, provenance-bound description of the authoritative source this envelope
represents and the exact fragments of it: which document, which page or turn, and a strong digest of
the bytes. It is stored verbatim on the event and returned by GET /v1/events and recall (the key is
absent on events written without one), so a citation can point past the event to the original
artifact. Available since v0.9.13.
{
"source": {
"schema_version": 2,
"source_id": "doc:policies/refunds.md",
"modality": "text",
"content_kind": "markdown",
"language": "en",
"source_fingerprint": "sha256:b493bf8a…",
"fragments": [
{ "fragment_id": "f1",
"locator": { "kind": "document", "page": 1, "section_path": ["Refunds"] },
"content_fingerprint": "sha256:b493bf8a…" }
]
}
}- Required:
schema_version(always2),source_id,modality,content_kind,source_fingerprint,fragments(1 to 1024).languageis optional. - Fingerprints are algorithm-qualified digests of exact bytes:
sha256:orblake3:plus 64 hex chars. - Each fragment needs
fragment_id,content_fingerprintand alocatorwhosekindis one ofconversation(session_id,turn_index, optionalspeaker,message_id),document(page,section_path,block_index),json(pointer),table(table,sheet,row,column,field),code(file, optionalsymbol,lines),log,media(page,frame_index,time_range,region,track) orgeneric(path,ordinal). An unknown locator field or a malformed fingerprint is422 INVALID_BODYwith the offendingfield_path. source.modalitymust equal the envelopemodality; a mismatch is422 INVALID_ENVELOPE(source modality "image" does not match envelope modality "text").
CortexDB does not fetch the source. It does check one thing: a fragment's content_fingerprint must be
the digest of the envelope's own text bytes, or the write is 422 INVALID_ENVELOPE (source fragment "f1" fingerprint does not match envelope text bytes).
source_fingerprint, the digest of the whole source, is not checked. Otherwise the descriptor is a claim
you make about where the content came from, kept immutable alongside the event.
Idempotency
idempotency_key (≤ 64 chars) is optional but recommended for safe retries. Same key + same body →
no-op; same key + different body → 409 IDEMPOTENCY_CONFLICT. Use a deterministic key per
source-system message (e.g. slack:C123:T456:1747293720). Idempotency records persist across restarts, so
a retry after a crash replays the same event_id (replayed_from_idempotency: true) rather than
duplicating. Keys are per scope: the same key in two different scopes creates two events. They
expire after ~24 h.
Operational directives
{
"directives": {
"extract": ["facts", "beliefs"],
"consolidate_into": "org:acme/dept:eng",
"confidence_floor": 0.7,
"ttl_for_belief_layer": "P30D",
"embed": "eager"
}
}| Field | Notes |
|---|---|
extract | Subset of facts, entities, beliefs, episodes, understanding. [] opts the event out of enrichment — honored by /v1/experience/bulk only (see Modalities) |
consolidate_into | Merge derived layers into a different scope (requires scope.write on both) |
confidence_floor | Drop derived records below this confidence |
ttl_for_belief_layer | ISO-8601 duration, stored with the event; derived records don't expire from it (use retention for expiry) |
embed | eager / lazy / none |
FAQ
Does the envelope block the write path? No — it returns 202 after the capture phase; extraction and
consolidation run asynchronously.
Can it handle tool outputs? Yes — the json content kind ingests structured tool outputs / sensor readings.
How does it handle identity? Three slots (caller, observed actor, subject) let bots act on behalf of a user or write about other entities.
See also
- Lifecycle — the six atomic operations of memory.
- Scopes — how scopes partition memory.
POST /v1/experience— the full API reference.