Skip to content

Session budgets ​

A session is a conversation that can go on for days through many turns. The agent's per-turn cost limit (Build → Runtime → Limits & budget → Max cost) caps one turn. A session budget caps what the whole session spends, all turns together. When it is reached the turn stops where it is, and the session either waits for the budget to be raised or refuses further messages.

Separately from both, what agents spend now counts toward the tenant's daily and monthly spend limits, like model inference does — see Tenant spend limits.

Setting one ​

There are three places, from the broadest to the narrowest:

  1. For an agent — Build → Agents → your agent → Build → Execution → Session budget. Every new session starts with it.
  2. For one session at creation — send budget in the request that starts the session.
  3. For one session later — add, raise, lower or remove it any time. Unlike a fixed cap, a budget is a setting of the session.

Agent default (Execution) ​

SettingDefaultWhat it does
Budget per session (USD)empty = no budgetThe most one session — all its turns together — may cost. Every new session starts with this budget, and it can be raised, lowered or removed for a single session.
When the budget is reachedPause and waitPause and wait, or Stop. See below.
Let API clients change the budgetoffOff: a client token can lower a session's budget but not remove it or raise it past the amount above. Dashboard users are not affected.

Execution settings are operational: they apply immediately and are not part of a published version. The per-turn limit keeps working and is separate — a turn runs under the lower of the two.

What happens when it is reached ​

ModeBehaviour
Pause and wait (default)The turn stops where it is. The session is budget_paused. Once the budget is raised or removed, the turn carries on from where it stopped, with no new message.
StopThe turn stops and new messages are refused until the budget is raised or removed. Raising it only reopens the session: nothing carries on by itself, the next message starts the next turn.

The budget is checked before each model request against what the session has actually spent, so a turn can overshoot by at most one request.

A turn that finishes by itself but leaves the session at or past its budget — the common case for a plain chat turn, which is a single request — counts as reaching it: the answer is delivered, then the session is stopped exactly as above. Raising or removing the budget of such a session simply puts it back to idle, since its turn is already complete and there is nothing to continue.

In Studio ​

The session header shows spent / budget with a meter. With no budget it shows Set budget; click the meter to Edit budget: an amount in USD, When the budget is reached (Pause and wait for a higher budget or Stop and refuse new messages), Save and Remove budget.

A session paused on its budget shows a banner above the composer:

Session budget reached — The agent used $10.00 of its $0.05 budget. Raise the budget and it carries on from where it stopped, with no new message.

with New budget (USD), Increase and continue and Remove budget and continue. The amount must be greater than what the session has already spent. A session whose budget stops offers Increase budget only, after which the message box is open again. The composer reads Budget reached — raise it above to go on while the session is stopped, and the session list marks the row with a Budget reached badge.

Over the API ​

Start a session with a budget ​

bash
curl -X POST "$CONSOLE_URL/api/client/v1/agents/responses" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "support-bot",
    "input": "Go through the backlog",
    "budget": { "max_cost_usd": 2, "on_reach": "pause" }
  }'
typescript
const res = await client.agents.responses.create({
  model: 'support-bot',
  input: 'Go through the backlog',
  budget: { max_cost_usd: 2, on_reach: 'pause' },
});

budget is read when the request starts a session. On a request that continues one (previous_response_id, session_id) it is ignored altogether — not applied and not validated — so a client may send its default budget on every call without it being refused (403) or rejected (400) on a follow-up turn. Change the budget of a running session with the PATCH below. "budget": null starts the session with no budget, overriding the agent's default. POST /agents/sessions takes the same field.

FieldValues
max_cost_usdA number greater than 0.
on_reachpause (default) or stop.

Read, change or remove it ​

http
GET   /api/client/v1/agents/sessions/{id}
PATCH /api/client/v1/agents/sessions/{id}
bash
# Raise it (and, because the session is paused on it, continue the turn).
curl -X PATCH "$CONSOLE_URL/api/client/v1/agents/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "budget": { "max_cost_usd": 5, "on_reach": "pause" } }'

# Change the budget only — do not continue.
curl -X PATCH "$CONSOLE_URL/api/client/v1/agents/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "budget": { "max_cost_usd": 5, "on_reach": "pause" }, "resume": false }'

# Remove it.
curl -X PATCH "$CONSOLE_URL/api/client/v1/agents/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "budget": null }'
typescript
const result = await client.agents.sessions.update(sessionId, {
  budget: { max_cost_usd: 5, on_reach: 'pause' },
});

if (result.object === 'agent.run') {
  // The session was paused on its budget: the turn continues as a background run (202).
  const run = await client.agents.runs.wait(result.id);
  console.log(run.status);
} else {
  console.log(result.runtime_status, result.spent_usd); // updated session (200)
}

// Change the budget only.
await client.agents.sessions.update(sessionId, { budget: { max_cost_usd: 5 }, resume: false });
// Remove it.
await client.agents.sessions.update(sessionId, { budget: null });

budget is required in the body (400 invalid_budget without it); { "budget": null } removes it. The new max_cost_usd has to be above what the session has already spent (400 invalid_budget otherwise). The session reports budget, spent_usd and runtime_status (budget_paused while it waits).

For a session that is paused on its budget, the PATCH also continues it: the turn runs as a background run and the answer is 202 with the run. Send "resume": false to change the budget only. Continuing needs background execution to be allowed for the agent (400 agent_background_disabled otherwise — nothing is changed in that case).

Two cases have nothing to continue, and the PATCH returns the session idle (200) instead of starting a run:

  • A session whose turn had already finished when it reached the budget (the plain chat turn above).
  • A budget that stops instead of pausing — the PATCH raises it and returns the session idle; resume has no effect, and the next message starts the next turn.

What a client sees when it is reached ​

A message sent to a session whose budget is used up is refused with 402:

json
{
  "error": {
    "type": "budget_reached",
    "code": "budget_reached",
    "message": "This session has reached its budget ($1.0200 of $1.0000). Raise or remove the budget to go on.",
    "spent_usd": 1.02,
    "max_cost_usd": 1
  }
}

A turn that hits the budget part-way is returned like any stopped run: status: "incomplete", stop_reason: "budget_reached" (and incomplete_details.reason), and the work done so far is kept.

Who may change it ​

When an agent has a session budget, it binds its API clients: a client token can lower a session's budget but cannot remove it or raise it past the agent's amount (or past what the session already has, if someone with dashboard access raised it) — 403 budget_override_forbidden. Turn on Let API clients change the budget in the Execution settings (allowClientOverride) to allow it. Dashboard users are not affected.

A paused session and API-only clients ​

A session that is budget_paused goes on only when its budget is raised or removed. For an agent with a session budget and Let API clients change the budget off, an API client can do neither: raising or removing is 403 budget_override_forbidden, and lowering has to stay above what the session has spent, which a paused session has already passed (400). Such a session stays paused until someone with dashboard access continues it — in Studio, open the session and use the banner's Increase and continue (or Remove budget and continue), which changes the budget and carries the turn on. Or turn on Let API clients change the budget, after which the PATCH above continues it.

Plan for this when an agent with a session budget is driven only through the API: either allow client overrides, or have an operator watch for budget.reached webhooks.

Events and webhooks ​

Every turn of a session with a budget writes session.usage (turn_cost_usd, spent_usd, budget). Reaching the budget writes budget.reached followed by session.status_idle with stop_reason: budget_reached, and sends the budget.reached webhook (conversationId, agentKey, spentUsd, maxCostUsd). See session events.

Tenant spend limits ​

A session budget is the agent's own cap. Separately, what agents spend counts toward the tenant's daily and monthly spend limits, like model inference does:

  • A turn does not start once a limit is used up: 429 with error.type: "rate_limit_error" and error.code: "spend_limit_exceeded". This holds from every channel — Studio, the API, portals, background runs, and resumed and continued turns. A turn that is queued (a background run, a budget continuation) is refused up front, so you get the 429 at once instead of a 202 and a run that fails later.
  • The cost of each priced model request is counted as it happens, so the requests of a turn that later fails are still counted — the provider billed them. A turn that is resumed, continued or retried cannot be debited twice.
  • Counters are kept per model key, and a sub-agent with its own model debits its own counter, so every model the turn can call is checked before it starts.
  • Sessions started by a synthetic actor (a schedule, a timeout sweep, a public A2A caller) count against the tenant's own counter.

The limits themselves are budget policies; see the Spend & Budgets API.

Config reference ​

json
{
  "config": {
    "execution": {
      "sessionBudget": { "maxCostUsd": 2, "onReach": "pause", "allowClientOverride": false }
    }
  }
}
KeyValuesDefault
execution.sessionBudget.maxCostUsdnumber > 0— (no budget)
execution.sessionBudget.onReachpause, stoppause
execution.sessionBudget.allowClientOverridetrue / falsefalse

(The client API spells the request fields max_cost_usd and on_reach; the stored config uses maxCostUsd and onReach.)

Troubleshooting ​

402 budget_reached on a new message. The session is at or past its budget. Raise or remove it with PATCH (or in Studio). For a pause budget the paused turn then continues; for a stop budget the next message starts a new turn.

400 invalid_budget on PATCH. Either the body is malformed, or max_cost_usd is not above what the session has already spent. The message names the amount spent.

403 budget_override_forbidden. The agent has its own session budget and does not let API clients raise or remove it. Lower it, ask someone with dashboard access, or enable Let API clients change the budget.

I raised the budget over the API and nothing continued. Check three things. You sent "resume": false, which only changes the budget. The session's turn had already finished when it crossed the budget (a plain chat turn): it goes back to idle and the answer was already delivered — send the next message. Or the budget stops rather than pauses, so nothing carries on by itself. In Studio, changing the budget from the header meter or the banner continues a paused session for you.

Spend in a session looks lower than the provider's bill. Turns with no priced model — a model with no pricing in the Model Hub — add nothing. The session list marks sessions that have Unpriced turns.

What's new ​

Session budgets, the 402 budget_reached refusal, client overrides and tenant spend limits for agents are new in the October 2026 agent runtime release.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub