skip to content
$worker

state

v0.21.4-alpha.3

Distributed key-value state management with reactive change triggers — registers the `state` trigger type and the `state::*` functions.

iiiverified
4,789 installs1,208 in 7d8 today
install
$iii worker add state@0.21.4-alpha.3
  • macOS: arm64 · x64
  • Linux: arm64 · armv7 · x64
  • Windows: arm64 · x64 · x86

exact versions are immutable; binary and bundle artifacts are digest-pinned.

agent-ready brief for v0.21.4-alpha.3
install + config + dependencies + readme + api reference, all in one place. fetch as agent-context.md for an llm to consume.
the same content rendered as discrete blocks below is exposed as a single markdown document at /workers/state.md?version=0.21.4-alpha.3. paste it into an llm prompt or pipe it through curl from a worker.

install

install
$iii worker add state@0.21.4-alpha.3

configuration

iii-config.yaml
- adapter:
    config:
      store_method: in_memory
    name: kv

dependencies

dependencies1

readme

README.md

state

Distributed key-value state storage with scope-based organization and reactive change triggers. Values are addressed by a scope (namespace) and a key, shared across every worker connected to the engine, and persisted through a pluggable adapter (kv or redis). Callers reach the store through six functions — state::set, state::get, state::delete, state::update, state::list, state::list_groups — and this worker also registers the state trigger type, which fires state:created, state:updated, or state:deleted after every successful mutation so downstream functions can react to data changes without polling. This worker is the standalone migration of the engine's built-in iii-state.

Install

iii worker add state

iii worker add fetches the binary, writes a config block into ~/.iii/config.yaml, and the engine starts the worker on the next iii start.

Functions

Function Input Returns Fires
state::set { scope, key, value } (data accepted as an alias for value) { old_value, new_value } state:created (new key) or state:updated
state::get { scope, key } the value, or null
state::delete { scope, key } the deleted value, or null state:deleted (even when the key did not exist)
state::update { scope, key, ops } — ordered atomic ops: set, merge, increment, decrement, append, remove { old_value, new_value, errors } state:created or state:updated
state::list { scope } flat array of every value in the scope
state::list_keys { scope } { keys } — the keys stored in the scope, adapter order (additive; no builtin counterpart — added for the console state UI, whose per-item navigation state::list's values-only shape cannot drive)
state::list_groups {} { groups } — sorted, deduplicated scope names
state::ui-content { path } { content, content_type } — content function for the injected console UI (internal; see Console UI)

The harness_binding and harness_binding_owner scopes are reserved control-plane state: public functions reject direct access, omit them from group listings, and never emit their bookkeeping writes as state events.

Console UI

The worker ships its console UI into any running console (the injectable-UI protocol, iii/tech-specs/2026-07-17-injectable-ui) — three contributions, one module each under ui/src/:

Module Slot What it does
ui/src/page/ host.pages the state-manager page (#/ext/state-manager): browse scopes → keys → edit one value as JSON and save it back with state::set
ui/src/configuration/ host.configForms custom form for the state configuration entry in the console's workers tab (adapter picker, persistence, trigger gate, limits) — replaces the generic schema form
ui/src/function-trigger-message/ host.functionTriggers how state::set / state::get triggers render in chat and the traces span view (overrides the console's built-in family for those two ids; errors and other state::* ids fall through)

ui/page.tsx is the esbuild entry: a thin setup(host) composing the three registrations plus the shared scoped stylesheet (ui/src/lib/styles.ts).

The page is live: it registers one tab-scoped state trigger binding (empty config = every scope/key) whose function_id is a per-tab handler (iii::state-ui::events::), and applies the created/updated/deleted events in place — new scopes and keys appear as they are written, an open item editor updates in place when clean, and unsaved edits are never clobbered (a "changed on the server" notice offers the incoming value instead). The binding is GC'd with the tab; the iii:: prefix keeps the per-event invocations out of the trace feed.

  • Build: esbuild over ui/page.tsx and ui/styles.css (cd ui && pnpm build; build.rs does this automatically) with react and @iii-dev/console-ui external — they resolve through the console's import map at runtime. Types come from the workspace-linked packages/console-ui (repo-root pnpm workspace).
  • Deployment: boot registers a console:script trigger for state/page.js and a console:style trigger for state/styles.css, both with function_id state::ui-content; the console fetches, hashes, and serves the assets, and disposes them when this worker disconnects. The stylesheet is scoped under [data-iii-ui="state"] and mounted as a (styles-before-scripts on boot, link-swap on change).
  • Dev loop: run cd ui && pnpm watch and start the worker with III_STATE_UI_WATCH=1 — it polls ui/dist/ and re-registers a changed asset's trigger, hot-swapping it in every open console tab.

Trigger type

This worker always registers the state trigger type. Bind a function to it with:

Field Required Default Description
scope no any scope Only fire for writes in this scope.
key no any key Only fire for writes to this key.
condition_function_id no Function invoked first with the event; only an explicit false return skips the handler (null/no result passes, an error skips and logs).

Trigger delivery is asynchronous: handlers run after the write completes and a handler failure never rolls the write back. Duplicate trigger ids replace the previous binding silently (builtin parity).

iii.registerTrigger({
  type: 'state',
  function_id: 'orders::on-status-change',
  config: { scope: 'orders', key: 'status' },
})

The handler receives the event payload:

{
  "type": "state",
  "event_type": "state:updated",
  "scope": "orders",
  "key": "status",
  "old_value": { "status": "pending" },
  "new_value": { "status": "shipped" }
}

event_type is one of state:created, state:updated, state:deleted; old_value is null for created keys and new_value is null for deleted keys.

Configuration

Field Default Description
adapter kv Storage adapter: kv (in-process; store_method: in_memory or file_based with file_path and save_interval_ms) or redis (redis_url, default redis://localhost:6379). Restart-tier: a runtime change is logged and takes effect at the next worker start.
triggers_enabled true Globally enable/disable state change-trigger fan-out. Applied live.
max_value_bytes unset (no limit) Reject state::set writes whose JSON-serialized value exceeds this many bytes (VALUE_TOO_LARGE). Minimum 1. Applied live.
save_interval_ms 5000 Persistence flush cadence (ms) for the file-backed kv adapter. 1003600000. Applied live by respawning the adapter's save loop (hot-retune; no-op for in-memory/redis).

Configuration is owned by the configuration worker — edit it from the console (Configuration → Workers → state) or seed it once via the worker's config block in config.yaml on first boot. triggers_enabled and max_value_bytes apply on the next write, save_interval_ms hot-retunes the save loop, and adapter takes effect on the next restart.

Requires removing the built-in iii-state worker

The built-in iii-state worker also owns the state trigger type and the state::* functions. Two owners of the same surface on one engine collide — whichever registers last wins — so this worker requires iii-state to be absent: omit it from the engine's config.yaml (a config that doesn't list a worker won't run it).

On boot, this worker queries the engine for connected workers and refuses to start with a clear error if iii-state is still active, so a stale config fails loudly instead of silently racing the built-in worker for ownership of state.

Store migration: if the builtin used a file-based kv store, point this worker's adapter.config.file_path at the builtin's existing directory (the default engine config used ./data/state_store.db). The on-disk format (one rkyv .bin file per scope) is identical, so the existing data loads as-is — no export/import step.

Latency

The builtin ran in-process inside the engine, so a state::* call cost microseconds; as a standalone worker every call crosses the engine⇄worker WebSocket, which puts round-trips in the low-millisecond range. That order-of-magnitude delta is inherent to the migration and applies to every externalized builtin. A formal benchmark was waived by project decision (2026-07-06).

Parity vs builtin

Behavior Builtin This worker
Function ids state::set/get/delete/update/list/list_groups same (exact)
set input {scope, key, value} (data alias) same
Events state:created/updated/deleted, payload {type:"state", event_type, scope, key, old_value, new_value} same
Trigger config {scope?, key?, condition_function_id?}; only explicit false blocks same
Duplicate trigger id silent replace same
Trigger metadata forwarded to handlers via call_with_metadata not forwarded (iii-sdk 0.20 TriggerRequest has no metadata field; same limitation as the http worker)
Store adapters kv (in_memory/file_based), redis, bridge kv, redis (bridge not ported — see docs/adr/0001)
kv on-disk format rkyv .bin per scope identical — builtin data loads as-is
save_interval_ms default 5000ms, floor 100ms, hot-retune same
max_value_bytes guards set only, VALUE_TOO_LARGE same (code is the message prefix)
Error codes coded ErrorBody (SET_ERROR, ...) code as message prefix (SDK handler errors carry a message)
Durability store dies with the ENGINE process store dies with the WORKER process (ADR 0001; file_based/redis unchanged)
Latency in-process µs engine⇄worker WS round-trip (low ms) — formal benchmark waived by project decision 2026-07-06
Telemetry (track_state_*) engine-internal counters none — out of scope for this migration; lands with the shared worker observability story

api reference (json)

agent-api-reference.json
{
  "functions": [
    {
      "description": "Fan-in gate: record one arrival against a barrier and answer the typed condition decision — `skip` until the expected set has arrived, then `allow` EXACTLY ONCE carrying every arrival's payload. Use as a binding condition so a coordinator wakes once when N producers finish instead of once per producer.",
      "metadata": {},
      "name": "state::barrier",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "BarrierConfig": {
            "description": "The caller-authored half: what this barrier is and how to read an arrival out of the event.",
            "properties": {
              "carry": {
                "default": null,
                "description": "JSON pointer into the event for the value to accumulate (default: the whole event).",
                "type": [
                  "string",
                  "null"
                ]
              },
              "expect": {
                "allOf": [
                  {
                    "$ref": "#/definitions/Expect"
                  }
                ],
                "description": "A count, or the exact set of arrival keys."
              },
              "id": {
                "description": "Barrier identity. Two bindings sharing an id share the barrier — that is how several predecessors feed one downstream.",
                "type": "string"
              },
              "key_from": {
                "default": null,
                "description": "JSON pointer into the event for the arrival key (default `/key`, the state event's own key). A missing pointer is an error, not a silent unnamed arrival: two arrivals that both resolve to nothing would collapse into one and the barrier would never complete.",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "expect",
              "id"
            ],
            "type": "object"
          },
          "Expect": {
            "anyOf": [
              {
                "format": "uint64",
                "maximum": 10000,
                "minimum": 1,
                "type": "integer"
              },
              {
                "items": {
                  "type": "string"
                },
                "maxItems": 10000,
                "minItems": 1,
                "type": "array"
              }
            ],
            "description": "What the barrier is waiting for: a COUNT, or a named set. A named set is stricter and better: it catches the arrival that was never going to come."
          }
        },
        "description": "The condition-contract envelope. `event` is the fired event; the rest of the envelope (binding, context) is accepted and ignored so the same function works as a condition and as a direct call.",
        "properties": {
          "condition_config": {
            "anyOf": [
              {
                "$ref": "#/definitions/BarrierConfig"
              },
              {
                "type": "null"
              }
            ]
          },
          "event": {
            "default": null
          }
        },
        "title": "BarrierInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "The typed decision a condition returns.",
        "oneOf": [
          {
            "properties": {
              "decision": {
                "enum": [
                  "allow"
                ],
                "type": "string"
              },
              "payload": true,
              "reason": {
                "type": "string"
              }
            },
            "required": [
              "decision",
              "reason"
            ],
            "type": "object"
          },
          {
            "properties": {
              "decision": {
                "enum": [
                  "skip"
                ],
                "type": "string"
              },
              "reason": {
                "type": "string"
              }
            },
            "required": [
              "decision",
              "reason"
            ],
            "type": "object"
          }
        ],
        "title": "Decision"
      }
    },
    {
      "description": "Reserve a PRIVATE state namespace for the calling worker: its scopes become invisible to public state::* and to trigger fan-out, and internal accessors `<functions_prefix>::state::{get, list, compare-and-set}` are registered. A worker may only claim the namespace matching its own worker name (engine-stamped caller identity). Idempotent; claims survive a state restart.",
      "metadata": {},
      "name": "state::claim-namespace",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Input for [`CLAIM_NAMESPACE_ID`]. `_caller_worker_id` is engine-stamped (overwritten on every dispatch, so a caller cannot forge it); the handler resolves it to the caller's worker NAME and requires it to equal `functions_prefix`.",
        "properties": {
          "_caller_worker_id": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          },
          "functions_prefix": {
            "description": "Must equal the calling worker's name.",
            "type": "string"
          },
          "scopes": {
            "description": "Storage scopes to reserve for this namespace.",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "functions_prefix",
          "scopes"
        ],
        "title": "ClaimNamespaceInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "claimed": {
            "description": "False when the namespace already owned everything asked for.",
            "type": "boolean"
          },
          "functions": {
            "description": "The accessor ids now serving this namespace.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "scopes": {
            "description": "Every scope this namespace owns after the call.",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "claimed",
          "functions",
          "scopes"
        ],
        "title": "ClaimNamespaceResult",
        "type": "object"
      }
    },
    {
      "description": "Atomically set a value only if it currently equals `expected` (omit `expected` to mean 'only if absent'). Returns { swapped, current } — on a miss, `current` is what is actually there, so a caller can recompute and retry. The primitive for counters, claims and any read-modify-write that two callers might race.",
      "metadata": {},
      "name": "state::compare-and-set",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Everything a function handler needs; one Arc cloned per registration.",
        "properties": {
          "expected": {
            "default": null,
            "description": "The value the caller believes is there. Omit to mean \"expect absent\" — the set-if-absent form."
          },
          "key": {
            "type": "string"
          },
          "scope": {
            "type": "string"
          },
          "value": true
        },
        "required": [
          "key",
          "scope",
          "value"
        ],
        "title": "CompareAndSetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "current": {
            "description": "What is actually stored, when the swap did not happen."
          },
          "swapped": {
            "type": "boolean"
          }
        },
        "required": [
          "swapped"
        ],
        "title": "CompareAndSetResult",
        "type": "object"
      }
    },
    {
      "description": "Delete a value from state",
      "metadata": {},
      "name": "state::delete",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "key": {
            "description": "Identifier for the value to delete within the scope.",
            "type": "string"
          },
          "scope": {
            "description": "Namespace that groups related keys.",
            "type": "string"
          }
        },
        "required": [
          "key",
          "scope"
        ],
        "title": "StateDeleteInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "The value that was deleted (read before delete), or null if it did not exist.",
        "title": "StateDeleteResponse",
        "type": [
          "string",
          "number",
          "boolean",
          "object",
          "array",
          "null"
        ]
      }
    },
    {
      "description": "Get a value from state",
      "metadata": {},
      "name": "state::get",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "key": {
            "description": "Identifier for the value within the scope.",
            "type": "string"
          },
          "scope": {
            "description": "Namespace that groups related keys.",
            "type": "string"
          }
        },
        "required": [
          "key",
          "scope"
        ],
        "title": "StateGetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "The raw value stored at scope/key, or null if absent.",
        "title": "StateGetResponse",
        "type": [
          "string",
          "number",
          "boolean",
          "object",
          "array",
          "null"
        ]
      }
    },
    {
      "description": "Get a group from state",
      "metadata": {},
      "name": "state::list",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "scope": {
            "description": "Namespace whose keys should be listed as a group.",
            "type": "string"
          }
        },
        "required": [
          "scope"
        ],
        "title": "StateGetGroupInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "The values in the given scope, as a JSON array (or null on the rare serialization failure).",
        "title": "StateListResponse",
        "type": [
          "string",
          "number",
          "boolean",
          "object",
          "array",
          "null"
        ]
      }
    },
    {
      "description": "List all state groups",
      "metadata": {},
      "name": "state::list_groups",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "StateListGroupsInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "groups": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "groups"
        ],
        "title": "StateListGroupsResult",
        "type": "object"
      }
    },
    {
      "description": "List the keys stored in a scope",
      "metadata": {},
      "name": "state::list_keys",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "scope": {
            "description": "Namespace whose keys should be listed as a group.",
            "type": "string"
          }
        },
        "required": [
          "scope"
        ],
        "title": "StateGetGroupInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "keys": {
            "description": "Keys stored in the scope, in the adapter's natural order.",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "keys"
        ],
        "title": "StateListKeysResult",
        "type": "object"
      }
    },
    {
      "description": "Internal: reload state configuration from the authoritative store on change.",
      "metadata": {},
      "name": "state::on-config-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "ConfigChangeRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "ConfigChangeAck",
        "type": "object"
      }
    },
    {
      "description": "Set a value in state",
      "metadata": {},
      "name": "state::set",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "key": {
            "description": "Identifier for the value within the scope.",
            "type": "string"
          },
          "scope": {
            "description": "Namespace that groups related keys (e.g. `users`, `orders`).",
            "type": "string"
          },
          "value": {
            "description": "Arbitrary JSON value to store. Replaces any existing value at `scope`/`key`."
          }
        },
        "required": [
          "key",
          "scope",
          "value"
        ],
        "title": "StateSetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Result of a stream set operation.",
        "properties": {
          "new_value": {
            "description": "The value after the update"
          },
          "old_value": {
            "description": "Previous value (if it existed)."
          }
        },
        "required": [
          "new_value"
        ],
        "title": "StreamSetResult",
        "type": "object"
      }
    },
    {
      "description": "Serve the state worker's injected console UI assets (content function for its console:script / console:style triggers).",
      "metadata": {
        "internal": true
      },
      "name": "state::ui-content",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Input of the content function: the console asks for one asset by path.",
        "properties": {
          "path": {
            "description": "The asset path from the trigger config (e.g. `state/page.js`).",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "title": "UiContentInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Output of the content function.",
        "properties": {
          "content": {
            "description": "The asset source, verbatim.",
            "type": "string"
          },
          "content_type": {
            "description": "MIME type the console should serve the asset with.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "content_type"
        ],
        "title": "UiContentResult",
        "type": "object"
      }
    },
    {
      "description": "Update a value in state",
      "metadata": {},
      "name": "state::update",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "MergePath": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              }
            ],
            "description": "Path target for a [`UpdateOp::Merge`] operation. Accepts either a single string (legacy / first-level field) or an array of literal segments (nested path).\n\nPath normalization rules applied by the engine: - absent / `Single(\"\")` / `Segments(vec![])` → root merge - `Single(\"foo\")` is equivalent to `Segments(vec![\"foo\".into()])` - `Segments([\"a\", \"b\", \"c\"])` walks three literal keys, never interpreting dots specially. `Segments(vec![\"a.b\".into()])` is a single literal key named `\"a.b\"`.\n\n**Variant ordering is load-bearing.** `#[serde(untagged)]` tries variants in declaration order, `Single` MUST come before `Segments` so a JSON string deserializes into `Single` rather than failing the array match first."
          },
          "UpdateOp": {
            "description": "Operations that can be performed atomically on a stream value",
            "oneOf": [
              {
                "description": "Set a value at path (overwrite)",
                "properties": {
                  "path": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "set"
                    ],
                    "type": "string"
                  },
                  "value": true
                },
                "required": [
                  "path",
                  "type"
                ],
                "type": "object"
              },
              {
                "description": "Merge object into existing value (object-only). Path may be omitted (root merge), a single first-level key, or an array of literal segments for nested merge. See [`MergePath`].",
                "properties": {
                  "path": {
                    "anyOf": [
                      {
                        "$ref": "#/definitions/MergePath"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "type": {
                    "enum": [
                      "merge"
                    ],
                    "type": "string"
                  },
                  "value": true
                },
                "required": [
                  "type",
                  "value"
                ],
                "type": "object"
              },
              {
                "description": "Increment numeric value",
                "properties": {
                  "by": {
                    "format": "int64",
                    "type": "integer"
                  },
                  "path": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "increment"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "by",
                  "path",
                  "type"
                ],
                "type": "object"
              },
              {
                "description": "Decrement numeric value",
                "properties": {
                  "by": {
                    "format": "int64",
                    "type": "integer"
                  },
                  "path": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "decrement"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "by",
                  "path",
                  "type"
                ],
                "type": "object"
              },
              {
                "description": "Append an element to an array or concatenate a string at the optional path. Path may be omitted (root append), a single first-level key, or an array of literal segments for nested append. See [`MergePath`] for the variant shape.",
                "properties": {
                  "path": {
                    "anyOf": [
                      {
                        "$ref": "#/definitions/MergePath"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "type": {
                    "enum": [
                      "append"
                    ],
                    "type": "string"
                  },
                  "value": true
                },
                "required": [
                  "type",
                  "value"
                ],
                "type": "object"
              },
              {
                "description": "Remove a field",
                "properties": {
                  "path": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "remove"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "path",
                  "type"
                ],
                "type": "object"
              }
            ]
          }
        },
        "properties": {
          "key": {
            "description": "Identifier for the value to update within the scope.",
            "type": "string"
          },
          "ops": {
            "description": "Ordered list of update operations applied atomically to the existing value.",
            "items": {
              "$ref": "#/definitions/UpdateOp"
            },
            "type": "array"
          },
          "scope": {
            "description": "Namespace that groups related keys.",
            "type": "string"
          }
        },
        "required": [
          "key",
          "ops",
          "scope"
        ],
        "title": "StateUpdateInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "UpdateOpError": {
            "description": "Per-op error reported by an atomic update operation.",
            "properties": {
              "code": {
                "description": "Stable error code, e.g. `\"merge.path.too_deep\"`.",
                "type": "string"
              },
              "doc_url": {
                "description": "Optional documentation URL for this error class.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "message": {
                "description": "Human-readable description with concrete numbers when applicable.",
                "type": "string"
              },
              "op_index": {
                "description": "Index of the offending op within the original `ops` array.",
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              }
            },
            "required": [
              "code",
              "message",
              "op_index"
            ],
            "type": "object"
          }
        },
        "description": "Result of an atomic update operation",
        "properties": {
          "errors": {
            "description": "Errors encountered while applying ops. Successfully applied ops are still reflected in `new_value`. Field is omitted from JSON when empty for backward compatibility.",
            "items": {
              "$ref": "#/definitions/UpdateOpError"
            },
            "type": "array"
          },
          "new_value": {
            "description": "The value after the update"
          },
          "old_value": {
            "description": "Previous value (if it existed)."
          }
        },
        "required": [
          "new_value"
        ],
        "title": "StreamUpdateResult",
        "type": "object"
      }
    }
  ],
  "triggers": [
    {
      "description": "State trigger",
      "invocation_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Trigger config schema — field-parity with the engine's StateTriggerConfig (engine/src/workers/state/trigger.rs:18-23).",
        "properties": {
          "condition_function_id": {
            "description": "Optional function evaluated per event; only an explicit `false` skips.",
            "type": [
              "string",
              "null"
            ]
          },
          "key": {
            "description": "Only fire for writes to this key (any key when unset).",
            "type": [
              "string",
              "null"
            ]
          },
          "scope": {
            "description": "Only fire for writes in this scope (any scope when unset).",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "StateTriggerSpec",
        "type": "object"
      },
      "metadata": {},
      "name": "state",
      "return_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "StateEventType": {
            "enum": [
              "state:created",
              "state:updated",
              "state:deleted"
            ],
            "type": "string"
          }
        },
        "properties": {
          "event_type": {
            "allOf": [
              {
                "$ref": "#/definitions/StateEventType"
              }
            ],
            "description": "Type of state change"
          },
          "key": {
            "description": "State key",
            "type": "string"
          },
          "new_value": {
            "description": "New value"
          },
          "old_value": {
            "description": "Previous value (null for created events)"
          },
          "scope": {
            "description": "State scope",
            "type": "string"
          },
          "type": {
            "description": "Always \"state\"",
            "type": "string"
          }
        },
        "required": [
          "event_type",
          "key",
          "new_value",
          "scope",
          "type"
        ],
        "title": "StateCallRequest",
        "type": "object"
      }
    }
  ]
}