Memory
Store and retrieve memories in the Memory Library. Memories can belong to one or more agents (linkedIds), are kind-typed (fact / episodic / semantic / instruction / preference), versioned, and bitemporal (validFrom / validUntil). Search blends BM25 + dense vectors via RRF with temporal decay; ingest runs a Mem0-style LLM judge that decides ADD / UPDATE / DELETE / NOOP against retrieved candidates.
All endpoints require authentication via X-API-Key header and the appropriate scope.
Memory Object
{
"_id": "mem_abc123",
"ownerId": "user_456",
"ownerName": "Alice Example",
"organizationId": "org_789",
"sharedWith": [],
"isPublic": false,
"source": "user",
"tags": ["preference", "languages"],
"category": "engineering",
"linkedIds": [],
"currentVersion": 2,
"kind": "fact",
"content": "User prefers TypeScript over JavaScript for new projects.",
"summary": "Prefers TypeScript",
"eventTime": "2026-05-01T09:00:00Z",
"validFrom": "2026-05-01T09:00:00Z",
"validUntil": null,
"sourceThreadId": "thr_789",
"sourceMessageId": "msg_456",
"sourceToolName": null,
"importance": 0.7,
"decayHalfLifeDays": 90,
"usageCount": 4,
"lastAccessedAt": "2026-05-10T12:00:00Z",
"links": [
{ "targetId": "mem_def456", "relation": "supports", "weight": 0.8 }
],
"confidence": 0.9,
"contradictionOf": null,
"scoreResult": {
"score": 82,
"grade": "B",
"rulesetVersion": "1.0.0",
"failedCriticalCount": 0,
"failedHighCount": 1
},
"lastAssessedAt": "2026-05-10T12:00:00Z",
"createdAt": "2026-05-01T09:00:00Z",
"updatedAt": "2026-05-10T12:00:00Z"
}
linkedIds lists the agents the memory belongs to. scoreResult (its quality score and grade) and lastAssessedAt are kept current by every change to the memory; assess returns the full report behind them.
GET /api/v1/primitives/memories
List memories with pagination, kind/tag/search filters, a linked-agent filter, and bitemporal asOf selection.
Scope: memory:read
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
kind | string | No | Filter by kind: fact, episodic, semantic, instruction, preference |
tags | string | No | Comma-separated tag list; a memory must carry all of them |
linkedIds | string | No | Comma-separated agent IDs; a memory must belong to all of them |
q | string | No | Full-text search on content, summary, and tags |
limit | integer | No | Max results (default 50, max 200) |
cursor | string | No | meta.nextCursor of the previous page; omit for the first page |
asOf | string | No | ISO timestamp -- return only memories valid at this instant (bitemporal). Defaults to now; 400 validation-error if not a valid date |
includeInvalidated | boolean | No | true includes rows whose validUntil is in the past (and asOf is then ignored) |
Response 200 OK
{
"data": [ /* Memory objects */ ],
"meta": {
"total": 25,
"limit": 50,
"nextCursor": null,
"requestId": "req_abc123"
}
}
POST /api/v1/primitives/memories
Create a new memory.
Scope: memory:write
Request Body
{
"kind": "fact",
"content": "User prefers TypeScript over JavaScript for new projects.",
"summary": "Prefers TypeScript",
"tags": ["preference", "languages"],
"category": "engineering",
"source": "user",
"linkedIds": [],
"importance": 0.7,
"confidence": 0.9,
"decayHalfLifeDays": 90,
"eventTime": "2026-05-01T09:00:00Z",
"validFrom": "2026-05-01T09:00:00Z",
"validUntil": null,
"sourceThreadId": "thr_789",
"sourceMessageId": "msg_456",
"sourceToolName": null,
"links": []
}
| Field | Type | Required | Description |
|---|---|---|---|
kind | string | Yes | fact / episodic / semantic / instruction / preference |
content | string | Yes | Memory body |
summary | string | No | One-line summary |
tags | string[] | No | Discoverability tags |
category | string | No | Free-form category |
source | string | No | Provenance label (default: user) |
linkedIds | string[] | No | Agent IDs the memory belongs to. When set, the memory is owned by that agent's owner |
importance | number | No | 0..1 retention weight |
confidence | number | No | 0..1 truth confidence |
decayHalfLifeDays | number | No | Per-row temporal decay half-life used at search time |
eventTime | string | No | ISO timestamp the memory describes |
validFrom | string | No | ISO timestamp the assertion starts being true |
validUntil | string | null | No | ISO timestamp the assertion stops being true (null = open-ended) |
sourceThreadId | string | No | Originating thread |
sourceMessageId | string | No | Originating message |
sourceToolName | string | No | Originating tool call |
links | object[] | No | Initial graph edges to other memories |
Response 201 Created
{
"data": { "memoryId": "mem_abc123" },
"meta": { "requestId": "req_abc123" }
}
GET /api/v1/primitives/memories/:id
Get a single memory by ID.
Scope: memory:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Memory ID |
Response 200 OK
Returns the full Memory object.
PATCH /api/v1/primitives/memories/:id
Update a memory. Content changes append a new version row -- history is preserved.
Scope: memory:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Memory ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
kind | string | No | Memory kind |
content | string | No | New content (triggers a new version snapshot) |
summary | string | No | One-line summary |
tags | string[] | No | Tags |
category | string | null | No | Category (pass null to clear) |
eventTime | string | No | ISO timestamp |
validFrom | string | No | ISO timestamp |
validUntil | string | null | No | ISO timestamp or null |
sourceThreadId | string | No | Thread provenance |
sourceMessageId | string | No | Message provenance |
sourceToolName | string | No | Tool provenance |
importance | number | No | 0..1 retention weight |
decayHalfLifeDays | number | No | Per-row decay half-life |
confidence | number | No | 0..1 truth confidence |
links | object[] | No | Graph edges |
linkedIds | string[] | No | Replace the agents and pools the memory belongs to |
changeNote | string | No | Free-form note attached to the new version |
Response 200 OK
The memory, as GET /primitives/memories/:id shows it.
DELETE /api/v1/primitives/memories/:id
Delete a memory and its version history. Irreversible.
Scope: memory:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Memory ID |
Response 204 No Content
POST /api/v1/primitives/memories/:id/access-events
Record an access -- increments usageCount and updates lastAccessedAt.
Scope: memory:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Memory ID |
Response 201 Created: { "usageCount": 5 }, how many times the memory has been used.
Sharing
Who can reach a memory is the platform's one sharing shape: its owner, its members
(role editor can use and change it, user can only use it) and its visibility
(public: every user can find and use it; in a multi-tenant deployment, every user of
its organization). Changing it stays with its owner and editors.
| Method | Path | Does | Scope |
|---|---|---|---|
| GET | /api/v1/primitives/memories/:id/permissions | Owner, members (userId, role) and visibility | memory:read |
| POST | /api/v1/primitives/memories/:id/permissions/members | Share with a user: { "userId", "role": "editor" | "user" } | memory:write |
| DELETE | /api/v1/primitives/memories/:memoryId/permissions/members/:userId | Stop sharing with a user | memory:write |
| PATCH | /api/v1/primitives/memories/:id/permissions | { "visibility": "public" | "private" } | memory:write |
Each, except a member's removal (204), answers the permissions as they are now:
{
"data": {
"resourceId": "<memory id>",
"owner": "<user id>",
"members": [{ "userId": "<user id>", "role": "user" }],
"visibility": "private"
},
"meta": { "requestId": "req_abc123" }
}
POST /api/v1/primitives/memories/import
Bulk re-ingest of the JSON shape GET /api/v1/primitives/memories/export emits.
Scope: memory:write
Request Body
{
"memories": [ /* Memory rows, or pass the full export wrapper */ ]
}
| Field | Type | Required | Description |
|---|---|---|---|
memories | array | No | Array of memory rows. If absent, the entire body is treated as the export wrapper. |
Response 200 OK
{
"data": { "imported": 41, "unchanged": 1, "skipped": 1, "errors": [{ "index": 7, "message": "row.content must be a non-empty string" }] },
"meta": { "requestId": "req_abc123" }
}
Imported memories belong to the caller. imported counts new memories; unchanged counts rows the caller already has (same content), which are not created again; skipped counts refused rows, each in errors with its index (from 0).
GET /api/v1/primitives/memories/export
Bulk-export every memory the caller can read as JSON.
Scope: memory:read
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
includeInvalidated | boolean | No | Include rows with validUntil in the past |
Response 200 OK
{
"data": {
"exportedAt": "2026-05-16T10:00:00Z",
"count": 25,
"memories": [ /* Memory objects */ ]
},
"meta": { "requestId": "req_abc123" }
}
POST /api/v1/primitives/memories/consolidate
Run deterministic sleep-time consolidation: detects duplicates, evicts stale low-value rows, promotes heavily-used ones. Pass dryRun: true to preview without writing.
Scope: memory:write
Request Body
{
"config": {
"evictionScoreThreshold": 0.2,
"evictionIdleDays": 90,
"duplicateOverlapThreshold": 0.85,
"promotionUsageThreshold": 10,
"promotionImportanceCeiling": 0.5,
"promotionImportanceTarget": 0.8
},
"dryRun": false
}
| Field | Type | Required | Description |
|---|---|---|---|
config | object | No | Override default thresholds |
dryRun | boolean | No | Return the plan without applying changes |
Response 200 OK
{
"data": {
"ownerId": "user_456",
"config": { /* thresholds used for this run */ },
"candidatesScanned": 120,
"plan": {
"evictions": [ /* memories selected for eviction */ ],
"promotions": [ /* memories selected for promotion */ ],
"duplicates": [ /* duplicate clusters */ ]
},
"applied": true,
"duplicatesInvalidated": 2,
"evictionsInvalidated": 3,
"promotionsApplied": 1,
"memoriesAutoLinked": 4,
"autoLinksAdded": 6,
"decayBackfilled": 0,
"startedAt": "2026-05-16T10:00:00Z",
"finishedAt": "2026-05-16T10:00:02Z"
},
"meta": { "requestId": "req_abc123" }
}
With dryRun: true, applied is false, plan lists what would change, and the three applied counts are 0.
POST /api/v1/primitives/memories/:id/assess
Run the Memory quality ruleset against a row and cache the score on it. Returns the full per-check report.
Scope: memory:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Memory ID |
Response 200 OK
{
"data": {
"rulesetVersion": "1.0.0",
"assessedAt": "2026-05-16T10:00:00Z",
"score": 82,
"grade": "B",
"weightAwarded": 41,
"weightTotal": 50,
"failedCriticalCount": 0,
"failedHighCount": 1,
"checks": [
{
"id": "mem-content-nonempty",
"title": "Content is non-empty",
"category": "content",
"severity": "critical",
"weight": 10,
"awarded": 10,
"status": "pass",
"passed": true
}
]
},
"meta": { "requestId": "req_abc123" }
}
Each check's status is pass, fail or skipped. A skipped check does not apply to this row (for example the eventTime check on a memory that is not episodic): it earns nothing and its weight is left out of weightTotal, so score is weightAwarded / weightTotal over the checks that apply.
POST /api/v1/primitives/memories/ingest
Mem0-style ingest. Embeds the new content, retrieves similar memories, asks an LLM to decide ADD / UPDATE / DELETE / NOOP, and executes the action.
Scope: memory:write
Request Body
{
"content": "User prefers TypeScript over JavaScript for new projects.",
"kind": "preference",
"tags": ["preference", "languages"],
"linkedIds": [],
"model": "gpt-4o",
"candidateK": 5
}
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | The new piece of information |
kind | string | Yes | fact / episodic / semantic / instruction / preference |
tags | string[] | No | Tags applied if the judge chooses ADD |
linkedIds | string[] | No | Agent IDs: the judge compares against those agents' memories, and the resulting memory belongs to them |
model | string | No | Override the judge LLM model |
candidateK | number | No | How many similar memories to show the LLM (default 5) |
Response 200 OK
{
"data": {
"action": "ADD",
"memoryId": "mem_abc123",
"reasoning": "No existing memory covers this preference."
},
"meta": { "requestId": "req_abc123" }
}
action is ADD, UPDATE, DELETE or NOOP. memoryId is omitted for NOOP.
Returns 400 validation-error for empty content/kind or judge parse errors; 503 configuration-error if no judge model or org is configured.
POST /api/v1/primitives/memories/:id/links
Create or update a graph edge from this memory to another. The edge is stored on this memory only (you need edit access to it and read access to the target); the target lists it as an incoming link (GET /primitives/memories/:id/backlinks).
Scope: memory:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Source memory ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
targetId | string | Yes | Target memory ID |
relation | string | Yes | supports / contradicts / refines / related |
weight | number | No | Edge weight in [0, 1] |
Response 200 OK
{
"data": { "success": true },
"meta": { "requestId": "req_abc123" }
}
DELETE /api/v1/primitives/memories/:memoryId/links/:id
Remove the link from this memory to a target. Only this memory changes; a link the target has back to it stays.
Scope: memory:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
memoryId | string | Yes | Source memory ID |
id | string | Yes | Target memory ID |
Response 200 OK
{
"data": { "success": true },
"meta": { "requestId": "req_abc123" }
}
GET /api/v1/primitives/memories/:id/backlinks
List memories that link to this one (reverse-edge lookup).
Scope: memory:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Memory ID |
Response 200 OK
{
"data": {
"memories": [ /* Memory objects with edges pointing to :id */ ],
"count": 3
},
"meta": { "requestId": "req_abc123" }
}
POST /api/v1/primitives/memories/:id/invalidate
Mark a memory as contradicted or superseded. Sets validUntil = now and contradictionOf (if supplied). History is preserved.
Scope: memory:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Memory ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
supersededBy | string | No | Memory ID that supersedes this one |
Response 200 OK
The memory, as GET /primitives/memories/:id shows it, its validUntil set.
POST /api/v1/primitives/memories/search
Hybrid (BM25 + dense vector) memory search with RRF fusion and temporal-decay re-ranking. Kind, tag, and linked-agent filters apply uniformly to both signals.
Scope: memory:read
Request Body
{
"query": "What does the user prefer for new TypeScript projects?",
"k": 10,
"kind": "preference",
"tags": ["languages"],
"rrfK": 60,
"weights": { "vector": 1.0, "text": 1.0 },
"decayHalfLifeDaysOverride": 90,
"asOf": "2026-05-16T10:00:00Z",
"includeInvalidated": false,
"rerank": true,
"rerankModel": "bge-reranker"
}
| Field | Type | Required | Description |
|---|---|---|---|
query | string | Yes | Natural-language query |
k | number | No | Max results (default 10, max 200) |
kind | string | No | Filter by memory kind |
tags | string[] | No | Filter by tags; a memory must carry all of them |
linkedIds | string[] | No | Agent IDs; a memory belonging to any of them matches, and memories shared with the whole organization are included too |
rrfK | number | No | RRF k constant (default 60) |
weights | object | No | {vector?, text?} per-list weights for RRF |
decayHalfLifeDaysOverride | number | null | No | Override per-row decay; pass null to disable decay |
asOf | string | No | ISO timestamp for bitemporal filter |
includeInvalidated | boolean | No | Include invalidated rows |
rerank | boolean | No | Run a reranker over the fused list |
rerankModel | string | No | Override the reranker model |
Response 200 OK
{
"data": {
"results": [
{ "memory": { /* Memory object */ }, "score": 0.0312, "rank": 1 }
],
"count": 1,
"contextBlock": "## Relevant memories\n..."
},
"meta": { "requestId": "req_abc123" }
}
results are ordered best first (rank 1 is the best match). contextBlock is the same results rendered as a markdown block ready to place in a system prompt.
GET /api/v1/primitives/memories/:id/versions
List every version of a memory with reconstructed content, newest first.
Scope: memory:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Memory ID |
Response 200 OK
{
"data": {
"versions": [
{
"versionNumber": 2,
"content": "Updated content",
"changeNote": "Clarified phrasing",
"createdAt": "2026-05-10T12:00:00Z"
},
{
"versionNumber": 1,
"content": "Original content",
"changeNote": null,
"createdAt": "2026-04-01T09:00:00Z"
}
],
"count": 2
},
"meta": { "requestId": "req_abc123" }
}
GET /api/v1/primitives/memories/:memoryId/versions/:version
Fetch one version of a memory with reconstructed content.
Scope: memory:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
memoryId | string | Yes | Memory ID |
version | integer | Yes | Version number (positive integer) |
Response 200 OK
{
"data": {
"versionNumber": 2,
"content": "Updated content",
"changeNote": "Clarified phrasing",
"createdAt": "2026-05-10T12:00:00Z"
},
"meta": { "requestId": "req_abc123" }
}
POST /api/v1/primitives/memories/:memoryId/versions/:version/restore
Restore the memory to a prior version. Appends a new version row -- does not mutate history.
Scope: memory:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
memoryId | string | Yes | Memory ID |
version | integer | Yes | Version number to restore to |
Response 200 OK
The memory, as GET /primitives/memories/:id shows it: its content is the restored version's, saved as a new version.
POST /api/v1/primitives/memories/:id/promote
Put a memory in your organization's org-global pool, so every agent in the organization reads it. Use it for a durable fact the whole team should share.
Scope: memory:write
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Memory id |
Response 200 OK: the memory, its linkedIds now holding the org-global pool's id.
POST /api/v1/primitives/memories/:id/demote
Take a memory out of the org-global pool: agents stop reading it from there.
Scope: memory:write
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Memory id |
Response 200 OK: the memory.