Skip to content

Executors

Executors are what happen when a transition fires. You declare them in YAML. The gateway handles the wiring.

KindPurpose
noopReturns immediately. Great for stubs and testing.
cliRuns a shell command, captures stdout.
restMakes an HTTP request.
mcpCalls a tool on a connected MCP server.
humanQueues for human action, emits an audit event.
workflowStarts a sub-workflow (internal).

You can also reference a named capability instead of declaring an executor inline:

executor:
capability: github.list_issues

This resolves at config-load time to the named capability’s executor and merges its guards and reliability into the calling context.


Returns immediately with no side effects. The input is echoed back as output.

executor:
kind: noop

Use it for:

  • Stubbing out capabilities while you build the workflow
  • Testing the gateway without real backends
  • Transitions that only need to move state (no external call)

Runs a shell command through a CLI connection. Captures stdout as the executor’s output.

executor:
kind: cli
connection: dotnet
args:
- test
- "$.arguments.project"

The connection references a named CLI connection from your config. The args array supports path expressions that resolve against the workflow context, input, and arguments.

If you don’t need a named connection, you can specify the command directly:

executor:
kind: cli
command: lint-check
args: ["$.input.service"]

The executor captures stdout as a string. If stdout is valid JSON, it’s parsed and available at $.output.json.* in output mappings. If it’s plain text, it’s available at $.output.text.

CLI executors benefit most from reliability policies. Network-free commands can still hang or flake:

executor:
kind: cli
connection: dotnet
args: [test]
reliability:
timeoutMs: 120000
retry:
maxAttempts: 3
backoff: exponential
initialDelayMs: 1000
maxDelayMs: 10000
retryOn: [timeout, transient_error]
fallback:
strategy: first_success
executors:
- kind: cli
command: dotnet
args: [test, --no-build]

That config says: try the test command, retry up to 3 times with exponential backoff if it times out or fails transiently, and if all retries fail, try a fallback command.


Makes an HTTP request through a REST connection.

executor:
kind: rest
connection: payroll
method: POST
path: /reimbursements
body:
employee: "$.workflow.input.employee"
amount: "$.workflow.input.amount"
currency: "$.workflow.input.currency"

The connection references a named REST connection, which provides the base URL and default headers:

connections:
payroll:
kind: rest
baseUrl: https://payroll.example.com
headers:
Authorization: "Bearer ${PAYROLL_TOKEN}"

The path supports variable interpolation from workflow context:

executor:
kind: rest
connection: docs
method: PUT
path: "/documents/{documentId}"
body:
content: "$.arguments.revisedDraft"

Path variables in {braces} are resolved from the workflow context.

Per-request headers can be added alongside the connection’s default headers:

executor:
kind: rest
connection: api
method: POST
path: /submit
headers:
X-Request-Id: "$.context.correlationId"
body:
data: "$.arguments.payload"

The body object supports path expressions. Each value is resolved at execution time:

Path prefixResolves to
$.workflow.input.*The input passed when the workflow was started
$.context.*The workflow’s accumulated context
$.arguments.*The arguments passed to the current transition

For retried requests, set idempotencyKey to prevent duplicate side effects:

executor:
kind: rest
connection: payroll
method: POST
path: /reimbursements
body:
employee: "$.workflow.input.employee"
amount: "$.workflow.input.amount"
idempotencyKey: true
reliability:
retry:
maxAttempts: 3
backoff: exponential
initialDelayMs: 1000
retryOn: [transient_error, timeout, rate_limited, connection_error]

When idempotencyKey is true, the gateway auto-derives a key from workflowId + transition + correlationId. Same key across retries, so your backend can deduplicate. You can also provide a custom template string:

idempotencyKey: "{workflowId}-{transition}-{correlationId}"

Calls a tool on a connected MCP server.

executor:
kind: mcp
connection: github
tool: list_issues
map:
repo: "$.arguments.repo"

References a named MCP connection (stdio or SSE):

connections:
github:
kind: mcp
command: github-mcp-server

The tool field is the tool name on the remote MCP server. The map object maps workflow data to the tool’s expected arguments:

executor:
kind: mcp
connection: planner
tool: normalize_plan
map:
goal: "$.workflow.input.goal"
plan: "$.arguments.plan"

Each key in map becomes an argument to the remote tool. Values are path expressions resolved at execution time.

The remote tool’s response is available in output mappings at $.output.*:

executor:
kind: mcp
connection: risk
tool: fmeca_analyze
map:
plan: "$.context.plan"
output:
fmeca: "$.output"
maxRpn: "$.output.maxResidualRpn"

Queues a transition for human action. The executor doesn’t complete the transition — it records an audit event and returns a “pending” status. A human watching the queue makes the actual decision.

executor:
kind: human
queue: engineering-approvals

The queue is a logical name. It shows up in audit events so your approval system knows where to route the request.

  1. The model calls workflow.submit with a human executor transition.
  2. The gateway records a human.approval.requested audit event (or similar) with the queue name.
  3. The workflow stays in its current state, waiting.
  4. A human uses a separate interface (your approval tool, a dashboard, a Slack bot) to review and submit the actual transition.

The human executor stops the model from completing the action. The actor: human gate stops the model from even submitting it. Use both for belt-and-suspenders:

transitions:
approve:
title: Approve the change
actor: human # only humans can submit this
target: approved
guards:
- { kind: permission, permission: workflow.approve }
executor:
kind: human # and the action itself waits for a human
queue: approvals

Starts a sub-workflow. This is used internally when a transition needs to kick off a separate workflow definition.

executor:
kind: workflow
definitionId: sub_process

The sub-workflow runs as its own instance with its own state and context. The parent transition waits for it to complete.


Any executor can have a reliability policy attached. You define it alongside the executor in a transition or capability.

reliability:
timeoutMs: 30000 # kill the executor if it takes longer than 30s
retry:
maxAttempts: 3 # try up to 3 times total
backoff: exponential # none | fixed | exponential
initialDelayMs: 1000 # first retry after 1s
maxDelayMs: 8000 # cap delay at 8s
retryOn: # which failures trigger a retry
- timeout
- transient_error
- rate_limited
- connection_error
fallback:
strategy: first_success # try fallback executors in order
executors:
- kind: cli
command: backup-command
- kind: noop # last resort: return empty
ConditionWhen it applies
timeoutExecutor exceeded its time limit
transient_errorTemporary failure (e.g. HTTP 503)
rate_limitedBackend returned a rate limit response
connection_errorCouldn’t reach the backend at all

If all retry attempts fail, the gateway tries each fallback executor in order. The first one that succeeds wins. This lets you degrade gracefully — try the primary, fall back to a simpler version, fall back to noop.

When idempotencyKey is set on an executor, the same key is used across retries and fallback candidates. Your backend can use this to deduplicate requests even if the gateway switches to a fallback executor.