Memories

Create a memory

Writes a fact to the specified memory store through the standard write algorithm: the content is embedded (or matched to text the store already holds), compared against the most similar currently-valid memory, and resolved to exactly one of three outcomes — skipped at or above duplicate_threshold, superseded at or above supersede_threshold, created below it. supersedes overrides that comparison entirely, retiring the memory it names. Every call records one assertion, whatever the outcome.

post/v1/projects/{project_id}/memories

Request

  • Base URL: https://api.naturali.ai
  • URL: https://api.naturali.ai/v1/projects/{project_id}/memories
  • Auth: one of:
    • HTTP bearer
    • OAuth 2 (authorization code, authorization URL https://api.naturali.ai/authorize, token URL https://api.naturali.ai/token, scopes: mcp:access)

Request body

memory_store_idstring required

Memory store to add the memory to (mstore_...)

contentstring required

The text content of the memory

source_type'manual' | 'conversation'

Whether there is a source to point at. conversation requires source_id; manual rejects it.

source_idstring

The conversation this fact was learned in. Required when source_type is conversation, and rejected otherwise.

tagsTagBag

Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with 400 VALIDATION_FAILED, never coerced. Keys are opaque and stored verbatim, so cost_center and costCenter are two different tags. Matched by JSONB containment wherever tags are read: the ?tags= filter and knowledge search.

Keys beginning system. are reserved: the platform writes them to record which conversation, actor, agent and role a row came from, and a write naming one is refused with 400 RESERVED_TAG_KEY. They are read and filtered like any other tag.

The bag is bounded, because every pair reaches the IAM context of every access check on the resource: at most 50 keys, each key at most 128 characters and each value at most 256. A write past a bound — including a merge that would grow the stored bag past the key count — is 400 VALIDATION_FAILED with meta.limit naming the bound it crossed. system.* keys are the platform's and do not count against the 50.

metadataMetadataBag

Caller-owned annotations on a resource, stored as the object they were written as: the types a value was written with are the types a read returns, so a filter can ask an ordering question about a number. Unlike other body fields, keys are stored and returned verbatim in the casing supplied — they are not converted between snake_case and camelCase.

No key is reserved, and that is the point: every piece of state the platform owns lives in its own typed column, so nothing written here reaches platform state. The platform never reads the bag — it is not an IAM context, not a policy input and not part of a prompt — which is what separates it from a tag bag.

supersedesstring

The memory this write replaces, named outright. The declaration outranks the thresholds in both directions: the target is retired and the write returns superseded however similar or distant the two texts are. This is what reaches a contradiction cosine cannot see ("The office is in Lisbon" then "We closed the Lisbon office"). The target must be a still-valid memory in the same memory store, and the caller needs memories:UpdateMemory on it as well as memories:CreateMemory on the store.

duplicate_thresholdnumber

Cosine similarity at or above which the incoming content is a duplicate of an existing memory and the write is skipped. Overrides the store's duplicate_threshold for this call; falls back to the store's value, then to 0.95.

supersede_thresholdnumber

Cosine similarity at or above which the incoming content restates a fact that has changed: the top match is invalidated and a new memory replaces it. Overrides the store's supersede_threshold for this call; falls back to the store's value, then to 0.90. The effective pair must satisfy supersede_threshold < duplicate_threshold, and the check runs against the effective pair — so overriding only one value cannot invert it against the store's other one.

Example request

{
  "memory_store_id": "mstore_V1StGXR8Z5jdHi6B",
  "content": "The customer prefers email communication over phone calls",
  "source_type": "manual",
  "source_id": "conv_V1StGXR8Z5jdHi6B",
  "tags": {
    "team": "finance",
    "env": "prod"
  },
  "metadata": {
    "author": "John",
    "revision": 2
  },
  "supersedes": "mem_V1StGXR8Z5jdHi6B",
  "duplicate_threshold": 0.95,
  "supersede_threshold": 0.9
}

Response

The write resolved against an existing memory — action is skipped (the fact was already known) or superseded (the fact had changed, and the returned memory is the replacement).

idstring
memory_store_idstring
contentstring

The text this memory currently holds. Stored once per distinct text per store and shared with every assertion that stated it, so a memory carries no copy of its own.

source_type'manual' | 'conversation'

Whether there is a source to point at. conversation means source_id names the conversation this fact was learned in; manual means there is nothing to point at — a direct API write, or an agent write made outside a conversation.

source_idstring nullable

The conversation this fact was learned in, when source_type is conversation; null when it is manual. Deliberately a loose pointer rather than a foreign key, so deleting the conversation leaves the id in place — the record of where the fact came from outlives its source.

tagsTagBag

Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with 400 VALIDATION_FAILED, never coerced. Keys are opaque and stored verbatim, so cost_center and costCenter are two different tags. Matched by JSONB containment wherever tags are read: the ?tags= filter and knowledge search.

Keys beginning system. are reserved: the platform writes them to record which conversation, actor, agent and role a row came from, and a write naming one is refused with 400 RESERVED_TAG_KEY. They are read and filtered like any other tag.

The bag is bounded, because every pair reaches the IAM context of every access check on the resource: at most 50 keys, each key at most 128 characters and each value at most 256. A write past a bound — including a merge that would grow the stored bag past the key count — is 400 VALIDATION_FAILED with meta.limit naming the bound it crossed. system.* keys are the platform's and do not count against the 50.

metadataMetadataBag

Caller-owned annotations on a resource, stored as the object they were written as: the types a value was written with are the types a read returns, so a filter can ask an ordering question about a number. Unlike other body fields, keys are stored and returned verbatim in the casing supplied — they are not converted between snake_case and camelCase.

No key is reserved, and that is the point: every piece of state the platform owns lives in its own typed column, so nothing written here reaches platform state. The platform never reads the bag — it is not an IAM context, not a policy input and not part of a prompt — which is what separates it from a tag bag.

invalidated_atstring date-time nullable

When the memory stopped holding, whether a supersede replaced it or a retraction withdrew it. Null means the memory is currently valid. Invalidated memories are excluded from listing, from write deduplication, and from knowledge search, but remain readable by ID — with their original text — for audit.

superseded_by_memory_idstring nullable

The memory that replaced this one, when it was superseded. Null for a valid memory and for a retracted one, which nothing replaced.

versioninteger

The memory's write version, starting at 1 and incremented on every update. States a precondition against it with expected_version or If-Match on PUT /memories/{memory_id}.

created_atstring date-time
updated_atstring date-time
action'created' | 'superseded' | 'skipped'

The outcome of the write. created means the fact was distinct and a new memory holds it. superseded means it restated a fact that had changed: the matched memory was invalidated, points at the replacement, and the replacement is what is returned. skipped means the fact was already known and the existing memory is returned unchanged. Every outcome is recorded as an assertion, readable at GET /v1/projects/{project_id}/memories/{memory_id}/assertions.

Example response

{
  "id": "mem_V1StGXR8Z5jdHi6B",
  "memory_store_id": "mstore_V1StGXR8Z5jdHi6B",
  "content": "The customer prefers email communication over phone calls",
  "source_type": "manual",
  "source_id": "conv_V1StGXR8Z5jdHi6B",
  "tags": {
    "team": "finance",
    "env": "prod"
  },
  "metadata": {
    "author": "John",
    "revision": 2
  },
  "superseded_by_memory_id": "mem_V1StGXR8Z5jdHi6B",
  "version": 1
}

Changes

No recorded changes to this operation across all 1 revision of this API.