Build
Steps
A step is one bounded unit of work inside a loop run. Steps execute in order, checkpoint their state, and write output into run context for later steps.
run input
step classify kind=agent
output saved as triage
step notify kind=action
reads context.triage
step approval kind=interaction
suspends until a response resolves it
run outputMost loops should start with one agent step or one action step. Add
suspending steps only when time, another system, or a person actually needs to
answer before the run continues.
Step types
| Step type | What it does | Use when |
|---|---|---|
agent | Runs one bounded agent turn. The agent receives instructions, may call tools, and returns output. | The run needs judgment, synthesis, or tool use. |
action | Calls one deterministic action with rendered parameters. It does not start an LLM turn. | The run needs an exact side effect, such as posting to Slack or writing a table row. |
sleep | Suspends until a duration or timestamp. | Time is the condition. |
wait_for_event | Suspends until a matching source event arrives. | Another system will send the answer as an event. |
interaction | Creates an interaction and suspends until the resolution policy resolves. | A human or agent must approve, review, or answer. |
loop | Starts another loop in the same project and completes after the child run is created. | One loop should hand work to another loop without waiting for its result. |
Mobius also materializes cleanup steps from cleanup configuration. Authored
loop specs cannot include cleanup in steps; the compiler rejects it so user
steps and system cleanup stay separate.
The step shape
Every authored step has key, kind, and config. Optional fields control
rendered input, retries, timeout behavior, and where output is saved.
steps:
- key: classify
name: Classify issue
kind: agent
input:
title: "{{ .inputs.event.issue.title }}"
body: "{{ .inputs.event.issue.body }}"
config:
agent_id: agt_triager
instructions: |
Classify the issue. Return JSON with label, severity, and summary.
output_schema:
type: object
required: [label, severity, summary]
retry:
max_attempts: 2
delay: 30s
timeout:
duration: 10m
on_timeout: fail
save_as: triagekey is stable inside the loop spec. name is display copy. kind selects
which config parser the compiler uses. config is different for each step
type.
Inputs and templates
Step input is rendered when the step starts. String leaves may use Go
text/template expressions against the run input and prior step context:
input:
issue_title: "{{ .inputs.event.issue.title }}"
triage_label: "{{ .context.triage.label }}"Use templates for values that change per run. Keep long instructions in the
step config, and pass small structured values through input.
Event triggers normalize their payload under inputs.event and routing facts
under inputs.meta. That means table events, provider events, email events,
and HTTP trigger events all give steps one consistent place to read from.
Saved output
Each step produces an output. Mobius stores that output under save_as; if
save_as is empty, it uses the step key.
steps:
- key: summarize
kind: agent
save_as: brief
config:
agent_id: agt_scout
instructions: "Summarize the run input in markdown."
- key: post
kind: action
config:
action_name: slack.message.post
parameters:
channel: "#eng"
text: "{{ .context.brief }}"Agent transcripts are not shared between steps by default. If one agent's work should guide a later step, have the producing step return a compact structured value and save it in context. This keeps later prompts inspectable and avoids dragging an entire transcript through the run.
Retries
retry.max_attempts is the total attempt count. 1 means no retry, and the
server caps the value at 10.
retry:
max_attempts: 3
delay: 30sRetries are for transient errors. Mark destructive actions carefully and make them idempotent, because Mobius cannot roll back a Slack message, GitHub label, or database write after an earlier attempt already reached the outside system.
Timeouts
timeout.duration bounds a step. Today timeout.on_timeout supports fail.
timeout:
duration: 15m
on_timeout: failFor sleep, wait_for_event, and interaction, the timeout prevents a
suspended run from waiting forever. For a run-level bound, use
defaults.wall_clock_timeout; that stamps a run deadline and lets the reaper
fail a run even if a step executor is still busy.
Suspending steps
Three step types can intentionally suspend a run:
| Step type | Resume condition | Timeout result |
|---|---|---|
sleep | The timer expires. | The run resumes by timer or fails if the run deadline passes first. |
wait_for_event | A source event matches event_type, optional source_id, match, and condition. | The step fails with wait_timed_out. |
interaction | The interaction reaches completed. | The step fails if its timeout expires or the interaction is cancelled. |
A run in suspended is not stuck. It is waiting on a timer, event, worker job,
or interaction. The run timeline tells you which step owns the wait.
Child loops
A loop step starts another loop in the same project:
steps:
- key: start-review
kind: loop
config:
loop_handle: review-every-pull-request
inputs:
pull_request: "{{ .inputs.event.pull_request }}"The child run records parent lineage back to the parent run and step. The
parent step completes once the child run is created, so use this shape when
fire-and-forget is acceptable. If the parent needs the child result, wait for a
source event such as run.completed instead.
Next
- Put steps in a versioned definition with loops.
- Start runs with triggers.
- Pause for people with interactions.
- Inspect execution from runs.