Reference

Spend refusals

The object every budget, quota and tree-limit refusal carries, so an agent or an app can wait, ask or stop without reading the message.

For: Developers handling budget and quota refusals.

A budget, quota or job-tree limit refusal adds an oz0 object beside the error's type and message. Use the object's fields to decide whether to wait, request more budget or stop. The message is for people and its wording can change.

{
  "type": "error",
  "error": {
    "type": "oz0_quota_exceeded",
    "message": "team ml has spent $41.2000 of its $40.00 quota for October 2026; ask for more ...",
    "oz0": {
      "v": 1,
      "scope": "team",
      "scope_id": "team:ml",
      "window": "month",
      "resets_at": "2026-11-01T00:00:00Z",
      "limit_usd": 40,
      "spent_usd": 41.2,
      "call_estimate_usd": 0.0912,
      "limit_name": "monthly_usd",
      "can_wait": true,
      "raise": {
        "by": ["team_admin", "tenant_admin"],
        "command": "oz0 quota request team:ml --usd 10 --tenant acme --reason '...'"
      }
    }
  }
}

A client branches on the fields: it waits until resets_at (or for Retry-After), follows raise.request once a budget request is open, or stops.

Which refusals carry it

Error typeStatusWhat
oz0_budget_exceeded402A job tree's budget or the tenant's monthly budget is spent
oz0_quota_exceeded402 at the gateway, resource_exhausted (429) at the runtime APIA person's, a key's, a service's or a team's quota: money a month, jobs a day, or jobs at once
oz0_budget_wait402A job tree's budget is spent and the job waits for a person to raise it (on_budget: wait)
oz0_budget_forecast402A forecast asks a person before the tree reaches its budget
oz0_tree_limit402A job tree's limit that is not money: model calls, tokens, children, children at once, depth, or an agent's delegation limits
oz0_budget_busy429The call does not fit only because other calls of the same budget still run; send it again when they settle

oz0_budget_unbounded (400) carries no object either: under reserve: strict the call's worst case cannot be bound, or under any mode it comes to more than a reservation can hold. No budget's figures are involved, and sending it again changes nothing.

oz0_budget_unavailable (503) carries no object: the budgets could not be read, which is an outage, not a refusal, and the call is sent again shortly. Neither does oz0_pool_cooling (429), a provider's cooldown, or oz0_killed.

Where it appears

SurfaceWhere the object is
The gateway (/llm/...), a node's local model call and a child job's admissionerror.oz0 of the answer's body
The runtime API's StartJobA oz0.runtime.v1.SpendRefusal detail of the resource_exhausted error, with the object's fields
GetJob and WatchJobJob.refusal of a job a budget, quota or limit ended, and BudgetWait.refusal of a job that waits for a budget decision
A flow's webhookThe 429's body, error.oz0
The MCP server_meta["ai.oz0/refusal"].oz0 of the isError tool result, and structuredContent.error.oz0 too when the tool declares no outputSchema
A tool's MCP sampling requesterror.data["ai.oz0/refusal"].oz0 of the JSON-RPC error the tool's server gets (code -1); on MCP's newer protocol, where the refused request ends the tool call, the agent's tool error carries the same object as structuredContent.error.oz0
A2AAn item {"@type": "type.googleapis.com/google.protobuf.Struct", "value": {"error": {...}}} of the JSON-RPC error's data, and a data part {"error": {...}} of a failed or budget-waiting task's status message
AG-UIRUN_ERROR: code is the error type and rawEvent.oz0 the object
The audit trailrefusal_detail of each run.refused entry, beside the clipped reason
The evidence record of a model callrefusal_detail, without the name of a person or team it is about

A remote agent's session learns it from the answer to its usage reports, in the Oz0-Over-Budget-Detail header beside Oz0-Over-Budget.

Fields

FieldTypeMeaningAbsent when
vnumberThe object's version: 1Never
scopestringWhat was spent or reached: tree, tenant, person (a person's account, an API key, a service identity or an operator certificate), team, or limit (a tree's limit that is not money)Never
scope_idstringThe tree's root job (tree, limit), or the principal or team (team:<name>) whose quota it isFor tenant, and when withheld
windowstringThe period the scope counts in: job (the tree, which never resets), month (the calendar month in UTC), day (the UTC day) or at_once (jobs running at the same time)Never
resets_atstring or nullWhen the window starts again, in RFC 3339Always present; null for a tree, a limit and jobs at once
limit_usdnumberWhat the scope may spend, in US dollarsFor limits and quotas on jobs, and when withheld
spent_usdnumberWhat the scope has spentAs limit_usd
reserved_usdnumberWhat calls of the scope still running hold at their worst caseWhen nothing is held, and when withheld
call_estimate_usdnumberThe refused call's worst case, at which it was to be reservedWhen the refusal is not about a call (a start, a child's admission), and when withheld
limit_namestringThe limit reached: model_calls, tokens, children, fan_out, depth, wait, delegation_max_depth, delegation_max_children, jobs_per_day, jobs_at_once or monthly_usdFor budgets, and when withheld
stepsnumberA count the tree reached: model calls, child jobs, children at once or depthUnless limit_name is one of those
steps_limitnumberThe limit of stepsAs steps
tokensnumberThe tree's tokensUnless limit_name is tokens
tokens_limitnumberThe tree's limit on tokensAs tokens
secondsnumberThe time the tree waited for peopleUnless limit_name is wait
seconds_limitnumberThe tree's limit on waitingAs seconds
jobsnumberThe jobs started today, or running nowUnless limit_name is jobs_per_day or jobs_at_once, and when withheld
jobs_limitnumberThe quota on those jobsAs jobs
calls_runningnumberFor oz0_budget_busy: the calls of the budget still runningFor every other type, and when withheld
forecast_chancenumberFor oz0_budget_forecast: the share of the agent's earlier trees that went on to pass the budget from hereFor every other type
can_waitbooleantrue when the job waits for a person's decision instead of failing: a tree under on_budget: wait, a forecast, a quota refusing a job's model callNever
raiseobjectWho can raise the scope, and howNever
raise.byarray of stringsThe roles that can: tree_owner (the person who started the tree), team_admin, tenant_adminNever (may be empty)
raise.requestobjectThe budget request opened about the refusalUntil one is opened: the gateway refuses before any request exists, so its answer never has one; a job's wait and its failure name it
raise.request.idstringThe request's ID, for oz0 quota approveAs raise.request
raise.request.urlstringThe request on the web UI's Approvals page, which opens with that request marked (decided ones shown too when it is no longer pending)Without a public URL (--public-url)
raise.commandstringThe CLI command that asks for more (with the tenant it is in), sets more, or approves the requestFor an agent's own delegation limits, and when withheld
withheldbooleantrue when the scope's name and figures were left out for this callerWhen nothing was left out

Additions are fine: a client ignores fields it does not know. A renamed field, or one whose meaning changes, needs a new version, v: 2.

Who sees the numbers

A person's or a team's figures and name are shown only to a caller who may read that quota today: the person themselves, for their own quota, or someone whose role holds usage.read and whom works-council mode lets see that figure (a team of at least the tenant's threshold of people, a service identity, or anyone once an admin turned the mode off).

For anyone else the object keeps v, scope, window, resets_at, can_wait and raise without its command, sets withheld, and the message names neither the quota nor its figures:

{"v": 1, "scope": "team", "window": "month", "resets_at": "2026-11-01T00:00:00Z", "can_wait": true,
 "raise": {"by": ["team_admin", "tenant_admin"]}, "withheld": true}

Each surface filters for its own caller. The gateway answers the node that acts for a job, so its answer is filtered for the person who started the job's tree; the runtime API, MCP, A2A and AG-UI filter for whoever called them; a webhook's answer for the webhook's own identity (service:webhook/<flow>). A job tree's own budget, the tenant's budget and the tree's limits are not figures about people, and are shown to everyone who gets the refusal.

The audit trail's run.refused entry keeps the refusal as the limit wrote it, whoever was refused: its reason and refusal_detail name the quota that stopped the start or the call, with its figures, because the trail is for those who hold audit.read. The evidence record of a model call keeps the figures but never the name of a person or team: scope_id and raise.command are left out for person and team, as the record names only kinds of budget.

Retry-After

A refusal of a quota or of the tenant's month whose resets_at is within 24 hours carries Retry-After, in seconds until the reset: on the gateway's 402, on a webhook's 429, and in the metadata of StartJob's error. A client that honours it waits without code of its own. A tree's budget and its limits never reset, so their refusals never carry it. oz0_budget_busy keeps its own short Retry-After (two seconds), since what it waits for is calls that settle, not a reset.

Copyright © 2026