How SOPs run
Runtime contract
- SOP definitions are loaded from
<shared>/sops/<sop_name>/SOP.tomlplus optionalSOP.md. - CLI
zeroclaw sopmanages definitions offline:list,validate,show,graph,rename,delete. - SOP runs are started by a live event fan-in (authenticated webhook, MQTT, filesystem, or AMQP), by the daemon’s periodic SOP maintenance tick for
crontriggers, or by the in-agent toolsop_execute. The remaining trigger types (peripheral and calendar) are defined and matched but not yet wired to a live event source (see SOP Fan-In). - Run progression uses tools:
sop_status,sop_approve,sop_advance. - Run state is process-local by default. With
sop.persist_runs = true, successful initialization of the default SQLite backend stores it under<data_dir>/sop/runs.dband restores active runs after restart. Initialization failure logs a warning and falls back to process-local memory. - SOP audit records are persisted in the configured Memory backend under category
sop.
Run state and audit history are separate surfaces. See Background work lifecycle for lifecycle ownership, cancellation, and restart semantics.
Event flow
graph LR
MQTT[MQTT listener] -->|topic match| Dispatch
TOOL[sop_execute tool] -->|manual| Dispatch
WH[Webhook request] -->|authenticated HTTP fan-in| Dispatch
CRON[Cron trigger] -->|daemon maintenance tick| Dispatch
GPIO[Peripheral trigger] -.->|defined, unwired| Dispatch
Dispatch --> Engine[SOP Engine]
Engine --> Run[SOP Run]
Run --> Action{Action}
Action -->|ExecuteStep| Agent[Agent Loop]
Action -->|WaitApproval| Human[Operator]
Human -->|sop_approve| Run
Getting started
-
sops_diris unset by default, so runtime SOP loading is off out of the box. Opt in by settingsops_dirthrough the gateway, zerocode, orzeroclaw config set. A relative value resolves against the install root (the directory holdingconfig.toml), so the documentedshared/sopsyields<install>/shared/sops, the same directory the SOP author writes to. An absolute or~-prefixed value is used as-is. Setting it back to""(or removing it) disables runtime SOP loading again; the CLI still falls back to<install>/shared/sopsfor offline inspection.Migrating from an earlier build? Relative
sops_dirvalues now resolve against the install root, matching howskill-bundlesdirectories resolve. Earlier builds had two different roots for the same setting, so check both before upgrading:Surface on earlier builds Root it used Where sops_dir = "shared/sops"landedRuntime loading and local zeroclaw sopCLIdata_dir<data_dir>/shared/sopsWeb and RPC SOP authoring <install>/shared<install>/shared/shared/sops(doubled segment)Both now resolve to the single canonical
<install>/shared/sops. Inspect both old locations and move any definitions you find there into<install>/shared/sops; definitions left behind in either tree become invisible after upgrade. Definitions authored through the old web or RPC surface are the easiest to miss, because they sit in the doubled writer path rather than the location the docs described.Any other relative value shifts the same way:
sops_dir = "my-sops"moves from<data_dir>/my-sops(runtime and CLI) or<install>/shared/my-sops(web and RPC authoring) to<install>/my-sops. Absolute and~-prefixed values are unaffected.The unset case moves too: the offline CLI fallback used to scan
<data_dir>/sopsand now scans<install>/shared/sops.zeroclaw sop listreads the new location, so an empty listing after upgrade means definitions are still sitting in one of the old trees. -
Create a SOP directory, for example:
~/.zeroclaw/shared/sops/deploy-prod/SOP.toml ~/.zeroclaw/shared/sops/deploy-prod/SOP.md -
Validate and inspect definitions:
sh
zeroclaw sop list zeroclaw sop validate zeroclaw sop show deploy-prod -
Trigger runs via configured event sources, or manually from an agent turn with
sop_execute.
Renaming a SOP
A SOP’s name is its address: the definition lives in <sops>/<name>/, and every
authoring surface persists a SOP under the name it was handed. Saving an edited
SOP under a different name would write a second copy and leave the original
behind, so an edit-save rejects a name change outright. Renaming is its own
operation.
sh
zeroclaw sop rename deploy-prod deploy-production
The daemon exposes the same operation as the sops/rename RPC method, taking
{"from": "...", "to": "..."}, and as POST /api/sops/{name}/rename with a
{"to": "..."} body. A taken target name comes back as a conflict
(409, RPC code -32020); an unknown source SOP comes back as not found
(404, RPC code -32021).
A rename:
- refuses a target name that is already taken, so it can never merge two SOPs;
- refuses a target that is not a single path component, the same check every other name-taking SOP operation applies;
- refuses a source that is not a real directory sitting in the SOP root. A symlink there would pass the name check and then redirect every read and write at whatever it points to, so rename checks without following links and confirms the source resolves inside the root before reading anything;
- re-runs strict save validation, so it cannot put a definition back on disk that the authoring surface would have refused to write;
- changes only the
[sop] namevalue inSOP.toml, keeping that line’s own comment and spacing. Steps, triggers, other comments, andSOP.mdare left exactly as they were; the one cosmetic change is that a single-quoted name comes back double-quoted.
The directory move is a single filesystem rename and it happens last, so the SOP
exists in exactly one place at every instant: an interrupted rename can leave
neither two copies nor none. If a rename is interrupted between the manifest
rewrite and the move, the SOP keeps its old directory while the manifest already
carries the new name, so zeroclaw sop list reports the new name. Re-running the
same rename finishes the job.
Renaming rewrites the definition on disk, exactly like saving or deleting one. A daemon that already loaded the old definition keeps running it until it reloads its SOPs, and run history, audit records, and anything else that captured the old name keeps the old name.
Authoring writes are serialized. Creating, saving, deleting, and renaming all
take an advisory lock on a .sop-authoring.lock file in the SOP root before
touching anything, so two of them cannot interleave even across processes: the
CLI writing into a root a running daemon also authors into waits its turn rather
than racing it. An authoring call that cannot take the lock within ten seconds
fails rather than queueing indefinitely. Reads are not serialized. Every file an
authoring call writes is replaced through an atomic rename, so a reader never
sees a truncated or half-written file. A save writes two files, SOP.toml and
SOP.md, one after the other, so a reader that runs during a save can briefly
see the manifest from one revision and the steps from the other, and a reader
racing the first save of a new SOP can see its manifest before its steps exist.
The next read after the save returns sees both files at the new revision.
Each step commits through a rename, so neither an ordinary reader nor a killed process ever sees a step half-done. The directory holding each renamed entry is flushed afterwards, so on Unix the ordering also survives a machine crash. If that flush fails after the rename has already happened, the operation still finishes and the failure is logged: the change is complete and visible, and only the crash guarantee for that one entry is lost. macOS honors the flush for ordering without forcing the device cache to drain, and Windows has no directory-sync primitive, so on those platforms the last step of the guarantee is the filesystem’s to keep rather than something ZeroClaw can force.
For trigger routing and auth details, see SOP Fan-In.