Skip to content

Deterministic Runners

Deterministic runners let Obra execute native non-LLM stage work directly inside a workflow. They complement LLM stages instead of replacing them.

They are useful when a stage should call a program, hit an HTTP endpoint, or run trusted Python logic with clear inputs and outputs.

What they are

Deterministic runners are the workflow-native equivalent of "do a concrete operation now" steps:

  • command stages run a process with explicit args and environment
  • HTTP stages call an API with bounded capture and retry policy
  • Python stages execute a registered callable with structured kwargs
  • LLM review still decides whether the result is semantically good enough

Compared with other tools:

  • like n8n or Zapier, they can call systems directly
  • like LangChain or CrewAI, they stay inside one workflow runtime with stage state, review loops, artifacts, and lineage
  • unlike external shell glue, they are visible to the same gateway and Workflow Studio control plane as the rest of the workflow

Runner kinds

Kind Typical use case Activation source
command CLI tools, tests, linters, local scripts Command tuple
http SaaS APIs, internal services, webhooks Endpoint URL
python Trusted local adapters and structured helpers Factory import path
workflow_runner Delegate to a child workflow Runtime launch metadata

The gateway discovery route currently exposes these kinds:

curl http://127.0.0.1:18790/api/runners/kinds \
  -H "Authorization: Bearer <token>"

Today that list includes command, http, and python. workflow_runner is real runtime behavior, but it is documented from the runtime and ADR rather than the current gateway runner-kind discovery route.

Config schema discovery

Use the gateway as the source of truth for runner-config fields:

curl http://127.0.0.1:18790/api/runners/kinds/command/config-schema \
  -H "Authorization: Bearer <token>"

curl http://127.0.0.1:18790/api/runners/kinds/http/config-schema \
  -H "Authorization: Bearer <token>"

curl http://127.0.0.1:18790/api/runners/kinds/python/config-schema \
  -H "Authorization: Bearer <token>"

Each response includes runner_kind, a fields array, and example_config.

Command runner config

Field Type Notes
args list[str] Additional argv entries
env dict[str, str] Extra environment variables
cwd str \| null Working directory override
timeout_s int \| null Runner timeout override
success_exit_codes list[int] Exit codes treated as success
output_capture stdout_json \| stdout_text \| exit_code_only Output decoding mode

Example config:

{
  "args": ["-q", "--tb=short"],
  "env": {"PYTHONUNBUFFERED": "1"},
  "cwd": "/workspace/project",
  "timeout_s": 120,
  "success_exit_codes": [0],
  "output_capture": "stdout_text"
}

HTTP runner config

Field Type Notes
method GET \| POST \| PUT \| PATCH \| DELETE \| HEAD HTTP method
url str Target URL
headers dict[str, str] Request headers
body_template str \| null Body text or JSON template
timeout_s int \| null Request timeout override
success_codes list[int] Status codes treated as success
response_capture body_json \| body_text \| status_only Response capture mode
max_response_bytes int \| null Response truncation limit
rate_limit_rps int \| null Per-host rate limit
tls_verify bool TLS verification toggle
retry_policy dict \| null Retry overrides

Example config:

{
  "method": "POST",
  "url": "https://automation.example.com/hooks/claims",
  "headers": {
    "Content-Type": "application/json",
    "Authorization": "Bearer ${secrets.AUTOMATION_TOKEN}"
  },
  "body_template": "{\"invoice_id\": \"${stage_input.invoice_id}\"}",
  "timeout_s": 30,
  "success_codes": [200, 201, 202],
  "response_capture": "body_json",
  "max_response_bytes": 65536,
  "rate_limit_rps": 5,
  "tls_verify": true,
  "retry_policy": {
    "max_retries": 3,
    "backoff_base_s": 1.0,
    "backoff_max_s": 30.0,
    "retryable_status_codes": [429, 500, 502, 503, 504]
  }
}

Python runner config

Field Type Notes
timeout_s int \| null Execution timeout override
kwargs dict[str, any] Structured keyword arguments

Example config:

{
  "timeout_s": 60,
  "kwargs": {
    "report_kind": "claims_exception",
    "strict_mode": true
  }
}

Variable interpolation

String values in runner_config support ${namespace.path} interpolation.

Grammar:

${stage_input.invoice_id}
${workflow.working_directory}
${stage.validate_invoice.output.exit_code}
${secrets.API_KEY}
$${literal}

Namespaces:

Namespace Source Example
stage_input Current stage input payload ${stage_input.invoice_id}
workflow Workflow refs and runtime metadata ${workflow.working_directory}
stage.<name>.output Prior-stage output artifact metadata ${stage.validate_invoice.output.exit_code}
secrets OBRA_SECRET_<NAME> environment snapshot ${secrets.API_KEY}

Security rules:

  • interpolation is single-pass only
  • ${secrets.*} is allowed in headers and body templates
  • ${secrets.*} is rejected in HTTP URLs and command args
  • array indexing is not supported in v1
  • $${ escapes to a literal ${

Validate expressions through the gateway:

curl -X POST "http://127.0.0.1:18790/api/interpolation/validate?resolve=true" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "expression": "${stage.validate_invoice.output.exit_code}",
    "context_hint": {
      "available_stages": ["validate_invoice", "notify_finance"]
    }
  }'

Cross-stage data flow

Deterministic runner output is normalized into candidate_stage_output_artifacts. That matters because downstream stages read named prior-stage output through stage.<name>.output.* interpolation and the same artifact payload powers persistence, comparisons, and workflow-run detail.

In practice:

  • a deterministic stage emits structured metadata
  • the runtime persists it as stage output artifacts
  • downstream stages resolve that output by stage name, not by "previous stage"

Two-tier retry

Obra uses two retry layers:

  • runner-level retry handles transient transport or infrastructure failures such as HTTP 503, connection errors, or retryable exit codes
  • quality-loop retry handles semantic failures, where the output technically completed but still fails review criteria

The quality loop can feed numeric retry overrides back into the runner through the shared retry_with contract instead of forcing a manual reconfiguration.

LLM error interpretation

Deterministic stages still participate in the same review loop as LLM-backed stages. When a stage fails semantically:

  • the deterministic runner returns structured context
  • the reviewer sees the failure details and the emitted artifacts
  • Obra can revise the same stage, escalate, or continue with findings

This is the key difference from a plain shell script glued onto a workflow.

For-each batch execution

Stages can fan out over a list with for_each. The runtime resolves the source list once, runs the child work through the shared parallel executor, preserves item order in the aggregate result, and honors fail_fast or all_settle.

for_each:
  source: "${stage_input.invoices}"
  max_concurrency: 4
  failure_policy: "all_settle"

Sub-workflow invocation

workflow_runner launches child workflows through the gateway control plane, using POST /api/workflow-runs/ under the hood. It is intended for workflow decomposition, not for bypassing the workflow runtime.

Runtime notes:

  • the runner waits on child workflow terminal state through the same control plane
  • cycle detection uses propagated workflow_chain
  • recursion is bounded by explicit workflow_runner_max_depth
  • child lineage can be inspected through GET /api/workflow-runs/{run_id}/lineage

The current gateway runner-kind discovery route does not list workflow_runner, so document it as a runtime capability rather than a discovered kind.

Domain presets

Domains can ship runner catalog entries so common stage actions are available without bespoke factory wiring.

Preset id Kind Intended use
business.http.webhook HTTP Call business system webhooks
software.test.pytest Command Run targeted pytest verification
software.lint.ruff Command Run ruff lint checks

Discover the catalog-backed runners that are installed in the current environment:

curl http://127.0.0.1:18790/api/runners/ \
  -H "Authorization: Bearer <token>"