Money budgets
A fleet of agents on metered API keys spends real money with nobody watching: a hook that fires on every inbound lead, a heartbeat every five minutes, a pipeline that calls a paid model in a loop. A budget is a USD ceiling on that spend. When a budget is used up, the engine refuses the next agent turn it covers before any model is called, and asks a person whether to raise it.
Nothing is limited until you write a budget: block. A brain without one
behaves exactly as before.
What counts
Only metered spend counts: the cost_usd_metered of each row in the spend
ledger (omnislash.usage_records, see features.md § Usage & Cost
Accounting). That is the money your API keys are billed. Subscription usage
(Claude Max / Pro, ChatGPT plans) is priced in the ledger but costs nothing
extra, so it is never counted. A brain that runs entirely on subscriptions
spends nothing against its budgets.
A turn’s spend reaches the ledger when the turn finishes (or pauses on a question), and a sub-agent’s spend counts toward the run and scenario of the agent that started it.
Scopes and windows
| Scope | Windows | What is summed |
|---|---|---|
brain | day, month | every metered ledger row in the window |
scenario (a workflow or a pipeline) | run, day, month | run: everything one run spent, sub-agents included. day / month: every run of the scenario in the window |
role (an *.agent.md, or a system role-*.md) | day, month | every agent of the role together, wherever it ran (chat, heartbeat, hook, workflow task, pipeline) |
day and month are calendar windows in your brain’s timezone
(defineProject({ locale: { timezone } }), default UTC). A day budget
refills at local midnight and a month budget on the 1st. A run window
belongs to one run and never refills.
A heartbeat or a hook has no budget of its own. Bound it through the role its
agent: runs as, and through the brain.
Where you set a limit
You (or an agent) can write the blocks below by hand, or through the budget tools (Changing a limit). There are three tiers, and the nearest one wins:
- Engine: every limit is
null(no limit). - Brain:
constitution/MANIFEST.yaml§budget. - Entity: a top-level
budget:in a scenario’s or a role’s frontmatter.
The tiers merge window by window. A tier that leaves a window out inherits
it from the tier below. null means “no limit here” and overrides a limit from
the tier below, so one scenario can opt out of a brain-wide default.
Brain tier: constitution/MANIFEST.yaml
budget:
brain: # the whole brain
day: 20
month: 400
scenario: # a DEFAULT for every scenario that doesn't set its own
run: 2
day: 10
role: # a DEFAULT for every role that doesn't set its own
day: 5
scenario and role are defaults, not shared pools. With the block above,
each scenario may spend $10 a day and each role $5 a day. The block is
strict: a misspelled window (montly) fails the load instead of being read
as “no limit”. An engine one release back rejects budget:, so add it after
upgrading.
Scenario tier: features/<feature>/<id>.scenario.md
---
description: Weekly competitor digest
budget:
run: 5 # one run of this scenario, sub-agents included
day: null # no day limit for this scenario, whatever MANIFEST says
---
month is not written here, so it comes from MANIFEST budget.scenario.month.
This top-level budget: is a different setting from loop.budget, which only
limits what the self-evolving loop’s own agents may spend per cycle
(self-evolving.md). Both apply independently.
Role tier: features/<feature>/<id>.agent.md
---
description: Writes outreach emails
modelRef: smart
budget:
day: 3
month: 60
---
A role has no run window. The same key works in a system role file
(constitution/Agents/system/role-*.md). A role whose feature is switched off
keeps no budget: of its own, but it is still under the MANIFEST
budget.role default (and the brain’s limits), like any role that wrote none.
Which budgets a turn is checked against
Every agent turn is checked before it starts: chat messages, heartbeat ticks,
hook fires, workflow tasks, pipeline runAgent calls, sub-agents and system
agents. Each turn is checked against:
- the brain’s limits;
- the limits of the role it runs as;
- the limits of the scenario it runs inside (a workflow run or a pipeline
invocation), plus that run’s
runlimit, if it runs inside one.
If any of these is at or over its effective limit (the configured limit
plus every raise granted for that window), the turn is refused. The refusal
ends the turn as failed, with errorCategory: 'budget_refused' and a
message naming the budget, the spend and the limit. No model is called and
nothing is billed.
What happens when a budget runs out
| Where the turn came from | What you see |
|---|---|
| A chat message | The turn fails with the budget message. The next message is admitted again once the budget allows it. |
| A heartbeat tick or a hook fire | The tick or fire ends failed. The next one checks again, so nothing piles up while the budget is stopped. |
| A workflow task (or its PR finalize step) | The run parks in awaiting_retry (shown as Retrying). Completed tasks are kept. The run continues at once after a raise; otherwise it is tried again after the next local midnight, and goes on if the window has turned over (a new day or month) or the limit was edited up. A run window never turns over: a raise frees it, and Cancel on its card cancels the run. |
A pipeline runAgent | The agent’s result is failed with errorCategory: 'budget_refused'. Your pipeline code decides what to do (below). |
Handling it in a pipeline
runAgent throws a TerminalError on any failed result, so a refused turn
ends the invocation. To handle the refusal yourself, use safeRunAgent
(pipelines.md § Helpers API) and check the category:
import { safeRunAgent } from '@omnislash-ai/pipeline-sdk';
import { BUDGET_REFUSED } from '@omnislash-ai/types/spend-budget';
const result = await safeRunAgent(ctx, { agent: writer, prompt });
if (result.status === 'failed' && result.errorCategory === BUDGET_REFUSED) {
// The budget is spent and a person has a card asking for a raise.
// Stop cleanly, keep partial output, or wait and retry later.
return { status: 'stopped', reason: result.error };
}
Test the category, never the message: the message is wording and may change.
budget_refused means the turn never started. It is a different category from
budget_exceeded, which marks a turn that ran past its own per-call
cli.maxBudgetUsd and so did spend money.
The incident card
The first refusal for a budget raises a HITL approval card. It goes to
the default HITL chat (TELEGRAM_HITL_CHAT_ID if you set one, otherwise the
primary owner’s DM) and to the dashboard’s queue. It shows the scope, the
window, the spend and the limit. There is one card per budget and window: every
refusal while the card is open joins it, so a heartbeat that keeps ticking does
not send a new card each time. A card for a day or month window expires
ten minutes after that window ends: the new day or month starts from the configured limit,
and its own first refusal asks again.
The card offers:
- +50%: adds half of the card’s limit for this window.
- ×2: adds the card’s full limit, doubling it for this window.
- A reply with an amount above the current limit (
120,$120): sets this window’s limit to that amount. - Cancel: keep the work stopped. A workflow run stopped by its own per-run budget is cancelled, because that window never turns over.
A raise applies to this window only: today, this month, or this one run. Tomorrow’s day budget starts again from the configured limit. To change a limit permanently, see Changing a limit.
After a Cancel, unattended work (heartbeats, hooks, workflow runs) is not asked
about this window again. A turn a person is waiting on is: when an owner writes
to an agent in a chat, or someone sends it a message from the dashboard or over
the MCP bearer door, and that turn is refused, a new card is raised. (A run or
a sub-agent they started does not count: no one is waiting on that turn.) So even when the brain’s whole budget is spent
and no agent can run, you lift the stop from where you are, not by editing a
file. A person can also raise the current window directly with budget_raise
(below), with no card at all.
Only a person answers the card. From a chat, that means an owner. From the
dashboard, anyone who can open it. An agent cannot answer it, even one with
hitl:write, because an agent raising its own spending ceiling would defeat the
ceiling. For the same reason every tool that lifts a ceiling needs a person’s
approval (Changing a limit). The self-evolving loop also cannot edit a budget: block: a change it
proposes may not touch one.
After a raise, the workflow runs that this budget stopped are re-driven. Chat, heartbeats, hooks and pipelines are not re-driven: their next message, tick, fire or call is admitted again.
Changing a limit
Three tools change budgets. Each takes scope (brain, scenario or role),
an id for one scenario or role, and a window (day, month, or run for a
scenario). Leaving out id on scenario / role means the MANIFEST default
for every scenario / role.
| Tool | What it does | Who may run it |
|---|---|---|
budget_update | Sets a limit in MANIFEST or in the scenario’s / role’s budget: block, when the change lowers the ceiling or adds one where there was none. A change that raises a limit or removes it (limitUsd: null where a limit held) is refused. | config:write: an agent, no approval by default. A lower ceiling only stops work sooner. |
budget_widen | The same write, when it raises or removes a limit. | budget:widen: restricted, and a person approves each call. |
budget_raise | Adds USD to the current window of one budget (today, this month, or one run: id is then the run’s id), like a raise on the card, and re-drives the workflow runs it stopped. Needs no agent turn to be admitted, so it works even when the brain budget is spent. | budget:widen: a person approves. |
Both writes are a dry run first (dryRun: true returns the diff, the ceiling
before and after, and widens), then a commit with the returned sha.
limitUsd: null writes “no limit here”: on a scenario or role that lifts the
MANIFEST default for that one.
manifest_update refuses the budget section, and role_update /
role_create / role_widen refuse a budget key, so neither is a way around
the person’s approval. An agent with raw file tools can still edit the files
directly; if that matters, keep file:write away from it or gate it.
Asking where the money stands
The budget_status tool (capability runtime:read) answers for any agent that
holds it: every scope with a limit, what it has spent in the current window, its
effective limit, the raises granted, and the card that speaks for the window
(card.status pending waits for an answer, cancelled means someone chose
to keep it stopped). Ask your chief “how much of the budget is left this
month?” and it reads this tool.
Known limits
- A budget stops a turn at its start, not halfway through. A turn already running when the budget is crossed finishes, so N agents running at once can overshoot by up to N turns. Set limits with that margin in mind if your single turns are expensive.
- Only model spend is counted. A paid third-party API your pipeline calls directly (video generation, TTS) is not in the ledger yet. It will count with no change to your budgets once the engine records it.
- No warning before the stop. Nothing tells you at 80%. Use
budget_statusin a heartbeat if you want an early warning. - The ledger’s retention bounds what a budget can see. Spend is summed from
the ledger, which keeps rows for
usage.retentionDays(default 365). Set it shorter than a month and amonthbudget, or therunbudget of a run that lasts longer than that, no longer sees its oldest spend: it reads low and stops late.