SOP reference: syntax, triggers & execution
SOP.toml metadata, SOP.md steps, the SOP CLI, MQTT/webhook/cron/peripheral/manual triggers, execution modes including deterministic no-LLM mode, run lifecycle, concurrency/cooldown, and audit.
A Standard Operating Procedure (SOP) is a deterministic, event-driven procedure with typed triggers, approval gates, and an auditable run record. SOPs are defined as SOP.toml + SOP.md files on disk and executed by the Rust SopEngine — distinct from YAML workflows, which are stored in Kumiho and run by the operator-mcp backend. Reach for an SOP when an external signal (an MQTT message, a webhook, a cron tick, or a hardware pin) must reliably fire a defined, gated, audited procedure — including a no-LLM deterministic mode for purely mechanical sequences.
Use this page as the complete reference for SOP file syntax, trigger types, execution modes, the run lifecycle, and the CLI and agent tools that drive runs. For a conceptual comparison of SOPs and workflows, start with the Workflows & SOP overview. For the related YAML workflow engine, see Your first workflow.
SOP system overview
Section titled “SOP system overview”An SOP lives in its own directory under a configured sops_dir. Each directory contains a required SOP.toml (metadata + triggers) and an optional SOP.md (the numbered procedure steps). The engine loads every SOP at startup, matches incoming events against each SOP’s triggers, and starts a run for every match. Runs progress through the agent loop, pausing at approval gates and deterministic checkpoints, while the audit logger records every start, step, and approval.
~/.revka/workspace/sops/ deploy-prod/ SOP.toml # metadata + triggers (required) SOP.md # numbered procedure steps (optional, but a run with no steps fails validation)The engine defines a shared dispatcher (dispatch_sop_event) for MQTT, cron, and peripheral triggers, but in the current daemon the only path that actually starts a run is the sop_execute tool, which calls engine.start_run directly. The dispatcher and its source wrappers exist in the source but are not yet wired into the running daemon (see Fan-in routing).
Event sources shared path MQTT message (topic match) │ Scheduler (window check) ─────┼──▶ dispatch_sop_event ──▶ SopEngine ──▶ SOP Run ──▶ Action Peripheral signal (board/signal) │ │ sop_execute tool (engine.start_run) ──┘ ┌────────────────────────┴───────────┐ ExecuteStep WaitApproval │ │ ▼ ▼ Agent loop Operator │ sop_approve ▼ SOP Run (resumes)Enable the SOP subsystem
Section titled “Enable the SOP subsystem”The SOP engine is activated by setting sops_dir in ~/.revka/config.toml. The tools and engine are registered only when [sop].sops_dir is present:
[sop]sops_dir = "sops" # defaults to <workspace>/sops when omitted; explicit value is used as-is (relative to CWD, with ~ expansion)default_execution_mode = "supervised"| Key | Type | Default | Meaning |
|---|---|---|---|
sops_dir | string | <workspace>/sops | Directory scanned for SOP subdirectories. The SOP engine is only activated when this is set. |
default_execution_mode | string | supervised | Mode applied to SOPs that don’t set their own execution_mode. |
max_concurrent_total | int | 4 | Global cap on simultaneous runs across all SOPs. |
approval_timeout_secs | int | 300 | Seconds to wait at an approval gate before timeout handling (0 = wait forever). |
SOP definition — SOP.toml
Section titled “SOP definition — SOP.toml”SOP.toml is the manifest: identity, priority, execution policy, concurrency/cooldown limits, and one or more [[triggers]].
[sop]name = "deploy-prod"description = "Deploy service to production"version = "1.0.0"priority = "high" # low | normal | high | criticalexecution_mode = "supervised" # auto | supervised | step_by_step | priority_based | deterministiccooldown_secs = 300max_concurrent = 1deterministic = false # true forces execution_mode = deterministic
[[triggers]]type = "webhook"path = "/sop/deploy"
[[triggers]]type = "manual"| Field | Type | Default | Meaning |
|---|---|---|---|
name | string | — (required) | Unique SOP identifier, used by sop_execute and the CLI. |
description | string | — (required) | Human-readable purpose. |
version | string | "0.1.0" | Semantic version of the procedure. |
priority | string | normal | One of low, normal, high, critical. Drives priority_based mode and approval-timeout escalation. |
execution_mode | string | ([sop].default_execution_mode) | One of auto, supervised, step_by_step, priority_based, deterministic. See Execution modes. |
cooldown_secs | int | 0 | Minimum seconds between runs of this SOP (0 = no cooldown). |
max_concurrent | int | 1 | Maximum simultaneous runs of this SOP. |
deterministic | bool | false | When true, overrides execution_mode to deterministic. |
Each [[triggers]] block declares one trigger; an SOP can have many. See Trigger types for the fields each type accepts.
SOP definition — SOP.md steps
Section titled “SOP definition — SOP.md steps”SOP.md defines the ordered procedure. Steps are parsed from the ## Steps section as a numbered list. Each item’s leading bold text is the step title; the rest is the body. Sub-bullets attach metadata.
## Steps
1. **Check readings** — Read sensor data and confirm it is within range. - tools: gpio_read, kumiho_memory_store
2. **Close valve** — Set GPIO pin 5 LOW. - tools: gpio_write - requires_confirmation: true
3. **Review** — Human review before proceeding. - kind: checkpointParser behavior:
- Numbered items (
1.,2., …) define step order. Numbering gaps raise a validation warning. - Leading bold text (
**Title**) becomes the step title; text after—is the step body. - tools:is a comma-separated list mapped to the step’ssuggested_tools(hints to the agent, not a hard allowlist).- requires_confirmation: trueforces an approval gate for that step, overriding the execution mode for that step only.- kind: checkpoint(vs the default- kind: execute) marks a step as a deterministic checkpoint — it pauses for approval when the SOP runs in deterministic mode.
SOP execution modes
Section titled “SOP execution modes”The execution mode controls how much autonomy the agent has between steps. A per-step requires_confirmation: true always overrides the mode for that step.
| Mode | Behavior |
|---|---|
auto | Execute all steps with no approval prompts. |
supervised | Require approval before the first step only. |
step_by_step | Require approval before every step. |
priority_based | critical / high SOPs run as auto; normal / low run as supervised. |
deterministic | No LLM round-trips. Steps run sequentially, each step’s output piped as the next step’s input; checkpoint steps pause for approval. |
SOP CLI commands
Section titled “SOP CLI commands”The revka sop subcommand manages definitions — it does not start runs. (Runs are started by triggers or the sop_execute agent tool.)
revka sop list # list all loaded SOPs with triggers and moderevka sop validate # validate all SOPs (warnings on empty fields, missing triggers/steps, numbering gaps)revka sop validate <name> # validate a single SOPrevka sop show <name> # detailed view of one SOP| Command | Purpose |
|---|---|
revka sop list | List every loaded SOP with its version, priority, mode, step count, triggers, and cooldown. |
revka sop validate [name] | Validate all SOPs, or one by name. Warns on empty name/description, missing triggers, missing steps, and step-numbering gaps. |
revka sop show <name> | Show full detail for a single SOP. |
Example revka sop list output:
SOPs (3): deploy-prod v1.0.0 [high] — Deploy service to production Mode: supervised Steps: 4 Triggers: webhook:/sop/deploy, manual Cooldown: 300s[sop] config
Section titled “[sop] config”The [sop] section in ~/.revka/config.toml governs the engine globally; per-SOP fields in SOP.toml refine behavior for a single procedure.
[sop]sops_dir = "sops"default_execution_mode = "supervised"max_concurrent_total = 4 # global limit across all SOPsapproval_timeout_secs = 300 # 5 minutes before approval-timeout handlingThe MQTT listener that would feed MQTT triggers is not currently wired into the daemon — see MQTT triggers for the caveat.
SOP trigger types
Section titled “SOP trigger types”An SOP fires when an incoming event matches one of its [[triggers]]. Five type values are supported.
[[triggers]]type = "mqtt"topic = "sensors/pressure"condition = "$.value > 85" # optional JSONPath condition
[[triggers]]type = "webhook"path = "/sop/deploy" # exact path match
[[triggers]]type = "cron"expression = "0 */5 * * *" # 5/6/7-field crontab
[[triggers]]type = "peripheral"board = "nucleo-f401re-0"signal = "pin_3"condition = "> 0" # optional numeric comparison
[[triggers]]type = "manual" # started via the sop_execute tool| Type | Fields | Notes |
|---|---|---|
manual | none | Started by the sop_execute tool (no CLI run command). |
webhook | path | Exact match against the request path. Defined in the engine but not yet wired to the gateway — see SOP webhook endpoints. |
mqtt | topic, optional condition | topic supports + (single-level) and # (multi-level) MQTT wildcards. |
cron | expression | 5, 6, or 7 fields; a 5-field expression has seconds prepended internally. |
peripheral | board, signal, optional condition | Matches the signal key "{board}/{signal}". |
Condition syntax
Section titled “Condition syntax”condition (on mqtt and peripheral triggers) is evaluated fail-closed: an invalid condition or a missing/unparseable payload yields no match rather than a match.
- JSONPath comparisons (typical for MQTT JSON payloads):
$.value > 85,$.status == "critical" - Direct numeric comparisons (typical for simple peripheral signals):
> 0,== 1 - Operators:
==,!=,>,<,>=,<=
MQTT (SOP event trigger)
Section titled “MQTT (SOP event trigger)”MQTT is intended as an IoT/automation fan-in: the MQTT SOP listener is not a chat channel — it is designed to route broker messages straight to the SOP engine rather than the agent chat loop.
The listener module sketches the broker settings it would consume — broker URL (mqtt:// plaintext / mqtts:// TLS), client ID, subscription topics, QoS (0/1/2), keep-alive seconds, optional username/password, and a use_tls flag that must agree with the scheme. None of these are loadable through config today.
The design intent is that the MQTT payload is forwarded into the SOP event payload (event.payload) and surfaced in step context, so an SOP step can read the triggering message, matched against a trigger condition like $.severity >= 2.
SOP webhook endpoints
Section titled “SOP webhook endpoints”The gateway exposes a single fixed POST /webhook route that handles inbound HTTP requests; it forwards the request to the agent chat loop and replies with {"response": ..., "model": ...}.
The webhook trigger type and its exact path-matching logic (path = "/sop/deploy" matches a request whose path is /sop/deploy) are defined in the SOP engine (match_trigger), but that matching is reachable only from tests.
Authentication
Section titled “Authentication”The POST /webhook route enforces these auth layers. When pairing is enabled (the default), supply the pairing bearer token; a webhook secret adds a second layer when configured:
POST /webhookAuthorization: Bearer <token>X-Webhook-Secret: <secret>X-Idempotency-Key: <unique-key>Content-Type: application/jsonAuthorization: Bearer <token>— pairing token fromPOST /pair(see Pairing & authentication).X-Webhook-Secret: <secret>— optional, required only when a webhook secret is configured.X-Idempotency-Key: <key>— optional dedup key. Default TTL 300s; a duplicate within the window returns200 OKwith"status": "duplicate". Keys share a single global store across the gateway.
Webhook routes are rate limited per client (webhook_rate_limit_per_minute, default 60).
For full webhook ingress details, see Webhook ingress.
Cron trigger evaluation
Section titled “Cron trigger evaluation”The scheduler evaluates cached cron triggers with a window-based check over (last_check, now], so no fire point is missed across a poll boundary, and a given expression dispatches at most once per tick even if several fire points fall in one window. Invalid cron expressions fail closed during cache build. Cron triggers require the daemon (revka daemon) to be running. For expression syntax and timezones, see Cron overview & expressions.
Fan-in routing
Section titled “Fan-in routing”The source defines a shared dispatcher dispatch_sop_event that routes events through the common match_trigger/start_run path, with convenience wrappers for MQTT (run_mqtt_sop_listener), cron (check_sop_cron_triggers), and peripheral (dispatch_peripheral_signal) sources.
When dispatch_sop_event is used (today: in tests, and intended for future wiring), it provides three consistent properties:
- One matcher path. Trigger matching uses the same
conditionand path/topic rules regardless of event source. - Run-start audit. Every started run is recorded by the audit logger before execution proceeds (see Audit logger).
- Headless safety. In a non-agent-loop context,
ExecuteStepactions are logged as pending rather than silently executed. Run an agent loop to driveExecuteStepsteps, or design the SOP to pause on approvals.
A single inbound event can match multiple SOPs; each one that matches will have a run started.
SOP run lifecycle & status
Section titled “SOP run lifecycle & status”Each run advances through up to seven statuses:
| Status | Meaning |
|---|---|
pending | Created, not yet started. |
running | Active execution. |
waiting_approval | Paused at an approval gate. |
paused_checkpoint | Paused at a deterministic checkpoint. |
completed | All steps completed successfully. |
failed | A step failed. |
cancelled | Cancelled by a user or the system. |
A run starts at pending, moves to running, and pauses at waiting_approval (mode/step gate) or paused_checkpoint (deterministic checkpoint) until an operator resolves it with sop_approve. Query live state with the sop_status tool.
SOP concurrency, cooldown, and approval timeout
Section titled “SOP concurrency, cooldown, and approval timeout”Three independent limits keep SOPs from overrunning:
# Global, in config.toml[sop]max_concurrent_total = 4 # across all SOPsapproval_timeout_secs = 300 # wait at an approval gate before timeout handling (0 = forever)# Per-SOP, in SOP.tomlcooldown_secs = 300 # minimum seconds between runs of this SOP (0 = none)max_concurrent = 1 # max simultaneous runs of this SOP| Scope | Field | Default | Meaning |
|---|---|---|---|
| Global | max_concurrent_total | 4 | Maximum simultaneous runs across all SOPs. |
| Global | approval_timeout_secs | 300 | Seconds at an approval gate before timeout handling (0 = no timeout). |
| Per-SOP | cooldown_secs | 0 | Minimum seconds between runs of one SOP. |
| Per-SOP | max_concurrent | 1 | Maximum simultaneous runs of one SOP. |
Approval-timeout behavior is priority-aware. When a gate exceeds approval_timeout_secs:
critical/highpriority SOPs auto-approve and continue (and the auto-approval is recorded in the audit trail).normal/lowpriority SOPs wait indefinitely for a manual decision.
SOP deterministic mode
Section titled “SOP deterministic mode”Deterministic mode runs an SOP as a pure pipeline with no LLM round-trips: steps execute sequentially and each step’s output is piped as the next step’s input. It is ideal for mechanical sequences (hardware actuation, fixed data transforms) where every step is fully specified and you want speed, repeatability, and zero token cost.
Enable it with either deterministic = true or execution_mode = "deterministic" in SOP.toml, then mark any human-review points as checkpoints in SOP.md:
[sop]name = "valve-shutdown"description = "Mechanical valve shutdown sequence"version = "1.0.0"priority = "critical"deterministic = true # forces deterministic mode
[[triggers]]type = "peripheral"board = "nucleo-f401re-0"signal = "pin_3"condition = "> 0"## Steps
1. **Read pressure** — Sample the pressure sensor. - tools: gpio_read
2. **Confirm shutdown** — Operator review before actuation. - kind: checkpoint
3. **Close valve** — Drive the valve closed. - tools: gpio_writeAt a checkpoint step the run pauses (paused_checkpoint) and persists state to a JSON file ({sop_dir}/{run_id}.state.json, or the system temp directory). Resume after review with the sop_approve tool. Each run reports llm_calls_saved, and the engine keeps a cumulative total across all deterministic runs.
SOP audit logger
Section titled “SOP audit logger”Every run start is recorded by SopAuditLogger into the configured Memory backend under category sop, giving you an auditable trail of which SOP fired, when, and from which event source. Common key patterns:
| Key pattern | Records |
|---|---|
sop_run_{run_id} | Run snapshot (start plus completion updates). |
sop_step_{run_id}_{step_number} | Per-step result. |
sop_approval_{run_id}_{step_number} | Operator approval record. |
sop_timeout_approve_{run_id}_{step_number} | Timeout auto-approval record (priority escalation). |
Inspect definitions with the CLI (revka sop list / show) and live run state with the sop_status tool. When [observability] backend = "prometheus", GET /metrics exposes the revka_* runtime metric families; SOP-specific aggregates are available through sop_status with include_metrics: true. See Audit log and Observability & tracing for the broader audit and metrics surface.
SOP agent tools
Section titled “SOP agent tools”Within an agent session, four MCP tools start and drive SOP runs:
| Tool | Purpose |
|---|---|
sop_execute <name> | Manually trigger an SOP by name (satisfies a manual trigger). |
sop_status <run_id> | Get the current run status and step results. Pass include_gate_status: true for trust-phase / gate-evaluator state, or include_metrics: true for SOP metric aggregates. |
sop_approve <run_id> | Approve a run paused at an approval gate or deterministic checkpoint. |
sop_advance <run_id> <result> | Report a step’s result and advance the run to the next step. |
These tools are documented alongside the rest of the agent’s scheduling and orchestration tooling in Scheduling & SOP tools.