Skip to content

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.

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)

SOPs are off until you enable them in ~/.revka/config.toml:

[sop]
enabled = true
sops_dir = "sops" # relative to the workspace; defaults to <workspace>/sops
default_execution_mode = "supervised"
KeyTypeDefaultMeaning
enabledboolfalseMaster switch for the SOP engine.
sops_dirstring<workspace>/sopsDirectory scanned for SOP subdirectories.
default_execution_modestringsupervisedMode applied to SOPs that don’t set their own execution_mode.
max_concurrent_totalint10Global cap on simultaneous runs across all SOPs.
approval_timeout_secsint3600Seconds to wait at an approval gate before timeout handling (0 = wait forever).

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 | critical
execution_mode = "supervised" # auto | supervised | step_by_step | priority_based | deterministic
cooldown_secs = 300
max_concurrent = 1
deterministic = false # true forces execution_mode = deterministic
[[triggers]]
type = "webhook"
path = "/sop/deploy"
[[triggers]]
type = "manual"
FieldTypeDefaultMeaning
namestring— (required)Unique SOP identifier, used by sop_execute and the CLI.
descriptionstring— (required)Human-readable purpose.
versionstring"0.1.0"Semantic version of the procedure.
prioritystringnormalOne of low, normal, high, critical. Drives priority_based mode and approval-timeout escalation.
execution_modestring([sop].default_execution_mode)One of auto, supervised, step_by_step, priority_based, deterministic. See Execution modes.
cooldown_secsint0Minimum seconds between runs of this SOP (0 = no cooldown).
max_concurrentint1Maximum simultaneous runs of this SOP.
deterministicboolfalseWhen 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.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: checkpoint

Parser 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’s suggested_tools (hints to the agent, not a hard allowlist).
  • - requires_confirmation: true forces 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.

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.

ModeBehavior
autoExecute all steps with no approval prompts.
supervisedRequire approval before the first step only.
step_by_stepRequire approval before every step.
priority_basedcritical / high SOPs run as auto; normal / low run as supervised.
deterministicNo LLM round-trips. Steps run sequentially, each step’s output piped as the next step’s input; checkpoint steps pause for approval.

The revka sop subcommand manages definitions — it does not start runs. (Runs are started by triggers or the sop_execute agent tool.)

Terminal window
revka sop list # list all loaded SOPs with triggers and mode
revka sop validate # validate all SOPs (warnings on empty fields, missing triggers/steps, numbering gaps)
revka sop validate <name> # validate a single SOP
revka sop show <name> # detailed view of one SOP
CommandPurpose
revka sop listList 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

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 = true
sops_dir = "sops"
default_execution_mode = "supervised"
max_concurrent_total = 10 # global limit across all SOPs
approval_timeout_secs = 3600 # 1 hour before approval-timeout handling

The MQTT broker that feeds MQTT triggers is configured separately under [channels_config.mqtt] — see MQTT triggers.

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
TypeFieldsNotes
manualnoneStarted by the sop_execute tool (no CLI run command).
webhookpathExact match against the request path (/sop/... or /webhook).
mqtttopic, optional conditiontopic supports + (single-level) and # (multi-level) MQTT wildcards.
cronexpression5, 6, or 7 fields; a 5-field expression has seconds prepended internally.
peripheralboard, signal, optional conditionMatches the signal key "{board}/{signal}".

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 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 plaintext
client_id = "revka-agent-1"
topics = ["sensors/alert", "ops/deploy/#"]
qos = 1 # 0 | 1 | 2
keep_alive_secs = 60
username = "mqtt-user" # optional
password = "mqtt-password" # optional
use_tls = true # must match the scheme (mqtts:// => true)
KeyTypeDefaultMeaning
broker_urlstring— (required)Broker URL; mqtts:// selects TLS, mqtt:// plaintext.
client_idstring— (required)MQTT client identifier.
topicslistSubscriptions; + and # wildcards supported.
qosint1Quality of service: 0 at-most-once, 1 at-least-once, 2 exactly-once.
keep_alive_secsint60Keep-alive interval.
username / passwordstringOptional broker auth.
use_tlsboolfalseEnable 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.

Two gateway routes can start SOP runs over HTTP:

Method + pathBehavior
POST /sop/{*rest}SOP-only. Matches the request path against webhook triggers. Returns 404 if no SOP matches — there is no LLM fallback.
POST /webhookChat 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.

When pairing is enabled (the default), supply the pairing bearer token; a webhook secret adds a second layer when configured:

POST /sop/deploy
Authorization: Bearer <token>
X-Webhook-Secret: <secret>
X-Idempotency-Key: <unique-key>
Content-Type: application/json
  • Authorization: Bearer <token> — pairing token from POST /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 returns 200 OK with "status": "duplicate". Keys are namespaced per endpoint (/webhook vs /sop/*).

Webhook routes are rate limited per client (webhook_rate_limit_per_minute, default 60).

Terminal window
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.

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.

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 condition and 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), ExecuteStep actions are logged as pending rather than silently executed. Run an agent loop to drive ExecuteStep steps, 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.

Each run advances through up to seven statuses:

StatusMeaning
pendingCreated, not yet started.
runningActive execution.
waiting_approvalPaused at an approval gate.
paused_checkpointPaused at a deterministic checkpoint.
completedAll steps completed successfully.
failedA step failed.
cancelledCancelled 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 SOPs
approval_timeout_secs = 3600 # wait at an approval gate before timeout handling (0 = forever)
# Per-SOP, in SOP.toml
cooldown_secs = 300 # minimum seconds between runs of this SOP (0 = none)
max_concurrent = 1 # max simultaneous runs of this SOP
ScopeFieldDefaultMeaning
Globalmax_concurrent_total10Maximum simultaneous runs across all SOPs.
Globalapproval_timeout_secs3600Seconds at an approval gate before timeout handling (0 = no timeout).
Per-SOPcooldown_secs0Minimum seconds between runs of one SOP.
Per-SOPmax_concurrent1Maximum simultaneous runs of one SOP.

Approval-timeout behavior is priority-aware. When a gate exceeds approval_timeout_secs:

  • critical / high priority SOPs auto-approve and continue (and the auto-approval is recorded in the audit trail).
  • normal / low priority SOPs wait indefinitely for a manual decision.

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_write

At 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.

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 patternRecords
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.

Within an agent session, four MCP tools start and drive SOP runs:

ToolPurpose
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.