Memory
Metadata and filters
The fields you attach when writing to a memory, and the filters that read them back.
Every item in a memory carries the same small set of fields, whatever produced it. Those fields are what retrieval filters on, what ranking sorts by, and what decides where an item is allowed to travel. This page is the reference for all of them.
What you set when pushing content
pushRows takes an array of rows. Four fields are structural, the rest is
yours.
await cortex.pushRows(officeId, feedKey, [
{
external_id: 'handbook/onboarding.md',
title: 'Engineering onboarding',
content: 'Day one: get an office invite, then run mi login...',
modality: 'document',
file_path: 'onboarding.md',
metadata: { team: 'eng', reviewed: '2026-08-01' },
// Anything else is kept verbatim on the row.
tags: ['handbook', 'onboarding'],
},
]);
| Field | Type | Notes |
|---|---|---|
external_id | string | Required. Stable identity. Pushing the same id again updates that item in place rather than adding a second copy. |
content | string | Required. The text that gets embedded and full-text indexed. |
title | string | Embedded alongside content, and used as the citation label. |
modality | string | What kind of thing this is, for example document, note, or file. |
file_path | string | The office drive filename, when the original was uploaded separately. |
metadata | object | Free-form. Stored with the row and returned on reads. |
Any other top-level key you send is stored verbatim on the row rather than
discarded. That matters because extraction rules read those keys by path: a
tags or wikilinks array becomes graph structure if a rule points at it. See
Typed entities for how a rule is
declared.
Sensitivity
Every item carries a sensitivity, and it is the field that decides where the item is allowed to go. There are exactly three values, enforced as a database constraint rather than a convention:
| Value | Meaning |
|---|---|
local-only | The default. Stays inside the cortex. |
redact | May leave, with sensitive spans removed. |
cloud-ok | May be sent to an external model or service. |
Items land as local-only unless something sets otherwise, so a memory you
have just filled is entirely local-only. You can restrict a query to one
band:
await cortex.answer(officeId, { query: 'pricing', sensitivity: 'cloud-ok' });
Event time and ingest time
Two timestamps mean different things, and confusing them is the usual cause of a wrong answer to a question about recency.
fetched_atstring
When the cortex ingested the row. A file written last year but synced this
morning has a fetched_at of this morning.
event_timestring
When the thing actually happened at its source: the message sent, the event started, the file last modified. This is the field to reason about when the question is about time.
Retrieval reads the question for temporal intent and reports what it decided in
signals.temporal_intent:
| Value | Triggered by | Effect |
|---|---|---|
none | An ordinary question | Ranked by relevance only. |
recency | "the latest", "most recent" | Adds a time-ordered leg. Results gain a time_rank. |
window | "in the last three months" | Applies a hard time bound. |
Because a window is a hard bound, a narrow or empty result set on a
time-phrased question is often the bound doing its job rather than missing
data. signals.temporal_intent tells you which happened.
Filtering a query
Every filter below is optional and they combine. All of them apply to
cortex.answer.
| Filter | Type | Notes |
|---|---|---|
source_table | string | One source table, for example calendar_events. |
integration_id | string | One integration, for example google-workspace. |
sensitivity | string | One of the three values above. |
since | string | RFC 3339 lower bound on item time. |
until | string | RFC 3339 upper bound on item time. |
limit | number | Maximum results. |
expand_graph | boolean | Pull one hop of graph neighbours around the top candidates. Defaults to on. |
include_raw | boolean | Return the full original row per result. Heavier payload. |
const evidence = await cortex.answer(officeId, {
query: 'renewal terms',
source_table: 'gmail_messages',
since: '2026-06-01T00:00:00Z',
limit: 10,
});
Narrowing to a single source is also the cheapest thing you can do to a slow query, because it shrinks every leg of the search at once.
Metadata on a fact you write
remember stores a conclusion rather than a source document, so it takes its
own metadata:
await cortex.remember(officeId, {
agent: 'atlas',
text: 'Solo tier moved to 19.99 per seat in July 2026.',
kind: 'decision',
confidence: 0.9,
source_universal_ids: evidence.universalIds,
metadata: { ticket: 'CLA-1234' },
});
| Field | Type | Notes |
|---|---|---|
agent | string | Required. Who is remembering. Every fact is attributed. |
text | string | Required. One self-contained fact. |
kind | string | A label such as decision or observation. |
confidence | number | 0 to 1. Weights the derivation edges back to the sources. |
source_universal_ids | string[] | What this was concluded from. Pass the ids from the answer you reasoned over. |
sensitivity | string | Defaults to local-only, like everything else. |
metadata | object | Free-form. |
Writes are idempotent per agent and text, so re-running the same remember
updates the existing fact instead of duplicating it.
Deleting
purge removes a filtered subset of a memory: the raw rows, their embeddings,
and everything derived from them. It takes the same filter vocabulary as a
query, and they stack the same way.
// Preview first. This is the default: an absent dry_run means "do not delete".
const plan = await cortex.purge(officeId, { source_table: 'gmail_messages' });
console.log(plan.matched, 'items,', plan.entities_deleted, 'entities');
// Then commit, in batched rounds.
await cortex.purgeAll(officeId, {
source_table: 'gmail_messages',
dry_run: false,
limit: 1000,
});
Four properties are worth knowing before you write the call:
It previews unless told otherwise
dry_run defaults to true. You have to pass dry_run: false to remove
anything, so a forgotten field can never be the destructive choice.
An empty selector is refused
A call with no filters returns 400 rather than matching the whole colony. A dropped variable cannot silently become "delete everything".
Rounds are capped
One call purges at most limit items, capped at 5000, so a wide selector
cannot hold a long lock on the colony database. Loop while remaining > 0,
or let purgeAll do it.
Filters are validated, not silently unmatched
Unlike a query, a bad sensitivity or time_field here is a 400. On a read
an unrecognized value is a confusing empty result; on a delete a silent
no-op is worse, because you would believe it worked.
To remove a whole source rather than a subset, use deregisterFeed, which
takes a feed_key or an integration_id and also drops the feed registration
itself. That is what the dashboard's disconnect-a-source flow calls.
From a shell, mi cortex purge previews by default and needs --yes to
delete:
mi cortex purge --office <id> --table gmail_messages --until 2026-01-01T00:00:00Z
mi cortex purge --office <id> --table gmail_messages --until 2026-01-01T00:00:00Z --yes --all
Current limits
Reads are capped, not paginated
The list and graph reads take a limit and have no cursor, so they are not a
way to walk an entire memory. Use them for sampling and use
retrieval for finding things.