> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tai42.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# States

> A declared JSON document, one per subject, that every door reads and writes through one platform store.

A **state** is a declared JSON document with one instance per **subject**. The
platform owns the store: a conversation turn, a hook fire, a scheduled job, a
builtin tool, and the HTTP API all read and write the same subject's document
through one seam, and every write lands in one audit trail. Domain behaviour —
which flow reads which state, how a record is shaped for a use case — lives in the
consumer that binds the state, never in the store.

The store is a first-class platform component. It ships with the platform but
serves nothing until its database is bound (see
[Enabling the store](#enabling-the-store)); until then every read and write refuses
loudly with a `501` rather than returning an empty document.

## The subject

A subject is four fields:

```json theme={null}
{
  "target_kind": "agent",
  "target_name": "A-42",
  "kind": "person",
  "key": "alice"
}
```

* `target_kind` and `target_name` are the **conversation-target scope** — the only
  tenancy the platform has. A subject never crosses this pair.
* `kind` names the subject family the state serves. Kind `person` is the one
  platform-validated family: its `key` is a person id resolved against the
  conversations identity store, and the resolved person's target must equal the
  subject's target. Every other kind is declared by the state and its key is
  opaque to the platform.
* `key` addresses one subject within its kind. It is trimmed and must be 1 to 512
  characters.

The store refuses a subject loudly (`422`) when its rule fires: an **undeclared
kind**, an **empty key**, an **unknown person**, or a **person whose target does
not match the subject's**. A subject `kind` is a lowercase identifier of at most
63 characters (`^[a-z][a-z0-9_-]{0,62}$`).

## Declarations and kinds

A state is declared before it is written. A declaration names the state, its base
JSON Schema, the subject kinds it serves, and the default kind a door's ambient
subject resolves to:

```json theme={null}
{
  "name": "status",
  "description": "Per-person status document.",
  "schema": { "type": "object", "properties": { "note": { "type": "string" } } },
  "subject_kinds": ["person"],
  "default_subject_kind": "person",
  "retention_days": 90
}
```

* `subject_kinds` is a non-empty, unique list; each entry matches the subject-kind
  charset above.
* `default_subject_kind` must be one of `subject_kinds`. It is the kind a door's
  **ambient** subject resolves to when no explicit kind is named (see
  [Doors](#doors)).
* `retention_days` is optional: a positive integer, or unset to keep records
  forever.
* A state `name` matches `^[a-z0-9][a-z0-9_-]*$` — no colon, because the write
  op-id namespace splits on `:`.

Two schema fields are **computed by the platform** and served on every read:
`effective_schema` (the base schema composed with every attached template's fragment)
and `regimes` (the absolute write-regime rules composed over the attachments). A client
that supplies a non-null value for either on a write is refused — neither can be
forged across the wire.

### Re-declaring

Once records exist, a re-declare accepts only **additive** schema changes: a
re-declare that would **remove or change** an existing field while records exist
is refused (`409`), and removing a subject kind still present in records is
refused the same way. `retention_days` is metadata, not schema, so changing it
alone is never gated.

## Templates and attachments

A **state template** is a reusable schema fragment with fillable parameters,
per-path writer regimes, attach-time declarations, a set of **template jq** programs,
an optional **reconcile** program, and a trace switch. It is a document the platform
owns:

```json theme={null}
{
  "kind": "state-template",
  "name": "utilities",
  "description": "A reusable fragment.",
  "parameters": {},
  "schema": { "type": "object", "properties": { "items": { "type": "array" } } },
  "regimes": [{ "path": ["items"], "regime": "composing" }],
  "template_jq": {},
  "reconcile": null,
  "trace": { "enabled": true }
}
```

A state template is data and the store is generic: nothing in a template is a use-case
feature — it is a mechanism any state can reuse. A `name` is a **slug** —
`^[a-z](?:[a-z0-9]|-(?=[a-z0-9])){0,62}$`, so no leading, trailing, or consecutive hyphens —
because the jq prelude maps `-`→`_` to build the `tjq_<template>__<name>` handle, and only a
slug makes that encoding unambiguous. A document that carries keys outside its own schema is
refused, so an operator who pastes a whole consumer document is told exactly where those
belong.

### Template jq

A template's `template_jq` is a map of named jq programs a door or a flow node runs against
a subject's record, each declaring a **`purpose`**:

* **`input`** — `{description, purpose: "input", params, jq}`. `jq` runs over the record
  subtree at the attach path (`.` is the attached data) with the effective `$parameters`, the
  `$declarations`, and its declared `params` delivered as the single object `$params`, and
  returns a value. Read-only.
* **`update`** — `{description, purpose: "update", params, reads, writes, jq}`. `jq`'s input is
  `{record, input}` — `.record` the current data at the attach path, `.input` the caller's
  argument — with `$parameters`/`$declarations` bound, and returns an ordered,
  template-relative op batch (the keyed-op shape the store applies). `params` are the key
  names the `.input` object must carry — uniform with an `input` program's `params` — so apply
  refuses an `.input` missing a declared key or carrying an undeclared one (`422`); a named
  update that declares `params` needs an
  [adapter](/babelfish#binding-a-node-to-a-state) in a binding to fill them. `reads`/`writes`
  are template-relative paths declared for readability; at put each `reads`/`writes` path is
  validated only for **structure** — that it resolves against the fragment — while the
  **write regime** (that a `writes` path lies under a declared-writable regime) is enforced at
  apply by the store guard.

Sibling `input` programs are ordered dependency-first, so one may call another by name. A
[binding](/babelfish#state-templates) on any door (or a babelfish node) injects an `input`
program's value into a run's input before the run and applies an `update` program's batch
after it; the record-level HTTP and CLI surfaces evaluate an `input` program and apply an
`update` program directly (see [Template jq](#template-jq-1)).

### Attaching

**Attaching** a template places its fragment at a path in a state's document. The template is
named in the URL segment (`PUT …/attachments/{template}`), never in the body; the body carries
only `path`/`parameters`/`declarations`/`options` (the last a per-operation, never-stored bag):

```json theme={null}
{ "path": ["extras"], "parameters": {}, "declarations": {}, "options": {} }
```

The effective schema is the base schema with every attached fragment composed in. Composing
two fragments at overlapping paths, attaching a second template at an already-occupied path,
or attaching where the base schema already carries the property is a conflict (`409`) naming
the colliding path. A consumer may register an **attach validator** on the states facet with
`register_attach_validator`: given the template document, the attachment's declaration values,
and the state's effective schema, it raises loudly to refuse an attach or a declarations
write — it is consulted before every attach and every declarations write.

### Attach reconciliation

Replacing a live attachment's `declarations` can retire part of a template while the state
still holds OPEN records shaped by the old declarations. The platform runs a built-in
**reconciler** that reads the template's `reconcile` program, and a consumer may register its
own with `register_attach_reconciler(reconciler)` — a pre-write hook run inside every `attach`
and `update_attachment_declarations` write, after the validators and before the write. It
receives an `AttachReconcileContext`: the `state` name, the template document, the `operation`
(`attach` | `update_declarations`), the `previous_declarations` (`None` on a first attach) and
`new_declarations`, the attach `options`, and a record door bound to the state. The reconciler
either **raises** to refuse the attach (naming the records the new declarations would orphan)
or **writes resolutions** through the context's record door and returns, letting the attach
commit with those writes.

The `options` bag on the `PUT`/`PATCH` attach body (`AttachBody.options`, `--options` on the
CLI) is a free-form, **per-operation** directive bag the reconciler reads to decide how to
settle open records against the new declarations — for example, whether to close records the
new declarations no longer cover. It is passed to the reconcilers for THIS attach only,
**never stored or served back**; any key outside
`path`/`parameters`/`declarations`/`options` is refused.

### Detaching

Detaching a template is guarded so a live reference is never stranded. The detach door
consults every registered **detach referee** before it removes the attachment and refuses
(`409`, listing the human-readable references each referee returns) while any binding still
calls the template's [template jq](#template-jq). The platform's own binding holders — preset
versions, conversation-route configs, hooks, and schedules — are checked, and any consumer that
registers a referee is too: the flow engine registers one for its
[per-node bindings](/babelfish#binding-a-node-to-a-state), just as it does against a state
delete. Nothing is auto-detached — the author removes or rewrites the referencing bindings
first, then detaches. A consumer registers a referee with `register_detach_referee`, beside
`register_delete_referee` on the tools facet.

### Regimes

A regime is a per-path writer rule composed over a state's attachments. Each rule is a
`{path, regime}` where the path may use the `"*"` wildcard (one list index or key)
and the regime is one of:

* **`free`** — no restriction. An undeclared path is `free`.
* **`composing`** — the path is built up by many writers, so a whole-path
  `set`/`remove` over it is refused (`422`); it admits only a keyed op or an append
  `set`. The refusal names the path and template, for example: `composing path ['items'] of template 'utilities' admits only keyed ops or an append set`.
* **`single`** — a single-writer path. This is enforced as an authoring check when
  a consumer binds a state, not at the store: a hook, schedule, or builtin-tool
  write on a `single` path is recorded in the audit trail with its door and actor,
  never refused at write time.

When a template's trace switch is enabled, the effective schema admits a `_trace`
object on every write under the attachment, and the platform stamps it (see
[The audit trail](#the-audit-trail)).

## Doors

Every door that can write a subject's document deposits one ambient
`StateContext` — the entering door, the resolvable subject candidates, and the
accountable actor — so a downstream write resolves its subject and provenance
without a per-door argument. The context is one generic object, so a run parked at
an async `ask_user` and resumed later rejoins the same attribution.

| Door                    | What it supplies                                                                                                                                                                                                        | Explicit subject                                                         |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| **Conversation turn**   | door `conversation`; the route's target scope; candidates `{"thread": <thread id>}` always, plus `{"person": <person id>}` on a multichannel target; actor = the turn's attribution user id; the turn id and inbound id | a flow reads the `turn.subject` block; a tool takes a `subject` argument |
| **Park → continuation** | the original turn's `StateContext`, stored whole beside the continuation identity and re-deposited when the run resumes                                                                                                 | —                                                                        |
| **Hook fire**           | door `hook`; the declared target and `{kind: key}`; actor = the hook's execution key                                                                                                                                    | a hook's `tool_kwargs` may carry `subject`                               |
| **Schedule fire**       | door `schedule`; the job's `subject` under its target; actor `null` (the fire is anonymous)                                                                                                                             | the job's `subject`                                                      |
| **Builtin tools**       | resolved from the ambient context, or from an explicit `subject` argument                                                                                                                                               | `subject` argument                                                       |
| **HTTP API**            | door `api`/`operator`; actor = the request principal                                                                                                                                                                    | the subject is in the path                                               |

A tool run with no ambient context — a sync run-tool call, a background run, the
MCP edge — is the `api` door and a state tool it calls must carry an explicit
subject; called with none it raises `no subject in scope`.

### A binding's subject and scope

A [binding](/babelfish#binding-a-node-to-a-state) — on any door definition (a preset,
hook, schedule, or channel route) and, in babelfish, per node — resolves its subject with
a jq `subject_expr` and gates itself with an optional jq `scope_expr`. Both mean the same
on every door and every node:

* **`subject_expr`** is **required** and non-empty, and yields either a full subject object
  `{target_kind, target_name, kind, key}` or a bare **key** string — for a bare key the
  `kind` is the state's declared subject kind and the target is the run's ambient target.
  A missing or empty `subject_expr` is refused (`422`).
* **`scope_expr`** is an optional **boolean** predicate over the run's inputs, evaluated
  first: `false` skips the state for that run — no input injections and no updates; a
  non-boolean is refused loudly; an absent expression means engaged.

When a door dispatches a preset that carries its own binding, the **door's `scope_expr`
wins** when present, else the preset's. A preset's own binding fires only when that preset
is the **outermost** dispatch; called as a sub-tool of another dispatch, its binding is a
no-op.

A binding fires on every door it reaches through, the MCP edge included: an MCP `tools/call`
that omits `arguments` still receives the binding's input injections — absent arguments are
treated as an empty argument object, never a skipped injection.

### The turn subject block

Every tool turn's payload carries a generic `turn` block so a flow's
`subject_expr` can read the subject the platform resolved:

```json theme={null}
{
  "id": "<turn id>",
  "inbound": { "id": "<inbound id>", "kind": "message", "source": "<source>" },
  "subject": {
    "target_kind": "agent",
    "target_name": "A-42",
    "person": "alice",
    "thread": "<thread id>"
  }
}
```

`person` is `null` off a non-multichannel target (a person is resolved only for
multichannel targets); `thread` is always present.

### Hook and schedule subjects

A hook may declare an optional subject: the target scope, a subject `kind`, and a
`key_expr` — a jq expression evaluated over the event payload at fire, which must
yield a non-empty string, else the fire fails loudly like any hook error. A
schedule carries its `subject` in the job's tool kwargs; a malformed subject is
refused when the job is created (`invalid schedule subject: …`), so a job that
could never resolve its subject is never persisted. Both surface a **Subject**
group on their Studio forms; see [Hooks](/concepts/hooks) and the scheduling
reference.

## The builtin tools

Four LLM-facing tools read and write the calling subject's document. Each resolves
its subject from the ambient context, or from an explicit `subject` argument
(`{target_kind, target_name, kind, key}`, or `{kind, key}` with the target taken
from the context); with neither resolvable it refuses, naming the state, kind, and
door.

| Tool                                                | Effect                                            |
| --------------------------------------------------- | ------------------------------------------------- |
| `state_read(state, subject=None)`                   | read the subject's document (a miss returns null) |
| `state_replace(state, data, subject=None)`          | replace the whole document                        |
| `state_merge(state, patch, subject=None)`           | shallow top-level merge of a patch                |
| `state_apply(state, ops, subject=None, op_id=None)` | apply a batch of path operations                  |

The tools supply only the consumer half of a write's provenance (their tool name,
run, and — for `state_apply` — the idempotency `op_id`); the door, actor, and turn
id are stamped by the platform, never derived by the tool.

## The audit trail

Every write is recorded. A state's `writes` trail is a keyset page
`{items, next_cursor}` — the same paging idiom as `subjects` and record search — with
each `items` row carrying the write's `seq` and timestamp `at`, the completed origin
that produced it, and the absolute `paths` it touched. `next_cursor` feeds the next
call (`?cursor=`) and is null on the last page. The origin carries what the consumer
supplied — `consumer`, `meta` (an opaque object the writing consumer supplies — its own identifiers, for instance — stored and shown as is), `run_id`, `op_id` — and,
stamped by the platform write chokepoint from the ambient context, `door`, `actor`,
`turn_id`, and `inbound_id`. Because door, actor, and turn are stamped by the platform,
the trail cannot be forged by a consumer.

Under a **traced attachment** the platform stamps a `_trace` object on every write:
`at` is always present; `run`, `turn`, and `inbound` are the run, turn, and inbound
ids, or null when the writer has none (a hook, a schedule, the API, or a builtin
`state_*` tool has none); and `meta` is an opaque object the writing consumer supplies
— its own identifiers, for instance — stored and shown as is. A flow's binding write
and a hook's keyed write get the same stamp.

## The HTTP surface

All routes are authed. Templates are a top-level sibling collection and prune is its
own route, so no literal segment sits on the `/api/states/{name}` position — a
state literally named `templates` stays reachable at `GET /api/states/templates` while
`GET /api/state-templates` lists templates.

### States

| Method & path                                      | Purpose                                             |
| -------------------------------------------------- | --------------------------------------------------- |
| `GET /api/states`                                  | list declarations                                   |
| `GET /api/states/{name}`                           | declaration, effective schema, attachments, regimes |
| `PUT /api/states/{name}`                           | declare (`?replace=`)                               |
| `DELETE /api/states/{name}`                        | delete a declaration                                |
| `GET /api/states/{name}/stats`                     | record and subject counts                           |
| `GET /api/states/{name}/attachments`               | list attachments                                    |
| `GET /api/states/{name}/attachments/{template}`    | read an attachment (404 if not attached)            |
| `PUT /api/states/{name}/attachments/{template}`    | attach a template                                   |
| `PATCH /api/states/{name}/attachments/{template}`  | update an attachment's declarations                 |
| `DELETE /api/states/{name}/attachments/{template}` | detach                                              |
| `GET /api/states/{name}/subjects`                  | list subjects (`?kind=&limit=&cursor=`)             |
| `POST /api/states/{name}/records/search`           | search records by document content                  |
| `GET /api/states/{name}/consumers`                 | what binds this state                               |

### Records

The subject is four path segments:
`/api/states/{name}/records/{target_kind}/{target_name}/{kind}/{key}`.

| Method & path   | Purpose                          |
| --------------- | -------------------------------- |
| `GET …`         | read the record                  |
| `PUT …`         | replace the document             |
| `PATCH …`       | shallow-merge a patch            |
| `POST …/deltas` | apply a batch of path operations |
| `DELETE …`      | erase the record                 |
| `POST …/fold`   | fold this subject into another   |
| `GET …/writes`  | the audit trail                  |

### Template jq

The record-level jq programs a state's attached templates declare share one path, the
method selecting the purpose:

| Method & path                  | Purpose                                                                |
| ------------------------------ | ---------------------------------------------------------------------- |
| `GET …/template-jq/{program}`  | evaluate an `input`-purpose program (query params supply its `params`) |
| `POST …/template-jq/{program}` | apply an `update`-purpose program (body `{input?, op_id?}`)            |

`{program}` is a program name, or `{template}.{name}` when a name is shared across the
state's attachments. A GET on an `update` program, or a POST on an `input` program,
refuses loudly.

### Templates and retention

| Method & path                        | Purpose                         |
| ------------------------------------ | ------------------------------- |
| `GET /api/state-templates`           | list templates                  |
| `GET /api/state-templates/{name}`    | read a template                 |
| `PUT /api/state-templates/{name}`    | upload a template (`?replace=`) |
| `DELETE /api/state-templates/{name}` | delete a template               |
| `POST /api/state-retention/prune`    | prune expired records           |

Errors map to status: `501` when the store's database is unbound (a stable code
`states-not-configured`), `404` not found, `409` exists / in use / conflict, and
`422` validation (schema, subject, regime, path, value).

## The command line

Two command groups mirror the HTTP surface. `tai states` manages declarations,
attachments, subjects, records, and the audit trail — `list`, `get`, `put`, `delete`,
`stats`, `attachments`, `attach`, `get-attachment`, `update-attachment` (its `--options`
carrying the per-operation reconcile directives),
`detach`, `subjects`, `search`, `read`, `replace`, `merge`, `apply`, `template-jq`
(`eval` an input program, `apply` an update program), `erase`,
`fold`, `writes`, `consumers`, and `prune`. `tai state-templates` manages template
documents — `list`, `get`, `put`, `delete`. Record commands take
`--target-kind`, `--target-name`, `--kind`, and `--key`; `apply`, `replace`, and
`merge` read their JSON from `--data` or a `--file`. See the
[CLI reference](/reference/cli).

## Enabling the store

The store is the `states` DB registry component. Like the `skeleton` component it
**defaults to the `default` database** when its binding is unset, so an ordinary
deployment serves the store out of the box. A deployment that wants records in a
separate database points the binding elsewhere with `TAI_DB_BINDING_STATES`, then
applies the component's migration chain with `tai db migrate`. Until the bound
database is configured, every read and write returns `501 states-not-configured`.

Record retention is opt-in: nothing is deleted until a retention is configured,
either per state (`retention_days` on the declaration) or as a deployment default
(`STATES_DEFAULT_RETENTION_DAYS`). `tai states prune` runs the sweep.
