Skip to content
RoutineOS
Esc
↑↓navigate↵open⌘Jpreview
On this page

The store and the write contract

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.

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 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:

{
  "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. Every field of the run input is in the Write contract reference.

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.

Was this page helpful?