iii-state
v0.19.4-alpha.5Distributed key-value state management with reactive change triggers.
full markdown
/workers/iii-state.md?version=0.19.4-alpha.5. paste it into an llm prompt or pipe it through curl from a worker.install
configuration
- adapter:
config:
file_path: ./data/state_store.db
store_method: file_based
name: kvdependencies
readme
iii-state
Distributed key-value state storage with scope-based organization and reactive triggers that fire on any state change.
State is server-side key-value storage with trigger-based reactivity. Unlike streams, state does not push updates to WebSocket clients — it fires triggers that workers handle server-side.
Install
iii worker add iii-stateResolves from the worker registry at workers.iii.dev.
Skills
Install the iii-state agent skill for Claude Code, Cursor, and 30+ other agents:
npx skills add iii-hq/iii --full-depth --skill iii-stateSample Configuration
- name: iii-state
config:
adapter:
name: kv
config:
store_method: file_based
file_path: ./data/state_store
save_interval_ms: 5000Configuration
| Field | Type | Description |
|---|---|---|
adapter |
Adapter | Storage adapter selected by name — one of kv (default), redis, or bridge — with a per-adapter config. Validated against the concrete per-adapter schema at set time. Restart-tier (see below). |
triggers_enabled |
boolean | Globally enable/disable state change-trigger fan-out. Defaults to true. Applied live. |
max_value_bytes |
number | Reject state::set writes whose JSON-serialized value exceeds this many bytes (VALUE_TOO_LARGE). Minimum 1; unset means no limit. Applied live. |
save_interval_ms |
number | Persistence flush cadence (ms) for the file-backed kv adapter. 100–3600000; defaults to 5000. Task-rebuild (respawns the save loop). |
Runtime configuration (hot reload)
iii-state registers its configuration with the builtin configuration worker
under the id iii-state, so the settings above can be read and changed at
runtime (e.g. configuration::set { id: "iii-state", value: { ... } }) without
restarting the engine. The config.yaml block is the seed used on first boot
only; afterwards the configuration entry is the runtime source of truth and a
runtime edit survives engine restarts (read back via a boot-read). Values are
validated against the schema at set time, and ${VAR:default} placeholders are
expanded on read.
Apply tiers:
| Tier | Fields | Behavior |
|---|---|---|
| Live | triggers_enabled, max_value_bytes |
Take effect on the next state write — no restart. |
| Task-rebuild | save_interval_ms |
Respawns the file-backed kv save loop at the new cadence (no-op for in-memory / non-kv adapters). |
| Restart-tier | adapter |
A change is logged and applies at the next engine start; the persisted entry drives adapter construction at boot. |
The configuration worker must be file-backed (the default
fsadapter) at its default directory for the restart-tier boot-read to pick up persistedadapteredits; otherwise the config.yaml block is used at boot.
Adapters
The adapter set is closed: configuration::set accepts only the built-in adapters
below, each with its own typed config schema, so an agent introspecting the schema
(or the console) sees concrete per-adapter fields rather than a free-form object.
kv
Built-in key-value store with in-memory or file-based persistence.
name: kv
config:
store_method: file_based
file_path: ./data/state_store
save_interval_ms: 5000| Field | Type | Description |
|---|---|---|
store_method |
string | in_memory (lost on restart) or file_based (persisted to disk). |
file_path |
string | Directory path for file-based storage. |
save_interval_ms |
number | Interval in milliseconds between automatic disk saves. Defaults to 5000. |
redis
Uses Redis as the state backend.
name: redis
config:
redis_url: ${REDIS_URL:redis://localhost:6379}bridge
Forwards state operations to a remote III Engine instance via the Bridge Client.
name: bridgeFunctions
state::set
Set a value in state. Fires state:created if the key did not exist, or state:updated if it did.
Parameters: scope (string), key (string), value (any)
Returns: old_value (any), new_value (any)
state::get
Get a value from state.
Parameters: scope (string), key (string)
Returns: value (any), or null if the key does not exist.
state::delete
Delete a value from state. Fires a state:deleted trigger.
Parameters: scope (string), key (string)
Returns: value (the deleted value, or null)
state::update
Atomically update a value using one or more operations. Fires state:created or state:updated.
Parameters: scope (string), key (string), ops (UpdateOp[])
| Operation | Shape | Description |
|---|---|---|
set |
{ "type": "set", "path": "status", "value": "active" } |
Set a field or replace the root value. |
merge |
{ "type": "merge", "path": "", "value": { "status": "active" } } |
Shallow-merge an object at the root. |
increment |
{ "type": "increment", "path": "count", "by": 1 } |
Add by to a numeric field. |
decrement |
{ "type": "decrement", "path": "count", "by": 1 } |
Subtract by from a numeric field. |
append |
{ "type": "append", "path": "events", "value": { "kind": "chunk" } } |
Push one element to an array or concatenate a string. |
remove |
{ "type": "remove", "path": "status" } |
Remove a field from the current object. |
Returns: old_value (any), new_value (any)
state::list
List all values within a scope.
Parameters: scope (string)
Returns: a flat JSON array of all stored values within the scope.
state::list_groups
List all scopes that contain state data.
Returns: groups (string[]) — sorted, deduplicated array of all scope names.
Trigger Type: state
Fires when a state value is created, updated, or deleted.
| Config Field | Type | Description |
|---|---|---|
scope |
string | Only fire for changes within this scope. When omitted, fires for all scopes. |
key |
string | Only fire for changes to this specific key. When omitted, fires for all keys. |
condition_function_id |
string | Function ID for conditional execution. If it returns false, the handler is skipped. |
State Event Payload
| Field | Type | Description |
|---|---|---|
type |
string | Always "state". |
event_type |
string | "state:created", "state:updated", or "state:deleted". |
scope |
string | The scope where the change occurred. |
key |
string | The key that changed. |
old_value |
any | The previous value, or null for newly created keys. |
new_value |
any | The new value. null for deleted keys. |
Sample Code
const fn = iii.registerFunction(
{ id: 'state::onUserUpdated' },
async (event) => {
console.log('State changed:', event.event_type, event.key)
console.log('Previous:', event.old_value)
console.log('Current:', event.new_value)
return {}
},
)
iii.registerTrigger({
type: 'state',
function_id: fn.id,
config: { scope: 'users', key: 'profile' },
})api reference (json)
{
"functions": [
{
"description": "Internal: re-apply the iii-state configuration when the authoritative configuration entry changes.",
"metadata": {
"internal": true
},
"name": "iii-state::on-config-change",
"request_schema": {},
"response_schema": {}
},
{
"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": {}
},
{
"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": {}
},
{
"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": {}
},
{
"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": "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#",
"properties": {
"new_value": {
"description": "The value after the update"
},
"old_value": {
"description": "The value before the update (None if key didn't exist)"
}
},
"required": [
"new_value"
],
"title": "StreamSetResult",
"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": "The value before the update (None if key didn't exist)"
}
},
"required": [
"new_value"
],
"title": "StreamUpdateResult",
"type": "object"
}
}
],
"triggers": [
{
"description": "State trigger",
"invocation_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"condition_function_id": {
"description": "Optional function ID to evaluate before invoking handler",
"type": [
"string",
"null"
]
},
"key": {
"description": "State key to watch (exact match filter)",
"type": [
"string",
"null"
]
},
"scope": {
"description": "State scope to watch (exact match filter)",
"type": [
"string",
"null"
]
}
},
"title": "StateTriggerConfig",
"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"
}
}
]
}