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)All event sources — MQTT, webhook, cron, and peripheral — funnel through one unified dispatcher (dispatch_sop_event), so trigger matching, run-start auditing, and headless-safety behavior are identical no matter how a run starts.
Event sources unified dispatcher MQTT message (topic match) │ POST /sop/* or /webhook (path match) │ Scheduler (window check) ─────┼──▶ dispatch_sop_event ──▶ SopEngine ──▶ SOP Run ──▶ Action Peripheral signal (board/signal) │ │ sop_execute tool (manual) │ ┌────────────────────────┴───────────┐ ExecuteStep WaitApproval │ │ ▼ ▼ Agent loop Operator │ sop_approve ▼ SOP Run (resumes)Enable the SOP subsystem
Section titled “Enable the SOP subsystem”SOPs are off until you enable them in ~/.revka/config.toml:
[sop]enabled = truesops_dir = "sops" # relative to the workspace; defaults to <workspace>/sopsdefault_execution_mode = "supervised"| Key | Type | Default | Meaning |
|---|---|---|---|
enabled | bool | false | Master switch for the SOP engine. |
sops_dir | string | <workspace>/sops | Directory scanned for SOP subdirectories. |
default_execution_mode | string | supervised | Mode applied to SOPs that don’t set their own execution_mode. |
max_concurrent_total | int | 10 | Global cap on simultaneous runs across all SOPs. |
approval_timeout_secs | int | 3600 | 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]enabled = truesops_dir = "sops"default_execution_mode = "supervised"max_concurrent_total = 10 # global limit across all SOPsapproval_timeout_secs = 3600 # 1 hour before approval-timeout handlingThe MQTT broker that feeds MQTT triggers is configured separately under [channels_config.mqtt] — see MQTT triggers.
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 (/sop/... or /webhook). |
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 the primary IoT/automation fan-in. The MQTT subscriber is not a chat channel — it routes broker messages straight to the SOP engine rather than the agent chat loop. Configure the broker under [channels_config.mqtt]:
[channels_config.mqtt]broker_url = "mqtts://broker.example.com:8883" # use mqtt:// for plaintextclient_id = "revka-agent-1"topics = ["sensors/alert", "ops/deploy/#"]qos = 1 # 0 | 1 | 2keep_alive_secs = 60username = "mqtt-user" # optionalpassword = "mqtt-password" # optionaluse_tls = true # must match the scheme (mqtts:// => true)| Key | Type | Default | Meaning |
|---|---|---|---|
broker_url | string | — (required) | Broker URL; mqtts:// selects TLS, mqtt:// plaintext. |
client_id | string | — (required) | MQTT client identifier. |
topics | list | — | Subscriptions; + and # wildcards supported. |
qos | int | 1 | Quality of service: 0 at-most-once, 1 at-least-once, 2 exactly-once. |
keep_alive_secs | int | 60 | Keep-alive interval. |
username / password | string | — | Optional broker auth. |
use_tls | bool | false | Enable TLS; must agree with the broker_url scheme. |
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. Match against it with a trigger condition like $.severity >= 2.
SOP webhook endpoints
Section titled “SOP webhook endpoints”Two gateway routes can start SOP runs over HTTP:
| Method + path | Behavior |
|---|---|
POST /sop/{*rest} | SOP-only. Matches the request path against webhook triggers. Returns 404 if no SOP matches — there is no LLM fallback. |
POST /webhook | Chat endpoint with SOP-first dispatch. Tries SOP matching first; if nothing matches, falls back to the normal LLM flow. |
Path matching is exact against the configured trigger path. A trigger path = "/sop/deploy" matches POST /sop/deploy.
Authentication
Section titled “Authentication”When pairing is enabled (the default), supply the pairing bearer token; a webhook secret adds a second layer when configured:
POST /sop/deployAuthorization: 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 are namespaced per endpoint (/webhookvs/sop/*).
Webhook routes are rate limited per client (webhook_rate_limit_per_minute, default 60).
Example request and response
Section titled “Example request and response”curl -X POST http://127.0.0.1:42617/sop/deploy \ -H "Authorization: Bearer <token>" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"message":"deploy-service-a"}'{ "status": "accepted", "matched_sops": ["deploy-pipeline"], "source": "sop_webhook", "path": "/sop/deploy"}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”Every source — MQTT, webhook, cron, peripheral, and the sop_execute tool — converges on the single dispatcher dispatch_sop_event, which gives the system three consistent guarantees:
- One matcher path. Trigger matching is identical regardless of source, so the same
conditionand path/topic rules apply everywhere. - 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 (for example, a webhook arriving with no active agent turn),
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; the response’s matched_sops lists each one that 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 = 10 # across all SOPsapproval_timeout_secs = 3600 # 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 | 10 | Maximum simultaneous runs across all SOPs. |
| Global | approval_timeout_secs | 3600 | 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.