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:
- Set a spending cap.
limits.budget_usd: 5, or whatever's sensible. - 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:
- Put a deadline on anything that waits for a person.
- Put an approval in front of anything that reaches a client.
- Add a check before anything you can't take back.
What each guardrail is for
| The worry | What handles it | Where you see it working |
|---|---|---|
| This could get expensive | Per-run budgets, and per-day budgets across a whole job | The budget bar on the run, run.budget_exceeded |
| This could run forever | wall_clock_timeout, wait timeouts, retry limits | The stop reason and the step's status |
| The agent could go round in circles | max_agent_turns, and max_turns on a step | Turn count, and the stop reason turn_limit_reached |
| It said it worked, but did it? | check steps, with real evidence | check.passed, check.failed, and any approval it opened |
| This job keeps failing | Repeat-call and consecutive-failure breakers | run.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: 10limits:
credit_budget: 1000Mobius 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: 25limits:
daily_credit_budget: 2500This 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.
| Budget | Counts BYOK spend? | Why |
|---|---|---|
Per-run budget, budget_usd or credit_budget | Yes, Mobius processing credits | A run budget measures Mobius consumption inside the run. Provider-side invoices stay outside Mobius. |
Per-loop daily budget, daily_budget_usd or daily_credit_budget | Yes, Mobius processing credits | A 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: 30mWhen 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: 30smax_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: failRight 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: 20Worth 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_fail | Result |
|---|---|
fail | Stop the run with stop_reason: check_failed. |
continue | Continue the run and keep the red verdict on the timeline. |
gate | Open 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: 10The 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: 3A 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}/versionsYou 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 reason | Meaning |
|---|---|
completed | The run finished successfully. |
step_failed | A step failed without remaining retries. |
check_failed | A check failed and on_fail: fail stopped the run. |
gate_rejected | A check gate or interaction was rejected. |
cancelled | A user or system cancelled the run. |
replaced | A concurrency policy replaced the run. |
wall_clock_exceeded | The run exceeded limits.wall_clock_timeout. |
budget_exceeded | A run budget or loop daily budget halted the run. |
turn_limit_reached | The run exceeded limits.max_agent_turns. |
progress_stalled | The duplicate-tool-call breaker halted the run. |
step_limit_reached | The 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
suspendedfar 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
- Set a per-run budget. Every job that uses an AI model or a metered action.
- Set
limits.wall_clock_timeout. - Set a deadline on every waiting step, including approvals.
- Keep retry counts low until you're sure the action is safe to repeat.
- Add a check before anything you can't take back.
- Add a daily budget if it runs on a schedule or a busy event.
- 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
- Read the stop-state model in runs.
- Choose the right deadline with agent time limits.
- Understand plan charges and credit records in billing and usage.
- Configure step retries and waits in steps.
- Follow every guardrail event in the event catalog.