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

ScopeWindowsWhat is summed
brainday, monthevery metered ledger row in the window
scenario (a workflow or a pipeline)run, day, monthrun: 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, monthevery 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:

  1. Engine: every limit is null (no limit).
  2. Brain: constitution/MANIFEST.yaml § budget.
  3. 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 run limit, 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 fromWhat you see
A chat messageThe turn fails with the budget message. The next message is admitted again once the budget allows it.
A heartbeat tick or a hook fireThe 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 runAgentThe 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.

ToolWhat it doesWho may run it
budget_updateSets 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_widenThe same write, when it raises or removes a limit.budget:widen: restricted, and a person approves each call.
budget_raiseAdds 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_status in 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 a month budget, or the run budget of a run that lasts longer than that, no longer sees its oldest spend: it reads low and stops late.

Screens from Sixty Seconds Studio, a fictional demo studio, running on the real dashboard.