configuration
v0.24.6Register, store, and watch typed configuration values for the engine.
Install
iii trigger compose::add worker=configurationRun in your project with the iii engine running. Exact versions are immutable and digest-pinned.
Full markdown
/registry/workers/configuration.md. Paste it into an LLM prompt or pipe it through curl from a worker.Install
iii trigger compose::add worker=configuration@0.24.6Configuration
- adapter:
config:
directory: ./config
name: fs
ttl_seconds: 0Dependencies
Readme
configuration
Schema-validated, reactive registry of named configuration entries. Workers register their configuration ids with a JSON Schema, publish values that are validated against it, and emit configuration triggers on every change so other workers react without polling.
The default fs adapter stores one YAML file per id under a configurable directory and watches it for external edits — manual edits surface as configuration:updated events the same way SDK calls do. The bridge adapter delegates to a remote III Engine and re-broadcasts its events into the local fan-out. Reads expand ${VAR:default} placeholders against the live process env on every call, so env changes propagate without a worker restart.
configuration is engine-owned. Configure it under engine.workers.configuration in a managed
worker-compose.yaml; do not add it as a project container.
Skills
Install the configuration agent skill for Claude Code, Cursor, and 30+ other agents:
npx skills add iii-hq/iii --full-depth --skill configurationSample Configuration
engine:
workers:
configuration:
adapter:
name: fs
config:
directory: ./config
ttl_seconds: 0Configuration
| Field | Type | Description |
|---|---|---|
adapter |
Adapter | Adapter for configuration persistence. Defaults to fs. |
ttl_seconds |
integer | Per-id cleanup countdown in seconds. When >0, an entry whose last subscriber trigger has been unregistered for this long is deleted. Defaults to 0 (no cleanup). |
Adapters
fs
File-system adapter that stores one YAML file per configuration id and watches the directory for external edits.
name: fs
config:
directory: ./config| Field | Type | Description |
|---|---|---|
directory |
string | Directory holding files. Created on boot. Defaults to ./config. |
The default location was previously ./data/configuration. When running on the default ./config, the adapter performs a one-time soft migration on boot: every still in ./data/configuration is moved across (logged at INFO). If a file with the same name already exists in ./config, that file is skipped with a WARNING and the legacy copy is left untouched for you to reconcile. An explicit directory: override disables the migration.
External writes / edits / removals to the watched directory are debounced (500 ms) and replayed as configuration:registered, configuration:updated, or configuration:deleted events through the same trigger fan-out used by SDK calls. A hand-edit that fails schema validation (after ${VAR} expansion) is rejected with a WARNING — the previous good value is kept rather than loaded.
bridge
Forwards every configuration::* call to a remote III Engine via the iii-sdk and registers a remote configuration trigger so changes on the source engine are mirrored into the local trigger fan-out.
name: bridge
config:
bridge_url: ${REMOTE_III_URL:ws://localhost:49134}| Field | Type | Description |
|---|---|---|
bridge_url |
string | WebSocket URL of the remote III Engine. Defaults to ws://localhost:49134. |
The bridge adapter cannot delete configurations on the remote engine; cleanup happens via the remote's TTL or by operating on the remote engine directly.
configuration::ensure over the bridge is forwarded to the remote engine's own configuration::ensure — the original seed candidate is sent to the remote and the seed-vs-preserve decision is made at the authoritative engine, so a stale local cache can never overwrite an operator value on the remote engine. If the remote engine does not expose configuration::ensure (an older engine), the call fails closed with ADAPTER_ERROR; the bridge deliberately does not fall back to a legacy configuration::register, which would risk overwriting remote state.
Functions
configuration::register
Declare a configuration id with a JSON Schema, name, description, and optional initial value. Idempotent — re-registering replaces the schema, name, description, and metadata; the stored value is kept unless initial_value is supplied. Validates initial_value against schema before persisting. Fires configuration:registered on first registration or configuration:updated on subsequent calls.
Parameters: id (string), name (string), description (string), schema (object — JSON Schema), initial_value (any, optional), metadata (any, optional)
Returns: the stored entry { id, name, description, schema, value, metadata }. Templates inside value are stored verbatim; expansion happens on read.
configuration::ensure
Atomically ensure an id exists with a schema, name, description, and metadata, seeding initial_value only when no non-null value is stored yet. If a value already exists (including false, 0, or "" — these are real values, not "empty"), it is preserved verbatim and the candidate seed is ignored and never validated. Unlike configuration::register, ensure never overwrites a stored value, so it is the race-free way for one or many workers to seed a default — use it instead of read-then-register. When the seed is applied it is validated against schema exactly like register validates initial_value. Fires configuration:registered on first creation or configuration:updated when an existing entry is refreshed.
register, ensure, set, and delete are linearized per store (one engine process — the fs adapter's on-disk store), so with the fs adapter a seed can never clobber a concurrent set and two racing seeds resolve to a single winner without a lost write. This lock guards the LOCAL store only: with the bridge adapter the authoritative store is the remote engine and its own linearization applies (the local cache is a best-effort mirror), so configuration::ensure is decided remotely (see the bridge adapter above), never against the local cache. External direct edits to an fs file are surfaced by the directory watcher and reconciled into the in-memory cache against the adapter's CURRENT authoritative state under the write path's lock — a watcher event is a hint, not a trusted snapshot. The event is captured and queued before it is applied, so a newer local register/ensure/set/delete can commit in between; the reconcile re-reads the adapter and adopts its current value, so a stale event never reverts an internal write and a stale delete never drops a locally recreated entry (a failed authoritative read keeps the last-known cache rather than deleting). This matters because the watcher echo-suppresses a superseded edit, so no later corrective event would repair a cache a trusted-verbatim snapshot had reverted. Genuine concurrent external edits remain last-writer-wins. The bridge adapter's authority is the remote engine's ordered event stream, so its relayed events are applied directly, without a reconciling round-trip. Trigger fan-out happens after each mutation commits, is suppressed for an external event a newer local mutation superseded, and is not strictly ordered across concurrent callers.
Parameters: id (string), name (string), description (string), schema (object — JSON Schema), initial_value (any, optional — the seed candidate), metadata (any, optional)
Returns: action (one of seeded, preserved, registered) and entry (the stored { id, name, description, schema, value, metadata }). seeded means the candidate was installed; preserved means an existing value was kept; registered means the entry was created or refreshed with a null value because no seed was supplied.
configuration::set
Replace the stored value for a registered id. Validates value against the registered schema. Fires configuration:updated.
Parameters: id (string), value (any)
Returns: old_value (any, or null if the entry had no prior value), new_value (any)
configuration::get
Read one configuration by id.
Parameters: id (string), raw (boolean, optional — defaults to false)
Returns: id (string), value (any). When raw is false, ${VAR:default} placeholders inside string fields are expanded against the live process env. When raw is true, the stored value is returned verbatim.
configuration::list
Enumerate every registered configuration. Never returns the stored value — pair with configuration::get once you have the id you want.
Returns: configurations (array of { id, name, description, schema, metadata }), sorted lexicographically by id.
configuration::schema
Read the schema, name, description, and metadata for one id without exposing the value.
Parameters: id (string)
Returns: id (string), name (string), description (string), schema (object), metadata (any, optional)
Error Codes
| Code | Meaning |
|---|---|
NOT_REGISTERED |
set was called against an id that has not been passed to register yet. |
INVALID_ID |
The id does not match [a-z0-9_-]{1,64} — the constraint applied so ids are safe filenames for the fs adapter. |
SCHEMA_INVALID |
The supplied value does not satisfy the registered JSON Schema. The error message lists each violation. |
SCHEMA_UNAVAILABLE |
set was called for an id whose schema is not yet known — the owning worker has not registered it this session. The fs adapter does not persist the schema, so it is absent until a worker re-registers after boot. |
NOT_FOUND |
get or schema was called against an id that is not registered. |
ADAPTER_ERROR |
The adapter failed to persist the change (disk error, bridge unreachable, etc.). |
Trigger Type: configuration
Fires when a configuration entry is registered, updated, or deleted — including external fs file edits and bridge-forwarded events from a remote engine.
| Config Field | Type | Description |
|---|---|---|
configuration_id |
string | Only fire for changes to this id. When omitted, fires for every id. |
event_types |
string[] | Subset of configuration:registered, configuration:updated, configuration:deleted. When omitted, fires for every event type. |
condition_function_id |
string | Function ID for conditional execution. If it returns false, the handler is skipped. |
Configuration Event Payload
| Field | Type | Description |
|---|---|---|
type |
string | Always "configuration". |
event_type |
string | "configuration:registered", "configuration:updated", or "configuration:deleted". |
id |
string | The configuration id that changed. |
name |
string | The registered name at the time of the event. |
description |
string | The registered description. |
schema |
object | The registered JSON Schema. |
old_value |
any | Previous value with ${VAR:default} placeholders expanded. null on configuration:registered. |
new_value |
any | New value with ${VAR:default} placeholders expanded. null on configuration:deleted. |
metadata |
any | Echoes the registered metadata; omitted when none was supplied. |
Sample Code
const fn = iii.registerFunction(
{ id: 'stream::onConfigChange' },
async (event) => {
console.log('Configuration changed:', event.event_type, event.id)
console.log('New value:', event.new_value)
return {}
},
)
iii.registerTrigger({
type: 'configuration',
function_id: fn.id,
config: {
configuration_id: 'iii-stream',
event_types: ['configuration:updated'],
},
})Mutations that fire triggers: configuration::register (:registered on first call, :updated on re-registration), configuration::ensure (:registered when it creates an entry, :updated when it refreshes one), configuration::set (:updated), TTL-driven cleanup (:deleted), and external fs create / edit / delete events. Reads (configuration::get, configuration::list, configuration::schema) do not fire triggers.
TTL Cleanup
The worker tracks the number of triggers currently bound to each configuration id. When ttl_seconds > 0 is configured AND the last trigger for an id is unregistered, a countdown deletes the entry after the TTL elapses. A new trigger registration before the countdown fires aborts the cleanup; while at least one trigger is bound to an id, that entry never expires.
ttl_seconds = 0 (the default) disables expiry entirely — entries persist until they are explicitly deleted via the adapter or, for the fs adapter, by removing the underlying file.
This is the cleanup story for ephemeral workers that come and go (sandboxes, short-lived consumers) and should not leave stale configuration behind when no one is left subscribing.
Usage Example: Migrating a Worker Off config.yaml
import { registerWorker } from 'iii-sdk'
const iii = registerWorker('ws://localhost:49134')
// 1. Declare the schema once at startup.
await iii.trigger({
function_id: 'configuration::register',
payload: {
id: 'iii-stream',
name: 'Stream worker',
description: 'Connection settings for the stream worker.',
schema: {
type: 'object',
required: ['port'],
properties: {
port: { type: 'integer', minimum: 1, maximum: 65535 },
host: { type: 'string' },
},
},
initial_value: { port: 3112, host: '127.0.0.1' },
},
})
// 2. Read the live value (placeholders expanded) anywhere you need it.
const { value } = await iii.trigger({
function_id: 'configuration::get',
payload: { id: 'iii-stream' },
})
// 3. Subscribe to changes elsewhere — set/external-edit/bridge events all
// arrive on the same trigger.
const onChange = iii.registerFunction('stream::onConfigChange', async (event) => {
console.log('Stream config now:', event.new_value)
return {}
})
iii.registerTrigger({
type: 'configuration',
function_id: onChange.id,
config: { configuration_id: 'iii-stream' },
})Skills
API reference (JSON)
{
"functions": [
{
"description": "Atomically ensure a configuration id exists with a schema, name, description, and metadata, seeding initial_value ONLY when no non-null value is stored yet. An existing non-null value (including false, 0, or empty string) is preserved verbatim and the candidate seed is ignored (and not validated). Unlike configuration::register, ensure never overwrites a stored value, so it is the race-free way for one or many workers to seed a default. Fires configuration:registered on first creation or configuration:updated when an existing entry is refreshed.",
"metadata": {},
"name": "configuration::ensure",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"description": {
"description": "Description shown in `configuration::list`.",
"type": "string"
},
"id": {
"description": "Configuration id. Must match `[a-z0-9_-]{1,64}` so it is safe as a filename in the `fs` adapter.",
"type": "string"
},
"initial_value": {
"description": "Optional seed value. Applied atomically ONLY when no non-null value is already stored for this id (the id is absent or its stored value is `null`). When a non-null value already exists it is preserved verbatim and this candidate is ignored — never validated. When the seed IS applied it is validated against `schema` exactly like `configuration::register` validates `initial_value`."
},
"metadata": {
"description": "Optional opaque metadata stored alongside the entry."
},
"name": {
"description": "Human-readable name shown in `configuration::list`.",
"type": "string"
},
"schema": {
"description": "JSON Schema describing the value shape. `set` validates against this."
}
},
"required": [
"description",
"id",
"name",
"schema"
],
"title": "ConfigurationEnsureInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ConfigurationEntry": {
"description": "A single configuration entry tracked by the worker.\n\n`value` is stored verbatim — including any `${VAR:default}` template strings. Expansion happens on read (`configuration::get`) and during trigger fan-out so env-var changes propagate without a worker restart.",
"properties": {
"description": {
"description": "One-line description of what this configuration controls.",
"type": "string"
},
"id": {
"description": "Stable identifier (e.g. `iii-stream`, `iii-observability`).",
"type": "string"
},
"metadata": {
"description": "Optional caller-supplied metadata (owner team, change ticket, etc.)."
},
"name": {
"description": "Human-readable display name.",
"type": "string"
},
"schema": {
"default": null,
"description": "JSON Schema describing the shape of `value`. Not persisted to disk by the fs adapter; rebuilt in-memory when the owning worker re-registers."
},
"value": {
"default": null,
"description": "Stored configuration body. May contain `${VAR:default}` placeholders."
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"EnsureAction": {
"description": "What `configuration::ensure` did with the stored value.",
"oneOf": [
{
"description": "The candidate `initial_value` was written because no non-null value was stored yet (the id was absent or its stored value was `null`).",
"enum": [
"seeded"
],
"type": "string"
},
{
"description": "A non-null value was already stored and preserved verbatim; the candidate seed (if any) was ignored. Name, description, schema, and metadata were still refreshed.",
"enum": [
"preserved"
],
"type": "string"
},
{
"description": "The entry was created or refreshed but no seed was applied and no value existed, so the value stays `null` (awaiting a later `set`/`ensure`).",
"enum": [
"registered"
],
"type": "string"
}
]
}
},
"properties": {
"action": {
"allOf": [
{
"$ref": "#/definitions/EnsureAction"
}
],
"description": "What ensure did: seeded the candidate, preserved an existing value, or registered the entry with a null value."
},
"entry": {
"allOf": [
{
"$ref": "#/definitions/ConfigurationEntry"
}
],
"description": "The stored entry after ensure. Templates inside `value` are kept verbatim; expansion happens on read."
}
},
"required": [
"action",
"entry"
],
"title": "ConfigurationEnsureResult",
"type": "object"
}
},
{
"description": "Read the active configuration by id (execution injection when present, otherwise the persistent base). Expands ${VAR:default} placeholders against the live process env unless raw=true is passed.",
"metadata": {},
"name": "configuration::get",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string"
},
"raw": {
"default": false,
"description": "When `true`, return the stored value verbatim (including `${VAR}` placeholders). When `false` (default), expand `${VAR:default}` against the live process env.",
"type": "boolean"
}
},
"required": [
"id"
],
"title": "ConfigurationGetInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string"
},
"value": true
},
"required": [
"id",
"value"
],
"title": "ConfigurationGetResult",
"type": "object"
}
},
{
"description": "List every registered configuration with id, name, description, and schema. Sorted by id; never returns the stored value.",
"metadata": {},
"name": "configuration::list",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ConfigurationListInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ConfigurationSchemaView": {
"description": "Subset of a [`ConfigurationEntry`] returned from `list` / `schema` — the schema-only view that never leaks the stored value.",
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"metadata": true,
"name": {
"type": "string"
},
"schema": true
},
"required": [
"description",
"id",
"name",
"schema"
],
"type": "object"
}
},
"properties": {
"configurations": {
"items": {
"$ref": "#/definitions/ConfigurationSchemaView"
},
"type": "array"
}
},
"required": [
"configurations"
],
"title": "ConfigurationListResult",
"type": "object"
}
},
{
"description": "Move an exact legacy configuration id at the authority, replacing the destination with the source and archiving the source as .yaml.bak. Active memory follows a successful move without being persisted. An absent persisted source leaves the destination unchanged and retires orphan source memory. The response contains the persisted entry. Stop consumers before migrating.",
"metadata": {},
"name": "configuration::migrate",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Move one exact legacy id with source priority, retaining its original backup.",
"properties": {
"from_id": {
"type": "string"
},
"to_id": {
"type": "string"
}
},
"required": [
"from_id",
"to_id"
],
"title": "ConfigurationMigrateInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ConfigurationEntry": {
"description": "A single configuration entry tracked by the worker.\n\n`value` is stored verbatim — including any `${VAR:default}` template strings. Expansion happens on read (`configuration::get`) and during trigger fan-out so env-var changes propagate without a worker restart.",
"properties": {
"description": {
"description": "One-line description of what this configuration controls.",
"type": "string"
},
"id": {
"description": "Stable identifier (e.g. `iii-stream`, `iii-observability`).",
"type": "string"
},
"metadata": {
"description": "Optional caller-supplied metadata (owner team, change ticket, etc.)."
},
"name": {
"description": "Human-readable display name.",
"type": "string"
},
"schema": {
"default": null,
"description": "JSON Schema describing the shape of `value`. Not persisted to disk by the fs adapter; rebuilt in-memory when the owning worker re-registers."
},
"value": {
"default": null,
"description": "Stored configuration body. May contain `${VAR:default}` placeholders."
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"MigrateAction": {
"description": "Migration never treats an existing null value as absence.",
"enum": [
"migrated",
"preserved",
"missing"
],
"type": "string"
}
},
"description": "Authoritative raw destination snapshot, without environment expansion.",
"properties": {
"action": {
"$ref": "#/definitions/MigrateAction"
},
"entry": {
"anyOf": [
{
"$ref": "#/definitions/ConfigurationEntry"
},
{
"type": "null"
}
]
}
},
"required": [
"action"
],
"title": "ConfigurationMigrateResult",
"type": "object"
}
},
{
"description": "Read-only migration contract negotiation. Revision 1 guarantees source priority and source archival; it does not migrate or write configuration.",
"metadata": {},
"name": "configuration::migration-capabilities",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
}
},
{
"description": "Register a configuration id with a name, description, and JSON Schema. Idempotent — re-registering replaces metadata and (when initial_value is provided) the value. Validates initial_value against the schema.",
"metadata": {},
"name": "configuration::register",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"description": {
"description": "Description shown in `configuration::list`.",
"type": "string"
},
"id": {
"description": "Configuration id. Must match `[a-z0-9_-]{1,64}` so it is safe as a filename in the `fs` adapter.",
"type": "string"
},
"initial_value": {
"description": "Optional initial value to install. Validated against `schema`."
},
"metadata": {
"description": "Optional opaque metadata stored alongside the entry."
},
"name": {
"description": "Human-readable name shown in `configuration::list`.",
"type": "string"
},
"schema": {
"description": "JSON Schema describing the value shape. `set` validates against this."
}
},
"required": [
"description",
"id",
"name",
"schema"
],
"title": "ConfigurationRegisterInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "A single configuration entry tracked by the worker.\n\n`value` is stored verbatim — including any `${VAR:default}` template strings. Expansion happens on read (`configuration::get`) and during trigger fan-out so env-var changes propagate without a worker restart.",
"properties": {
"description": {
"description": "One-line description of what this configuration controls.",
"type": "string"
},
"id": {
"description": "Stable identifier (e.g. `iii-stream`, `iii-observability`).",
"type": "string"
},
"metadata": {
"description": "Optional caller-supplied metadata (owner team, change ticket, etc.)."
},
"name": {
"description": "Human-readable display name.",
"type": "string"
},
"schema": {
"default": null,
"description": "JSON Schema describing the shape of `value`. Not persisted to disk by the fs adapter; rebuilt in-memory when the owning worker re-registers."
},
"value": {
"default": null,
"description": "Stored configuration body. May contain `${VAR:default}` placeholders."
}
},
"required": [
"description",
"id",
"name"
],
"title": "ConfigurationEntry",
"type": "object"
}
},
{
"description": "Retrieve the schema, name, and description for a configuration id. Mirrors a single entry from configuration::list.",
"metadata": {},
"name": "configuration::schema",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"title": "ConfigurationSchemaInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Subset of a [`ConfigurationEntry`] returned from `list` / `schema` — the schema-only view that never leaks the stored value.",
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"metadata": true,
"name": {
"type": "string"
},
"schema": true
},
"required": [
"description",
"id",
"name",
"schema"
],
"title": "ConfigurationSchemaView",
"type": "object"
}
},
{
"description": "Replace the active configuration and notify consumers. flush=true (default) validates and persists the complete value for a registered id. flush=false updates memory only and may precede registration; validation uses the schema when available. No automatic later flush.",
"metadata": {},
"name": "configuration::set",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"flush": {
"default": true,
"description": "Persist the complete value (default true). False updates only active memory and permits delivery before the owning worker registers its schema.",
"type": "boolean"
},
"id": {
"type": "string"
},
"value": {
"description": "New configuration value. Validated against the schema when available."
}
},
"required": [
"id",
"value"
],
"title": "ConfigurationSetInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"new_value": {
"description": "Current stored value (templates not expanded)."
},
"old_value": {
"description": "Previous stored value, or `null` when the entry had no value yet."
}
},
"required": [
"new_value"
],
"title": "ConfigurationSetResult",
"type": "object"
}
}
],
"triggers": []
}