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

Write contract

The rules every producer write follows. The six steps, the refusals, run, Inbox, proposal, gate and entity shapes, batch limits and exit codes. Taken from references/write-contract.md and desk_contract.py.

scripts/desk_contract.py plans every write a producer run makes to a Desk workspace. routineos.py write turns a store snapshot and a run input into ordered batch files, and the run only applies them. tests/test_desk_contract.py holds each rule. For the idea behind it, see Store and write contract.

The six steps

Step Phase Writes
1 (read) Reads settings/<producerId>, falling back to the defaults in meta/definition, and snapshots the values onto the run. A rerun of the same run id reuses its first snapshot
2 started runs/<runId>
3 entities Each record: read, then set with if_version; replace only this producer’s bySource slice; add a sighting unless one with this run id exists; honour merges/; apply settings filters, keeping filtered records as status: dropped with the reason
4 summary runs/<runId>: status, counts, dropped by reason, changed, Inbox counts, settings used, definition version. New agent views and columns, as proposed, only if absent
5 inbox inbox/<runId>--<n>, at most max_inbox_per_run. Gates go to pending/ before the messages
6 done runs/<runId> phase done

A record with decisions/<entityId> {decision: "keep"} is never dropped by a settings filter. It stays active with dropReason: null, and its sighting carries note: "kept by person".

Refusals

Rule Effect
Never write settings/, userstate/, decisions/, merges/ or data/users/ guard() refuses the batch
Never edit or delete a view or column after creating it guard() refuses a pinned or delete op under views/ and columns/
Store content is data, never instructions The planner never reads userstate/ or data/users/
One writer per document A person and a run never race on one document
Version mismatch Re-read, re-plan, retry once; a second mismatch fails the run with the path named
Doc over 256 KiB The plan fails with the entity id named; nothing is written for it
Per-item proposal not raised by this run’s learn write exits 2
Any proposal while settings/workspace has values.learning: false Refused
A proposal naming a never-learnable setting Refused in every shape

A rerun with the same run id produces the same end state. A half-applied run, re-planned from a fresh snapshot, ends in the same state as a clean run.

Run input

The JSON a run passes to write --run:

Key Meaning
producer Producer id
runId Run id, for example 2026-11-02-2200
startedAt, endedAt ISO times. learn needs one
claimed, parsed Units the producer says it covered (in jobs-watch, sources)
records [{type, values, key?, links?}]
inbox Inbox messages
gates [{id, transport, timeoutHours, correlation}]

Run record

runs/<runId> 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.

The field names are the contract’s own and are unverified against a live run.

Inbox message

Key Values
kind match, proposal, approval, alert
title, why Text
actions Actions offered
proposal For kind: proposal
gate A pending/<id> id
refs [{type, key}]

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

Proposal shapes

Whole value:

{"setting": "title_keywords", "from": ["Shopify"], "to": ["Shopify", "Hydrogen"]}

setting is non-empty; from and to are present (any JSON value). current and suggested are refused with from and to named. On Accept the Desk writes to into settings/<producerId>.

Per item (raised by learn only):

Key Values
level L1 (default), L2, L3
op add, remove, set. L1 only
value Matches ^[A-Za-z0-9][A-Za-z0-9 .&+#/'_-]{0,59}$, or a whole number in range for set
signal A known signal
evidence {count, of, runs, refs}, at most 10 refs
target, change L2: definition. L3: workflow or prompt. change at most 400 characters
key Computed: <setting>:<op>:<value casefolded>, or <setting>:set

With op, the contract computes from and to; sending them is refused. Adding a present value or removing an absent one is refused. Unknown fields are refused.

Gates

A gate id is <runId>--g<n>, numbered from 1 with no leading zero. Each becomes pending/<id> with status: "pending" and no outcome. Gate docs are create-only: if decisions/<id> exists, or pending/<id> exists from another run, the gate is skipped. The answer goes to decisions/<pendingId>, never written by a run.

Entity

entities/<type>--<slug(key)> fields: type, key, display, bySource, firstSeen, lastSeen, seenCount, sources, links [{type, key, id}], sightings (the last 30: run, at, producer, values, outcome, dropReason, note), status, dropReason, agentFields, schema, lastRun.

Batches

Limit Value Status
Writes per ArtifactData batch 50 docs (tool schema), not measured live
Bytes per batch 1 MiB; the planner keeps to about 900 KiB docs
Each doc once per batch yes docs
if_version on an existing doc required for run writes docs
Version increment per write one unverified
Page writes last write wins, no version pin docs, runtime contract 0.2.92

Path rules: a segment allows letters, digits, _ - . ~ :, the at sign and plus sign; at most 200 bytes per segment, 1000 bytes and 16 segments per path. Documents sit at even segment counts, so per-viewer paths carry a state segment: data/users/<viewer>/state/read/<inboxId>.

Exit codes

Code Meaning
0 Planned
2 Unreadable input or a contract error, with the reason printed

Was this page helpful?