Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SOP Fan-In: Overview

A fan-in is an external event source that starts SOP runs. Each source delivers events to the SOP engine through dispatch_sop_event, which matches every event against every loaded SOP’s triggers and starts runs for those that match.

One ZeroClaw instance can bind several fan-ins at once: an MQTT topic, a filesystem path, and an AMQP routing key can all feed the same engine without separate processes. Each source has a dedicated guide below.

How dispatch works

  • One matcher path: a single matcher evaluates every trigger type, so matching behaves the same regardless of source.
  • Run-start audit: started runs are persisted via SopAuditLogger.
  • Headless execution: cron triggers and webhook deliveries drive their runs to completion. The maintenance tick hands each started cron ExecuteStep or DeterministicStep to the headless run driver, and the webhook route hands each started ExecuteStep to it (dispatch already runs deterministic steps); the driver executes the step as the SOP’s own agent. Sources that still have no live driver log ExecuteStep actions as pending through process_headless_results instead of executing them.
  • Headless ownership: a headless step runs as the agent named by the step’s agent, falling back to the SOP’s agent, and that agent must be configured and enabled. There is no ambient agent turn to inherit an identity from, so a procedure with no owner is rejected at authoring time and refused at dispatch; it is never run as some other configured agent. A manual SOP is the one case that saves without an owner, because sop_execute supplies one; the dashboard’s run endpoint, which has no agent behind it, refuses to start such a procedure.
  • Headless tool scope: a headless step turn is narrowed exactly like a live one: the SOP control tools (sop_execute, sop_advance, sop_approve) are always removed so a step cannot drive its own run, and the step’s declared scope is applied on top when sop.step_scope_enforce is set. A child agent spawned from the step inherits the same exclusions.
  • Untrusted input: topic and payload text are capped, normalized, prompt-guard screened, and framed before reaching model context.

Sources

Every SOP trigger type, its fields, and its dispatch status, projected directly from the SopTrigger registry:

TypeFieldsNotes
mqtttopic, optional conditionMQTT message arrival. Live: delivered by the MQTT listener.
webhookpathInbound HTTP request. Live: gateway /sop/* and SOP-first /webhook routes.
cronexpressionTime-based firing. Live: dispatched by the SOP maintenance tick (daemon / channel-start paths).
peripheralboard, signal, optional conditionHardware signal. Defined and matched, but no peripheral listener feeds it.
filesystempath, optional condition, optional eventsFilesystem change. Live: delivered by the filesystem watcher.
calendarcalendar_source, optional calendar_ids, optional conditionCalendar event state. Defined and matched, but no poller feeds it live.
channelchannel, optional alias, optional conditionInbound message or forge/platform event on a configured channel (telegram, discord, slack, git, …). Live: delivered by the channel orchestrator when the channel’s SOP dispatch is enabled. The Git forge producer sets an event topic of the form <channel>.<alias>:<event_type> and puts event_type in the payload, so an authored condition filters forge events by type without a second trigger shape.
manualnoneAgent-initiated run via the sop_execute tool. Not an external fan-in.
amqprouting_key, optional conditionAMQP delivery. Live: delivered by the AMQP consumer in a SOP dispatch mode.

Each source has a dedicated guide in the sidebar. Live sources (delivered by a running listener or authenticated gateway request) start runs as events arrive; cron triggers are dispatched by the daemon’s periodic SOP maintenance tick; agent-initiated runs start from inside an agent turn via sop_execute; the remaining defined-but-unwired sources (peripheral and calendar) validate and match but have no live event source routing into the dispatcher yet.

Security defaults

ConcernMechanism
Webhook authenticationGateway pairing bearer authentication plus optional gateway.webhook_secret/X-Webhook-Secret; /sop/* and /webhook share the same rate limiter. At least one control must be configured for SOP dispatch, and every configured control must pass. The separate [channels.webhook] alias secrets never authorize these routes
Webhook replay protectionOptional X-Idempotency-Key, namespaced per SOP path as well as separately between /sop/* and /webhook. Keys are reserved before dispatch and mean at-most-once attempt, not proof that a prior run started
MQTT transportmqtts:// with use_tls = true for TLS transport
Filesystem rootsBroad roots (/, /home, /etc, /var, /proc, /sys, /dev, /tmp) rejected at config validation unless allow_broad_roots; include/exclude globs scope events
Filesystem symlinksSymlink event paths are rejected before any metadata, hash, or content read by default; follow_symlinks = true opts in but still requires the canonical target to resolve inside a watched root
Untrusted trigger inputTopic and payload text are capped, normalized, prompt-guard screened, and framed before model context
Unsafe trigger blockuntrusted_input_guard = "block" refuses unsafe untrusted events with BlockedUnsafe; default warn audits and allows
Cron validationInvalid cron expressions fail closed during parsing and cache build
Headless ownershipA headless SOP must declare an owning agent that is configured and enabled; authoring blocks the save and the driver refuses the step rather than borrowing another configured agent. A dashboard-started manual run is refused the same way
Headless tool scopeHeadless step turns always exclude the SOP control tools and honour the step’s declared scope under step_scope_enforce; child agents spawned from the step inherit those exclusions
Headless driver lifetimeCron and webhook drivers are owned by the daemon generation that started them: drained (30s), then aborted and joined (5s) before a reload rebuilds the config and SOP engine. A driver that reaches no await point in that grace keeps running and is carried into the next generation, logged rather than detached
Headless dispatchSources with no live driver log run progression instead of auto-executing ExecuteStep

Troubleshooting

SymptomLikely causeFix
SOP never starts from a live sourcetrigger pattern mismatch or a failing conditionVerify the trigger pattern matches the delivered event; check the condition against the payload
SOP started but a step did not executea source with no live driver (peripheral, calendar) reached ExecuteStepUse cron, a webhook, or another live source, run an agent loop for ExecuteStep, or design the run to pause on approvals
Cron SOP starts, then fails every run with “no owning agent”the procedure declares no agent, and a headless run has none to inheritSet agent in SOP.toml (or on the step) to a configured agent alias
Cron SOP fails every run with “which is disabled”the owning agent is configured with enabled = falseRe-enable the agent, or point agent at one that is enabled
Dashboard run button returns 422 for a manual SOPthe procedure’s execute steps declare no owner, and a dashboard run has no agent turn to inherit one fromSet agent in SOP.toml (or on the step), or start the procedure from an agent with sop_execute
A cron step cannot call a tool it used interactivelythe step’s scope excludes it under step_scope_enforce, or it is a SOP control toolWiden the step’s scope; the SOP control tools stay excluded by design
Webhook trigger never firesexact trigger path mismatch, SOP subsystem unavailable, or authentication rejectedRun zeroclaw daemon with sop.sops_dir configured, match the full request path exactly, and provide the configured bearer/secret headers
Peripheral or calendar trigger never firesevent source not wired into the dispatcherUse a live source (Webhook, MQTT, Filesystem, AMQP) or start the run with sop_execute
Cron trigger never firesmaintenance tick not running (no zeroclaw daemon or zeroclaw channel start; standalone gateway start does not run it), sops_dir unset/empty, or maintenance_interval_secs = 0Run zeroclaw daemon (or zeroclaw channel start) with sop.sops_dir set to a non-empty value (unset by default; the documented value is shared/sops) and sop.maintenance_interval_secs non-zero (default 60)

See also