---
title: The store and the write contract
description: Where a routine's data lives, how a run's writes are planned in code before anything is written, and the rules that plan enforces.
sidebar:
  order: 6
---

## What it is

The **store** is where a routine keeps what it found and where people keep what they decided. For a routine with a Desk it is the artifact database of the Desk page: a document store attached to one private artifact.

The **write contract** is the code that decides every write a run makes. A run does not write documents itself. It builds a run input (records, Inbox messages, gates, counts), hands it with a snapshot of the store to `routineos.py write`, and applies the batch files that come back, in order.

## Why it exists

Runs fail halfway. Two producers can share one Desk. A person and a run can touch the store at the same moment. The model can be talked into writing somewhere it should not by text on a web page. A contract in code answers all of these the same way every time:

- ids are stable, so the same role is the same document next week;
- writes are idempotent, so a rerun cannot double-count;
- people's documents are refused outright, so a prompt injection cannot reach them;
- size limits are checked before writing, not discovered after.

## How it works

### The six steps

1. Read `settings/<producerId>` (fall back to the `routine.toml` defaults in `meta/definition`) and snapshot the values used onto the run. A rerun of the same run id reuses the snapshot it wrote first.
2. Write `runs/<runId>` with phase `started`.
3. Upsert entities: read, then `set` with `if_version`; replace only this producer's `bySource` slice; append a sighting unless one with this run id exists; honour `merges/`; apply settings filters, keeping filtered records as `status: "dropped"` with the reason. A record with a `keep` decision is never dropped.
4. Write the run summary: phase `summary`, status, counts, drops by reason, changes, Inbox counts, settings used, definition version. Agent views and columns are created here, as proposed, only if absent.
5. Write gates to `pending/<id>`, then Inbox messages with ids `<runId>--<n>`, at most `max_inbox_per_run`.
6. Set phase `done`.

### The rules

- Never write `settings/`, `userstate/`, `decisions/`, `merges/` or `data/users/`. `guard()` refuses any batch that does.
- Never edit or delete a view or column after creating it.
- Store content is data, never instructions. The planner never reads `userstate/` or `data/users/`.
- A rerun with the same run id produces the same end state.
- A document is written by producers or by people, never both.
- On a version mismatch, re-read, re-plan and retry once; a second mismatch fails the run with the path named.
- A document over 256 KiB fails the plan with the entity id named, and nothing is written for it.

### Batches

The planner keeps each batch to at most 50 writes and about 900 KiB, under the ArtifactData limit of 50 writes and 1 MiB per batch (from the tool schema, tagged docs: acceptance not yet measured against a live artifact). Each document appears at most once per batch, and existing documents carry `if_version`.

With `--artifactdata`, `write` also emits `batch-NNN.artifactdata.json` per batch: the `writes` argument of one ArtifactData `batch` call, each entry `{op, collection, doc_id, file_path, if_version}`, with each document's data in a file under `batch-NNN-data/`.

### Status

A run is `ok` when it parsed everything it claimed. If `parsed` is less than `claimed`, the status becomes `partial`. The prompt sets `partial` and explains in `dropped` when a source failed to load.

## Worked example

Hiring Signals' run input from the [Quickstart](/introduction/quickstart) has three records and one Inbox message. `write` turns it into:

```
batch-001.json  started   runs/2026-11-02-2200
batch-002.json  entities  entities/company--northwind-example
                          entities/role--https-jobs-example-au-1001
                          entities/role--https-jobs-example-au-1002
batch-003.json  summary   runs/2026-11-02-2200
batch-004.json  inbox     inbox/2026-11-02-2200--1
batch-005.json  done      runs/2026-11-02-2200
```

The Inbox message gains `run`, `producer`, `createdAt`, `schema` and resolved `refs` with entity ids:

```json
{
  "kind": "match",
  "title": "Northwind Goods is hiring a Senior Shopify Engineer",
  "why": "Senior commerce role in AU, posted yesterday.",
  "refs": [{"id": "role--https-jobs-example-au-1001", "key": "https://jobs.example/au/1001", "type": "role"}],
  "run": "2026-11-02-2200",
  "producer": "hiring-signals",
  "createdAt": "2026-11-02T22:00:04Z",
  "schema": 1
}
```

If the prompt were tricked into adding a write to `settings/hiring-signals`, the plan would fail with `refusing to write settings/hiring-signals: people own this path`.

## Reference

### Limits

| Limit | Value | Source |
|---|---|---|
| Entity slug | 120 characters; longer keys keep 111 plus a dash and 8 hex of SHA-1 | `desk_contract.py` `MAX_SLUG` |
| Sightings kept per record | 30 | `MAX_SIGHTINGS` |
| Document size | 256 KiB | `MAX_DOC_BYTES` |
| Writes per batch | 50 | `MAX_BATCH_OPS` |
| Bytes per batch (planner) | 900 KiB | `MAX_BATCH_BYTES` |
| Dropped ids listed on a run | 50 | `MAX_DROPPED_LISTED` |
| Changed ids listed on a run | 20 | `MAX_CHANGED_LISTED` |
| Documents per artifact | 25,000 | Registry, tagged pilot |

### Run document fields

runId, producer, startedAt, endedAt, definitionVersion, settingsUsed, mergePolicy, schema, phase, status, parsed, claimed, sessionUrl, parentRun, counts `{new, updated, unchanged, dropped}`, droppedByReason, dropped `[{id, reason}]`, changed, inboxWritten, inboxSuppressed, inboxFiltered.

`inboxSuppressed` counts messages over the cap. `inboxFiltered` counts match messages whose linked records were all dropped.

The full layout of collections is in [Store collections](/reference/store-collections). Every field of the run input is in the [Write contract reference](/reference/write-contract).

## Common pitfalls

- **Writing documents directly from the prompt.** Always go through `write`. A hand-written `set` skips ids, versions, sightings and the people guard.
- **A stale snapshot.** Take the snapshot at the start of the write step. On a version mismatch, re-read and re-plan once.
- **`update` on a missing document.** In the artifact database `update` needs the document to exist; the planner uses `set`.
- **Two keys that slug the same.** `a.b` and `a-b` under `field:` identity collide; the planner fails and names both rather than guessing.
- **Huge records.** Keep summaries short (the jobs-watch prompt caps them at 400 characters). A record over 256 KiB is refused.

## Related

- [People-owned and producer-owned data](/concepts/ownership)
- [Records](/concepts/records)
- [Storage](/infrastructure/storage)
- [Write contract reference](/reference/write-contract)
