Operate

Safety and usage

Guardrails are the limits you put on a job so that when something goes sideways it stops for a reason you can read, rather than running all night.

You'd give a new hire a company card with a limit on it. Same idea. Not because you expect trouble, but because a stuck job at 2am should cost you nothing and leave a clear note about what happened.

The two-minute version

If you read nothing else on this page, do these two things to every job that runs without a person watching:

  1. Set a spending cap. limits.budget_usd: 5, or whatever's sensible.
  2. Set a time limit. limits.wall_clock_timeout: 30m.

That's thirty seconds of work and it's the difference between a surprise on your bill and a stopped job with budget_exceeded written on it.

Then, as the job does more:

  1. Put a deadline on anything that waits for a person.
  2. Put an approval in front of anything that reaches a client.
  3. Add a check before anything you can't take back.

What each guardrail is for

The worryWhat handles itWhere you see it working
This could get expensivePer-run budgets, and per-day budgets across a whole jobThe budget bar on the run, run.budget_exceeded
This could run foreverwall_clock_timeout, wait timeouts, retry limitsThe stop reason and the step's status
The agent could go round in circlesmax_agent_turns, and max_turns on a stepTurn count, and the stop reason turn_limit_reached
It said it worked, but did it?check steps, with real evidencecheck.passed, check.failed, and any approval it opened
This job keeps failingRepeat-call and consecutive-failure breakersrun.progress_stalled, loop.auto_paused, the loop showing paused

One thing worth internalising: a guardrail stop is not a failure. When a run ends with budget_exceeded or wall_clock_exceeded, the limit you set did exactly its job. Read it as good news about the limit, then go find out why the job needed more than you expected.

Configure run budgets

Run budgets live in spec.limits. Use dollars when you think in account spend, or credits when you think in Mobius usage. One credit is $0.01, and you set exactly one unit.

limits:
  budget_usd: 10
limits:
  credit_budget: 1000

Mobius shows both units in the run budget rail, and reports credits as decimals so a sub-credit operation reads as 0.4 rather than rounding to zero. The budget is a hard limit. When spend reaches the ceiling, Mobius halts at the next checkpoint with stop_reason: budget_exceeded and emits run.budget_exceeded.

Mobius checks the budget between operations, not in the middle of one. It won't kill a call halfway through and leave you with a half-finished mess. In practice that means a run with a 1000-credit budget can finish at 1000 credits plus whichever call was already in flight, so don't set a cap so tight that one extra call matters.

Mobius does not add a synthetic per-run budget when you omit one. Available organization funds can still stop spending, but that is not a safe substitute for a limit on one run. Set an explicit budget on every unattended loop, which is the sentence to remember from this page.

Cap the whole job's daily spend

A per-run budget bounds one execution. If a job runs every five minutes, or fires on every inbound email, you also want a cap across all of them:

limits:
  daily_budget_usd: 25
limits:
  daily_credit_budget: 2500

This is a rolling 24-hour window across every run of that job. Mobius checks it before starting a run: if the window is already used up, the run is refused with billing_cap_reached and kind loop_daily_budget. It also checks mid-run, and halts with budget_exceeded if the ceiling gets crossed while work is in progress.

Set one on anything driven by a schedule or a busy event source. A ticket triage job is one bad forwarding rule away from a thousand runs, and the daily budget is what turns that from a bill into an alert.

If you bring your own model key

Skip this section if you're using Mobius's included models.

Bring-your-own-key (BYOK) means two bills: your AI provider charges you directly for the model usage, and Mobius charges credits for the platform work around it. Mobius budgets only govern the Mobius side.

BudgetCounts BYOK spend?Why
Per-run budget, budget_usd or credit_budgetYes, Mobius processing creditsA run budget measures Mobius consumption inside the run. Provider-side invoices stay outside Mobius.
Per-loop daily budget, daily_budget_usd or daily_credit_budgetYes, Mobius processing creditsA daily loop window measures platform-billed spend, including BYOK processing.

The practical result: a BYOK run can halt mid-run when its per-run budget is reached, because that budget measures Mobius-billed processing. A BYOK start can also be refused when platform-billed spend has already exhausted the loop's daily window. Your provider's direct bill is not counted in either Mobius budget.

Put a clock on it

limits.wall_clock_timeout is the overall time limit for one run:

limits:
  wall_clock_timeout: 30m

When the deadline passes, Mobius stops the run with stop_reason: wall_clock_exceeded. This catches the case where something is still technically busy but never going to finish, which is the case a spending cap alone won't catch.

Retries handle the boring failures, a network blip or a provider having a bad minute:

retry:
  max_attempts: 3
  delay: 30s

max_attempts counts every attempt, so 1 means no retry at all. The maximum is 10.

Warning: Retries do not make a destructive action safe. Mobius can run the step again; it can't unsend the email. Use the provider's duplicate protection where it exists, or put an approval in front.

Anything that waits should have its own deadline:

timeout:
  duration: 2h
  on_timeout: fail

Right now on_timeout supports fail. When a wait times out, Mobius emits wait.timed_out, fails the step, and fails the run. Cancelling an interaction also closes the wait, so an obsolete run doesn't sit in suspended forever.

Stop the agent going round in circles

limits.max_agent_turns caps how much thinking happens across the whole run:

limits:
  max_agent_turns: 20

Worth setting when a job has several agent steps, retries, or checks that involve an agent. It's separate from a step's own max_turns, which limits tool use inside one turn. Hit the run-wide cap and Mobius stops with stop_reason: turn_limit_reached.

Checks: prove it, don't take its word for it

Here's the failure mode this exists for. You ask an agent to fix something, then you ask the same agent whether it worked. It says yes. It is not lying; it's just a poor judge of its own work.

A check step evaluates a condition against real evidence and records the verdict. Put it after the thing that produces the evidence and before the thing that depends on it:

steps:
  - id: test
    kind: action
    config:
      action_name: ci.run_tests
      parameters:
        repo: acme/api
 
  - id: verify
    kind: check
    config:
      checks:
        - name: tests_passed
          kind: expr
          expr: steps.test.output.exit_code == 0
          evidence: [test]
        - name: review_summary
          kind: agent
          prompt: "Decide whether the test output supports shipping this change."
          evidence: [test]
      on_fail: gate
      gate:
        targets: ["user:lead@acme.example"]
        prompt: "Review the failed checks before this run continues."

kind: expr is an exact test: this value equals that value. Use it whenever you can, because it always gives the same answer. kind: agent asks a separate agent for a yes-or-no verdict with a reason, which is for the cases you can't write as a rule. Leave agent out and Mobius uses its own reviewer, mobius-reviewer.

Check data, not narrative. "The test action exited 0" is a real check. "Ask the agent that did the work whether it went well" is not. Prefer action results, files, logs, and returned records over anything an agent said about itself.

on_fail decides what happens when a check goes red:

on_failResult
failStop the run with stop_reason: check_failed.
continueContinue the run and keep the red verdict on the timeline.
gateOpen a request_approval interaction carrying the failed assertions and evidence. Approval resumes the run; rejection stops it with gate_rejected.

Breakers, for when something is properly stuck

Two automatic ones, for the failure modes that a budget catches too slowly.

An agent that keeps making the exact same tool call with the exact same arguments is stuck, not working. The repeat-call breaker stops it:

limits:
  max_duplicate_tool_calls: 10

The run stops with progress_stalled and emits run.progress_stalled.

A job that has failed several times in a row probably has something wrong with it, and running it again at 7am tomorrow won't help. The circuit breaker pauses the whole job:

limits:
  pause_after_consecutive_failures: 3

A successful run resets the count. Cancelled runs don't count either way. When it trips, Mobius pauses the job, emits loop.auto_paused, and refuses to start anything new until a person un-pauses it.

That last part is deliberate. A paused job is a thing you have to look at, which is exactly what you want after three failures in a row.

Setting these in the app

In the loop editor, open Settings and set Budget and Turns. Those are the two that matter most and they're right there.

The run page shows the budget bar, the stop reason, what each step cost, the evidence behind any checks, and a banner explaining the stop.

Daily job budgets, the repeat-call threshold, and the circuit-breaker threshold don't have app controls yet. Set those through the spec or the API for now.

From the CLI

Use the CLI to start and watch guarded runs:

mobius runs start morning-brief --inputs '{"scope":"today"}'
mobius runs stream run_01...

The CLI can inspect loops, start runs, stream events, and cancel runs. Use the loops command group; the old automations group has been removed. The app or HTTP API remains the clearest surface for editing a complete guardrail configuration.

From the API

Send guardrails under limits in the authored spec:

POST /v1/projects/{project}/loops/{id}/versions

You can also override the per-run budget at start:

POST /v1/projects/{project}/loops/{id}/runs
{
  "inputs": {
    "scope": "today"
  },
  "budget_usd": 2.5
}

Start-request budgets affect only that run. They do not change the loop's published spec or its daily budget.

Stop reasons

stop_reason explains why a terminal run stopped:

Stop reasonMeaning
completedThe run finished successfully.
step_failedA step failed without remaining retries.
check_failedA check failed and on_fail: fail stopped the run.
gate_rejectedA check gate or interaction was rejected.
cancelledA user or system cancelled the run.
replacedA concurrency policy replaced the run.
wall_clock_exceededThe run exceeded limits.wall_clock_timeout.
budget_exceededA run budget or loop daily budget halted the run.
turn_limit_reachedThe run exceeded limits.max_agent_turns.
progress_stalledThe duplicate-tool-call breaker halted the run.
step_limit_reachedThe run exceeded the plan's per-run step cap.

When a run surprises you, read the stop reason first. Then open the timeline event that caused it. In that order; it saves a lot of guessing.

What to keep an eye on

  • Jobs hitting run.budget_exceeded. Once is informative; every day means the budget is wrong or the job is.
  • check.failed, grouped by which check. A check that never fails isn't telling you anything.
  • Jobs that auto-paused, from loop.auto_paused.
  • Runs sitting in suspended far longer than the wait should take. Usually a person who never saw the request.
  • Your artifact storage, before turning on anything that generates big files.

Before you leave a job unattended

  1. Set a per-run budget. Every job that uses an AI model or a metered action.
  2. Set limits.wall_clock_timeout.
  3. Set a deadline on every waiting step, including approvals.
  4. Keep retry counts low until you're sure the action is safe to repeat.
  5. Add a check before anything you can't take back.
  6. Add a daily budget if it runs on a schedule or a busy event.
  7. Watch the first few real runs on the timeline before widening the scope.

FAQ

Why can a run spend slightly more than its budget?

Mobius enforces budgets at checkpoints. A checkpoint happens before the next model call, after a metered action records usage, at step boundaries, and between agent tool iterations. The call already in flight is allowed to finish, so the final spend can include one extra call or action.

Should I use a check or an approval?

A check is when Mobius can work the answer out from evidence. An approval is when a person has to decide.

When you're not sure, use a check with on_fail: gate. Mobius evaluates the evidence itself and only interrupts a human when something looks wrong, which is the right default for anything running at volume.

What's a sensible starting budget?

For a job with one agent step and a couple of actions, a dollar a run is generous. Set that, run it a few times, then look at what it actually cost on the run page and set something closer to reality.

Too tight is a nuisance; too loose defeats the point. Erring loose for the first week and then tightening is the usual approach.

Next