---
title: Concepts
description: The primitives in an OpenMemory space - memories, journal, records, actions, KV, events, context, batch, and forget - and how they fit together.
---

Everything lives in a **space**. A space has one API key, a timezone, a dream mode, and these stores:

| Primitive | What it holds | Typical use |
| --- | --- | --- |
| [Memories](#memories) | Short facts with full-text recall | Preferences, people, decisions |
| [Journal](#journal) | One entry per day (and per week) | What happened, decisions, open loops |
| [Records](#records) | Typed JSON in named collections | Commitments, goals, routines, contacts |
| [Actions](#actions) | An approval ledger | Anything with side effects that needs a yes |
| [KV](#kv) | Namespaced JSON values | Settings, sync cursors, locks |
| [Events](#events) | A capped append-only log | Run history, audit breadcrumbs |

Two calls tie them together: [`GET /v1/context`](#context) reads what a run needs in one go, and [`POST /v1/batch`](#batch) writes several things atomically. [Dreaming](/docs/dreaming) tidies memory overnight, and [forget](#forget) deletes things for good.

## Spaces

Create one with `POST /v1/spaces` (no auth, rate limited per IP address). The response contains `space_id` and `api_key` (`om_...`). The key is shown once. Only its SHA-256 hash is stored.

Send the key on every other call as `Authorization: Bearer om_...` or `X-API-Key: om_...`.

- `GET /v1/space` shows settings, usage counts, limits, and `delete_after` (when the space will be deleted if it stays unused).
- `PATCH /v1/space` changes `timezone` (IANA name) or `dream_mode` (`client`, `server`, or `off`).
- `GET /v1/export` returns everything as one JSON document.
- `DELETE /v1/space` deletes the space and everything in it, permanently.

Spaces are isolated: one key can never read or change another space. Spaces unused for 90 days are deleted.

## Memories

A memory is one fact, in one or two sentences.

```json
{
  "subject": "Sam",
  "text": "Prefers code reviews on Thursdays",
  "tier": "core",
  "tags": ["work"],
  "source_ref": "slack:C123/1712",
  "pinned": false,
  "origin": "user",
  "expires_at": null
}
```

| Field | Meaning |
| --- | --- |
| `subject` | Who or what it is about (up to 200 characters). Optional. |
| `text` | The fact (1 to 4,000 characters). Required. |
| `tier` | `core` facts are always in context. `long` (default) facts are recalled on demand. `scratch` facts are short-lived. |
| `tags` | Up to 20 tags of up to 50 characters each. |
| `source_ref` | Where it came from (message id, URL). Do not paste message bodies here. |
| `pinned` | Pinned facts are always in context and never changed by dreaming. |
| `origin` | `user` when the person stated it, `agent` (default) when the agent inferred it. Dreaming writes `dream`. |
| `expires_at` | Optional ISO 8601 time after which the fact is hidden and later removed. |

Writing the exact same `subject` and `text` again returns the existing fact (`200`, `created: false`) instead of storing a duplicate.

**Recall** (`GET /v1/memories`, MCP `recall`) searches with full-text matching that ignores case and accents and works in any script, including Greek (final sigma and tonos are folded). Any word may match, words match as prefixes, and pinned and core facts get a small boost. Without `q` it lists facts: pinned first, then core, long, scratch, newest first. You can filter by `subject` (exact, case-insensitive), `tier`, or `tag`.

Each recall increments `recall_count` on the facts it returns (pass `touch=false` over REST to skip that). Dreaming uses recall counts to decide which facts to promote to core.

Change a fact with `PATCH /v1/memories/{id}`. Delete one with `DELETE /v1/memories/{id}`, or many with [forget](#forget).

## Journal

One entry per date and kind. `kind` is `day` (default) or `week`. Dreaming writes `week` digests.

```bash
curl -s -X PUT https://api.openmemory.sh/v1/journal/2026-11-01 \
  -H "Authorization: Bearer $OPENMEMORY_KEY" -H 'content-type: application/json' \
  -d '{"summary":"Shipped the export endpoint","decisions":["Use JSON, not CSV"],"open_loops":["Ask Sam about retention"],"merge":true}'
```

`PUT` replaces the entry unless `merge` is `true`. With merge, the summary is appended on a new line and the lists are combined without duplicates. Lists hold up to 50 items of up to 500 characters each, and the summary up to 4,000 characters. `GET /v1/journal` lists entries newest first, filtered by `from`, `to`, and `kind`.

## Records

Records are typed JSON objects in named **collections** (`commitments`, `goals`, `routines`, anything you like). Collection names use letters, digits, `_`, `.`, and `-`, up to 64 characters. Names starting with `dream.` are written by dreaming.

```json
{ "key": "deck-for-sam", "status": "open", "due_at": "2026-11-05T09:00:00Z", "data": { "what": "Send Sam the deck" } }
```

- **Upsert by key.** `POST /v1/collections/{collection}/records` with a `key` updates the record that has that key (and bumps its `version`). Without a key it always creates.
- **Compare-and-set.** `PATCH` takes `expected_version`. If the record has moved on, you get `409` with `current_version` in the error details.
- **Shallow merge.** `PATCH` merges `data` into the existing object. Set a field to `null` to remove it.
- **Query** by `status` (comma-separated), `key`, or `due_before`. Results are ordered by due date (undated last). Page with `next_cursor`.

Records whose status is `done`, `dropped`, `closed`, `cancelled`, `archived`, or `rejected` count as closed. Every other status counts as open for context.

## Actions

The approval ledger. An agent proposes something with side effects, the owner approves it, and execution is recorded with a receipt.

```bash
curl -s -X POST https://api.openmemory.sh/v1/actions \
  -H "Authorization: Bearer $OPENMEMORY_KEY" -H 'content-type: application/json' \
  -d '{"class":"email_reply","summary":"Reply to Sam: Thursday works","payload":{"to":"sam@example.com","body":"Thursday works"},"idempotency_key":"reply-sam-1712","expires_at":"2026-11-03T18:00:00Z"}'
```

- Each action gets a short id per space: `A1`, `A2`, ... Ids are case-insensitive (`a7` works).
- **Idempotency.** Reusing an `idempotency_key` with the same payload returns the existing action (`200`, `created: false`). Payloads are compared by content, so key order does not matter. The same key with a different payload is a `409`.
- **Compare-and-set transitions.** `POST /v1/actions/{id}/transition` with `{"from": "approved", "to": "executing"}` succeeds only if the action is still `approved`. Two workers cannot both execute A7.
- **Expiry.** Pending or approved actions past `expires_at` become `expired` and can no longer be approved or executed.
- **Receipts.** Send `receipt` (any JSON object) with `to: "done"` or `to: "failed"` to record what happened.

Allowed transitions:

| From | To |
| --- | --- |
| `pending` | `approved`, `executing`, `denied`, `expired`, `superseded`, `blocked` |
| `approved` | `executing`, `denied`, `expired`, `superseded` |
| `executing` | `done`, `failed` |
| `blocked` | `pending`, `denied`, `superseded` |
| `failed` | `pending` |
| `done`, `denied`, `expired`, `superseded` | none (final) |

`GET /v1/actions` lists open actions (`pending`, `approved`, `executing`) by default. Pass `status=done,failed` for others.

## KV

Namespaced JSON values for settings, cursors, and small state. A namespace uses the same name rules as collections. Namespaces starting with `_` are reserved. Values can be any JSON up to 64 KB.

`PUT /v1/kv/{ns}/{key}` with `{"value": ..., "expected_version": n}`:

- omit `expected_version` to upsert,
- `0` to create only (fails with `409` if the key exists, which makes a simple lock),
- `n` to update only if the value is still at version `n`.

`expires_at` makes a value disappear after a time. `GET /v1/kv/{ns}` lists a namespace and `GET /v1/kv/{ns}/{key}` reads one value.

## Events

An append-only log of small structured entries: `{"kind": "run", "key": "daily-brief", "status": "ok", "detail": {...}}`. It is capped per space and the oldest events are dropped first. List newest first with filters `kind`, `since`, and `before_id` (for paging). Keep message bodies and secrets out of `detail`.

## Context

`GET /v1/context` (MCP `get_context`) is the one call to make at the start of a run. It returns:

| Field | Contents |
| --- | --- |
| `now`, `timezone`, `today` | The current time and the space's local date. |
| `profile.text` | Pinned and core facts, one per line (`- Subject: text [mem_id]`), cut to `budget_chars`. `truncated` tells you whether some were left out. |
| `profile.dream_summary` | The owner profile written by the last dream, if any. |
| `due` | Open records due within `due_within_days`. |
| `open_records` | Open records in the collections you name in `collections`. Empty if you name none. |
| `actions` | Pending, approved, and executing actions. |
| `journal` | Day entries from the last `journal_days` days. |
| `weekly` | The latest weekly digest. |
| `kv` | The namespaces you list in `kv`, as `{ns: {key: value}}`. |
| `last_dream` | Id, status, finish time, and report of the most recent dream. |

Parameters: `budget_chars` (200 to 20,000, default 4,000), `journal_days` (0 to 31, default 7), `due_within_days` (0 to 90, default 7), `kv` (comma-separated namespaces), and `collections` (comma-separated, limits `due` and fills `open_records`).

## Batch

`POST /v1/batch` applies up to 50 writes in one transaction: all succeed or none do.

```json
{
  "ops": [
    { "op": "memory.remember", "input": { "text": "Sam is out Nov 10-14", "origin": "user" } },
    { "op": "journal.put", "date": "2026-11-01", "input": { "summary": "Planned the review", "merge": true } },
    { "op": "record.update", "collection": "commitments", "id": "rec_...", "patch": { "status": "done", "expected_version": 3 } },
    { "op": "action.transition", "id": "A7", "input": { "from": "executing", "to": "done", "receipt": { "message_id": "m1" } } },
    { "op": "kv.set", "ns": "cursors", "key": "gmail", "input": { "value": { "history_id": 4411 } } },
    { "op": "event.append", "input": { "kind": "run", "status": "ok" } }
  ]
}
```

Ops: `memory.remember`, `memory.update`, `memory.delete`, `journal.put`, `record.put`, `record.update`, `record.delete`, `action.propose`, `action.transition`, `kv.set`, `kv.delete`, `event.append`.

The response is `{"results": [...]}` with one result per op, in order. A failed version or status check, or a missing row, aborts the whole batch with `409`. The error message names the op index. Ops that read first (`record.update`, a journal merge, `action.propose`) see the state from before the batch.

Batch is REST-only. Over MCP, each write is its own tool call.

## Forget

`POST /v1/forget` (MCP `forget`) hard-deletes. See [Forget and privacy](/docs/forget-and-privacy) for the full rules.

```json
{ "query": "Sam Lee", "scopes": ["memories", "records", "journal"], "dry_run": true }
```

- `ids` deletes those memories (up to 200).
- `query` deletes memories containing **all** of its words, plus records and journal entries containing the **exact phrase** (only in the scopes you list; the default scope is `memories`).
- Journal text is redacted by default (the phrase becomes `[forgotten]`). Set `journal_mode: "delete"` to delete the entries instead.
- `dry_run: true` returns what would be deleted without deleting anything.

Deleted items leave content-free tombstones (kind, id, and time only).

## Errors

Errors have one shape: `{"error": {"code", "message", "details"?}}`.

| Status | Code | When |
| --- | --- | --- |
| 400 | `bad_request` | Invalid input. `details.issues` lists the fields. |
| 401 | `unauthorized` | Missing or unknown key. |
| 404 | `not_found` | No such item in this space. |
| 409 | `conflict` | A version, status, or idempotency check failed. |
| 413 | `bad_request` | Request body larger than 256 KB. |
| 422 | `quota_exceeded` | The space is full for that kind of item. See [Limits](/docs/limits). |
| 429 | `rate_limited` | Too many requests. Retry in a minute. |
