Loops

Loops

An loop is the durable process you build in Mobius. It has a name, a handle, a concurrency policy, one or more triggers, and an ordered list of steps. Every execution is a run against one frozen version of the loop.

Loop (latest_version, published_version)
  └── LoopVersion 1, 2, 3, ...
        └── LoopRun 1, 2, ...
              └── steps, events

What goes in a spec

Five things matter, in roughly this order of importance:

  1. Triggers. What starts a run.
  2. Steps. What the run does.
  3. Concurrency policy. What happens when a trigger fires while a prior run is in flight.
  4. Default agent and default inputs. Convenience defaults for steps that don't override.
  5. Tags. For organization (env: prod, owner: platform).

Here's a realistic spec:

name: Triage GitHub issues
handle: triage-issues
default_agent_id: agt_triager
settings:
  concurrency:
    policy: queue        # allow | forbid | replace | queue
    max: 5
triggers:
  - kind: event
    event_type: github.issues.opened
    config:
      repos: ["deepnoodle-ai/api"]
steps:
  - name: classify
    type: agent
    prompt: |
      Classify this issue. Return JSON: {label, severity, summary}.
      Issue: {{ .inputs.issue.title }}\n\n{{ .inputs.issue.body }}
    save_as: triage
  - name: label
    type: action
    action: github.add_label
    with:
      issue_number: "{{ .inputs.issue.number }}"
      label: "{{ .context.triage.label }}"

Two steps and one trigger. Most useful loops don't need much more shape than this.

Step types

TypeWhat it does
agentOne bounded agentic turn with tools.
actionOne deterministic call into a worker or platform action.
sleepSuspends the run for a time or duration.
wait_for_eventSuspends until a matching source event arrives.

Steps run in order. Each step's output lands in the run context under the step name (or save_as), so later steps can reference it like {{ .context.classify.summary }} or {{ .context.triage.label }}.

Picking a concurrency policy

This is the choice that catches most new users. The runtime applies the policy when a trigger fires while a previous run on the same loop is still active.

PolicyUse it when
allow (default)The work is independent per run. Event-driven triage usually wants this.
forbidA second run would be wasteful or destructive. Most nightly cleanups.
replaceOnly the latest input matters. "Sync to upstream"-style loops.
queueOrder matters and you want all of them to run. Cap with max.

If you don't know which to pick, default to allow and tighten it the first time you see overlapping runs cause trouble.

Versions

Every edit produces a new version. Versions are immutable: once a run starts on v3, it executes v3 forever, even if you publish v4 halfway through. That matters when you're rolling out a risky change.

The workflow:

# Inspect
mobius loops list
mobius loops get triage-issues
mobius loops list-versions triage-issues
 
# Iterate
mobius loops create-version triage-issues --file spec.yaml
mobius loops publish-loop-version triage-issues 4

Publishing flips the new version to published, sets it as the loop's published_version, and marks the previous published version superseded. Existing runs keep running on whatever version they started.

Status

Loops have four statuses:

StatusMeaning
draftNo published version yet. You can still start test runs against the latest draft.
activePublished version executes on triggers and API starts.
pausedTriggers are suppressed. In-flight runs keep going.
archivedSoft-deleted. History is readable; new runs are rejected.

Pause a loop before doing a risky change. Archive it when it's genuinely done.

Common CLI operations

mobius loops list
mobius loops get triage-issues
mobius runs list --loop-id aut_01...
mobius runs start triage-issues --inputs @issue.json
mobius loops update triage-issues --file spec.yaml
mobius loops delete triage-issues          # archives, doesn't drop history
  • Runs for the execution model.
  • Triggers for how runs get started.
  • Actions for the call-out side.