# Budgets An unattended agent run can spend money faster than you can watch it. Budgets are how a Hankweave workflow carries spending limits in the program itself: the author records which steps are expensive, which are critical, and what partial work is still worth keeping, and the operator sets the dollar and time envelope for a particular run. This page walks through that contract in the order you will meet it – who sets which limits, the four resources a budget can cap, how dollars reach individual codons, what happens when a cap trips, how loops and resumed runs interact with budgets, and how to catch a bad configuration before any tokens are spent. ## Why is cost a program construct and not a dashboard? A dashboard can tell us what a run spent, but it cannot carry spending decisions into the next version or the next handoff. When we author a workflow, we already know which steps are expensive, which are critical, and what partial output would still be useful. Putting those decisions in the workflow definition keeps them versioned and reviewable alongside the work they govern, and leaves the operator free to choose the spending and time limits for a particular run. The contract divides responsibility between two parties. A **hank** is the workflow definition; a **codon** is one step in that definition. The author describes the shape of the work – its expensive and critical steps – and sets policies for partial output. The operator owns the wallet, clock, and context envelope around a run. The runtime combines those responsibilities into a resolved plan. The contract is concrete: the author can set shares and `onExceeded: "fail"` when partial output is worse than none; the operator can set an envelope such as `--max-cost 5.00` and `--max-time 600`. The runtime resolves the applicable ceiling for each axis, derives per-codon limits, and shows that plan before tokens are spent. The author's shape cannot raise the operator's envelope, and a CLI ceiling does not bypass the author's `onExceeded: "fail"` policy. The diagram below shows how the two inputs flow into enforcement. Author and operator settings resolve into ceilings, which become per-codon budgets; a metered-message check and a time watchdog can each request a SIGTERM, and `onExceeded` decides the outcome. ![budgets: the mental model diagram](/content-assets/cf45dff5691c48c0/diagrams/concepts-budgets/1.png) budgets: the mental model diagram
Diagram as text ```text author: { shares, onExceeded } ─┐ operator: { maxDollars, maxTime } ─┴→ resolved ceilings ↓ per-codon budgets ↙ ↘ metered messages time watchdog ↓ ↓ limit check ─────────────→ SIGTERM request ↓ onExceeded outcome ```
Reading the figure top to bottom gives the three verbs this page uses throughout: a budget **allocates** shares, a cap **trips**, and `onExceeded` decides whether the codon is marked completed or failed. We can let savings flow forward under proportional allocation, design a loop to add value one iteration at a time, and inspect the resolved plan with `--validate` before spending. A cheap first run is the practical starting point while you learn what the work needs: ```bash # A 50-cent ceiling for a first safety run bunx hankweave@0.10.0 hank.json data/ --max-cost 0.50 -m haiku ``` That command caps the whole run at fifty cents on a small model, so a misconfigured workflow fails cheaply. Budgets also change what a spending limit can buy. Without loops, cost-to-benefit can be a step function: `$9.99` buys nothing useful while `$10.03` buys 10 units. A cost-driven loop lets each iteration add marginal value, so the operator can choose a point on that curve rather than betting on a single threshold. One complete 0.10.0 execution of the anchor workload used five Haiku codons and two `pi/baseten/deepseek-ai/DeepSeek-V4-Pro` judgment codons. Its configured per-codon caps sum to `$9.00`; the accepted capture reports `$0.8830` in tracked codon cost for that complete execution. The configured sum is a ceiling, not a measured total or price promise, and the tracked figure covers codons in that capture rather than provider health checks or sentinel calls. The enforcement boundary matters as much as the numbers. Dollar caps are checked incrementally as metered assistant messages arrive. A cost delta is accumulated, the cap trips when the accumulated value reaches the limit, and the running agent receives a SIGTERM request. That can interrupt a codon after an already-billable message; a cap is not a prepaid spending guarantee. Time uses a separate watchdog described below. A provider estimate that lands exactly on a dollar cap is still an edge case: the `>=` comparison operates on floating-point accumulation, so provider-side rounding precision determines the boundary result. There are two further limits to this contract. Some models have no provider-registry pricing; for those models cost tracking remains `$0`, so a dollar cap never trips and preflight warns. The runtime loads model and pricing data as part of provider resolution; the pricing is embedded in that model data rather than exposed as a separate reader-facing pricing surface. Provider rate limits, database-read limits, and customer-facing API-call counts are not budget axes in this release. The four implemented currencies are dollars, time, output tokens, and context tokens. You can still use a dashboard. By keeping cost decisions in the workflow definition, we also carry them through versioning and handoffs, rather than depending on monitoring configuration left in another environment. > **Pitfall:** A dollar budget on an unpriced model reports `$0` and never trips. Use `maxTimeSeconds`, `maxOutputTokens`, or `maxContextTokens` as an additional guard, and heed the preflight warning. ## Who sets what – the two-party contract With the rationale in place, the next question is which party controls which field. The author sets the shape: child codon and loop IDs can receive proportional shares, and a step that must not produce partial output can set `onExceeded: "fail"`. The operator sets the envelope with `--max-cost ` and `--max-time `, or with `budget.maxDollars` and `budget.maxTimeSeconds` in `hankweave.json`. For ordinary runtime settings, the precedence order is CLI, then `HANKWEAVE_RUNTIME_*` environment variables, then hank overrides, then `hankweave.json`, then defaults. Budget ceilings are special: `hankweave.json` and hank-override `maxDollars`/`maxTimeSeconds` combine with `min()` to form the effective global ceiling, while an explicit `--max-cost` or `--max-time` replaces that result and can loosen it. `HANKWEAVE_RUNTIME_*` parsing does not provide budget settings. This is distinct from model selection and from the persisted execution plan; see [Hanks](/0.10.0/files/concepts/hanks) for the broader configuration discussion. The following tables list the fields each party can set, starting with the author-controlled fields on a hank override: | field | type | default | required | constraints | description | | ---------------- | ---------------- | -------- | -------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `maxDollars` | `number` | | no | > 0 | Total cost budget in USD. | | `maxTimeSeconds` | `number` | | no | > 0 | Wall-clock time limit in seconds. | | `allocation` | `enum` | `shared` | no | `shared` \| `proportional` \| `proportional-strict` | How the dollar budget is distributed among children. 'shared': first-past-the-post (default). 'proportional': pre-allocate shares, unspent flows back. 'proportional-strict': pre-allocate shares, unspent evaporates. | | `shares` | `object` | | no | | Map of child codon/loop ID to fraction (0-1) of the budget. | | `onExceeded` | `enum` | | no | `complete` \| `fail` | What happens when budget is exceeded. 'complete' = graceful completion (default). 'fail' = codon failure, triggers onFailure policy. | Codons accept a narrower set of fields – hard caps rather than allocation policy: | field | type | default | required | constraints | description | | ------------------ | --------- | ------- | -------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `maxDollars` | `number` | | no | > 0 | Max cost in USD. Execution stopped when exceeded. | | `maxTimeSeconds` | `number` | | no | > 0 | Max wall-clock time in seconds. | | `maxOutputTokens` | `integer` | | no | > 0 | Max output tokens. | | `maxContextTokens` | `integer` | | no | > 0 | Max context window tokens (high-water mark of input+output per turn). Useful for capping context growth independent of cost. | | `onExceeded` | `enum` | | no | `complete` \| `fail` | Override hank-level onExceeded for this codon. | A loop uses the same container shape as a hank override for its dollar and time settings: | field | type | default | required | constraints | description | | ---------------- | ---------------- | -------- | -------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `maxDollars` | `number` | | no | > 0 | Total cost budget in USD. | | `maxTimeSeconds` | `number` | | no | > 0 | Wall-clock time limit in seconds. | | `allocation` | `enum` | `shared` | no | `shared` \| `proportional` \| `proportional-strict` | How the dollar budget is distributed among children. 'shared': first-past-the-post (default). 'proportional': pre-allocate shares, unspent flows back. 'proportional-strict': pre-allocate shares, unspent evaporates. | | `shares` | `object` | | no | | Map of child codon/loop ID to fraction (0-1) of the budget. | | `onExceeded` | `enum` | | no | `complete` \| `fail` | What happens when budget is exceeded. 'complete' = graceful completion (default). 'fail' = codon failure, triggers onFailure policy. | On the operator side, the runtime configuration exposes the two global budget fields: | field | type | default | required | constraints | description | | ---------------- | -------- | ------- | -------- | ----------- | ------------------------------------------------------------------------ | | `maxDollars` | `number` | | no | > 0 | Global cost budget in USD. Effective global = min(runtime, hank). | | `maxTimeSeconds` | `number` | | no | > 0 | Global wall-clock time limit in seconds. Effective = min(runtime, hank). | In 0.10.0, the parser accepts `--max-cost` and `--max-time`, although both flags are omitted from `--help`. The resolved plan at `--validate` is the useful confirmation that the flags are active. The JSON equivalents are `budget.maxDollars` and `budget.maxTimeSeconds`. The budgets scenario shows the author side of this contract in practice: a `$0.20` proportional pool split into equal shares, with the hank defaulting to `onExceeded: "complete"` and `write-second` overriding that policy to `"fail"`. ```json "overrides": { "budget": { "maxDollars": 0.2, "maxTimeSeconds": 600, "allocation": "proportional", "shares": { "write-line": 0.5, "write-second": 0.5 }, "onExceeded": "complete" ``` Since 0.6.1, the resolved plan has shown the global ceiling, allocation mode, per-codon dollar caps with their source, time limits, and `onExceeded` policies before any tokens are spent. It appears in `--validate` and at run startup, and the validation section below shows what it looks like. > **VersionNote:** In 0.10.0, `--max-cost` and `--max-time` are functional parser flags but are absent from `--help`; use the resolved plan from `--validate` to inspect their effect. ## Which resource are you capping? So far we have talked about "limits" generically. A budget actually has four currencies. Dollars and time can be set at hank, loop, and codon levels. Output tokens and context tokens are codon-level only. | Currency | Field | Accepted levels | What it caps | | -------------- | ------------------ | ----------------- | -------------------------------- | | Dollars | `maxDollars` | hank, loop, codon | USD cost, fungible across codons | | Time | `maxTimeSeconds` | hank, loop, codon | Wall-clock seconds | | Output tokens | `maxOutputTokens` | codon | Model-generated output tokens | | Context tokens | `maxContextTokens` | codon | High-water mark of context fill | Each currency behaves differently once set. Under `shared` and `proportional` allocation, `maxDollars` is fungible: money one codon does not spend can remain available to later codons. Under `proportional-strict`, unspent shares evaporate. Cost tracking requires the model to have provider-registry pricing. `maxTimeSeconds` is a wall-clock watchdog. The runtime checks elapsed time during cost updates and also checks it with a one-second interval independent of cost events. When the limit is exceeded, the codon runner requests SIGTERM. This is an active watchdog, not merely an end-of-session check. That mechanism does not establish prompt teardown in every execution harness (the process wrapper). An older recorded attempt ran for roughly 24,923 seconds against a 2,400-second cap before its budget error surfaced with another failure; the source/runtime cause of that observation is unresolved, and it is not a new 0.10.0 timer experiment. For unattended work, add an external process deadline as a separate safety boundary; it is not a checkpoint-recovery mechanism. `maxOutputTokens` caps model-generated output for one codon. Context handling is separate: `continuationMode: "fresh"` starts a new session, while `continuationMode: "continue-previous"` continues the previous codon's session and conversation history. `maxContextTokens` tracks the high-water mark of `inputTokens + outputTokens` per turn, not a cumulative total. A turn here is one assistant-message round trip. This limit is useful when context-window fill affects quality independently of dollar cost. Provider quota, database-read limits, and customer-facing API-call counts are not additional currencies in this release. ## How dollars are allocated among codons Of the four currencies, only dollars are divided among children. A container budget can allocate its dollar pool in three modes: `shared`, `proportional`, and `proportional-strict`. The default is `shared`. | Mode | How children receive dollars | What happens to savings | | --------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | `shared` | Children draw from one remaining pool in execution order; the next codon receives what remains. | A cheaper earlier codon leaves more for later codons. | | `proportional` | Named children receive shares before work begins. | Unspent dollars flow back to the pool for later codons. | | `proportional-strict` | Named children receive the same kind of share. | Unspent dollars evaporate; the partition stays strict. | For example, when a child has a `$2` share and spends `$1`, `proportional` returns the unused dollar to the pool, while `proportional-strict` lets it evaporate. `shares` maps child codon or loop IDs to fractions from `0` through `1`. It is meaningful only with `proportional` or `proportional-strict`. Setting it without one of those allocation modes is a preflight error. Proportional allocation also requires `maxDollars` on the container, because there must be a total to divide. Given those inputs, the resolution chain for a codon is: 1. Find the effective ceiling. 2. Subtract prior spending to find the remaining pool. 3. Apply the allocation mode. 4. Apply the codon's hard cap: `min(allocated, codon.maxDollars)`. 5. If there is no codon-specific result, that codon-level result is uncapped; the enclosing effective ceiling still bounds the shared pool. The codon's own dollar cap is applied last, so it remains a hard ceiling even when its allocation is more generous. `onExceeded` at the codon level overrides the hank or loop container; when neither level sets it, the policy defaults to `complete`. The anchor workload's three normalize codons each carry a `$0.50` hard cap. Those configured caps are separate from the workload's `$9.00` sum of per-codon caps and from its measured tracked cost. Time is not allocated proportionally. Codons execute sequentially, so dividing a time budget by shares has no useful meaning. Time remains a shared watchdog at the container level and a hard cap at the codon level. ## What happens when a cap trips A metered cost or watchdog check can request an interrupt while the codon is running. After that interrupt, `onExceeded` decides the codon's outcome; the policy is separate from the interrupt mechanism. | Policy | Result | When the result fits | | -------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | `complete` (default) | The codon is marked completed, partial output survives, and downstream codons still run. | Best-effort work where partial output is useful. | | `fail` | The codon is marked failed non-retriably with reason `budget exceeded`; its `onFailure` policy determines what follows. | All-or-nothing work where partial output is worse than none. | The default is a soft-cap policy: "completed" describes the runtime outcome, not proof that the agent finished the task. A completed codon can carry `budgetExceeded` metadata rather than a failure reason. That object identifies the currency that tripped, its limit, and the amount used: `{ currency, limit, used }`. If the cap trips before a required handoff is written, `complete` can leave the next codon with no file, truncated JSON, or an old artifact. A later model may improvise a plausible replacement instead of exposing the missing work. Put a deterministic boundary check in the consumer's rig, before its agent starts, and check required fields and input coverage rather than just file existence. See [Start with a guarded handoff](/0.10.0/files/author/testing-and-hardening#start-with-a-guarded-handoff). Keeping useful partial work is separate from allowing the pipeline to continue. For a required producer, choose `onExceeded: "fail"` with an intentional `onFailure` policy such as `"abort"`; failure status does not itself erase work already written. Checkpoints govern what recovery can restore. If best-effort completion is intentional, define the partial-result contract and make the boundary check reject anything the consumer cannot safely use. Prompt instructions alone cannot enforce that contract after interruption. See [Events](/0.10.0/files/reference/events) for the complete event shapes and surrounding contract. The two events most relevant here are `codon.completed`, which carries the `budgetExceeded?` flag, and `budget.summary`, which reports the ceiling, allocation, and per-codon rows: | id | category | journaled | sentinelRouted | payloadFields | receipts | | --------------- | ------------ | --------- | -------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | codon.completed | server-state | true | true | codonId, success, cost, duration, exitStatus, failureReason?, failureIgnored?, budgetExceeded? | schemas/event-schemas.ts:560, schemas/event-schemas.ts:961, schemas/event-schemas.ts:1235, hankweave-runtime.ts:1886, hankweave-runtime.ts:2064, hankweave-runtime.ts:2253, hankweave-runtime.ts:3365 | | id | category | journaled | sentinelRouted | payloadFields | receipts | | -------------- | ------------ | --------- | -------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | budget.summary | server-state | true | true | ceiling, allocation, rows, totals | schemas/event-schemas.ts:724, schemas/event-schemas.ts:979, schemas/event-schemas.ts:1267, hankweave-runtime.ts:6478 | `budget.summary` is a runtime state-based summary built from the budget and the run's codons. It is not a provider invoice and is not a guaranteed retry-inclusive equivalent of every billing record. When the outcome is failure instead, `onFailure` is the follow-up policy for a failed codon: `abort`, `retry`, or `ignore`. `abort` keeps a retriable failure active but shuts down for a non-retriable failure; `retry` retries only retriable failures until `maxAttempts` and shuts down for a non-retriable failure or exhausted attempts; `ignore` continues. A retry configuration's `delayMs` has a maximum of 60,000 milliseconds. Two configurations deserve an early warning. `onExceeded: "fail"` with `onFailure: "retry"` still triggers a preflight warning, but a budget-exceeded failure is non-retriable: the run shuts down rather than automatically retrying the same allocation. A fail-policy codon late in a shared pool may find that earlier codons have exhausted the pool before it runs. ## Budgets inside loops Loops change what a budget means, because the same codons run repeatedly against one pool. A loop-level dollar budget is one pool shared across all iterations. When that pool is exhausted, the planner terminates the loop early and removes the remaining codons of the current iteration. The loop can therefore end before all planned work in that iteration runs. A codon-level `maxDollars` inside a loop means "per iteration." A `$1.00` codon cap provides `$1` for each iteration, not `$1` for the whole loop; the loop-level budget controls the aggregate. `shared` is usually the useful allocation mode inside a loop. The number of iterations is often unknown, so assigning a proportional share to each iteration is rarely meaningful. An iteration limit remains a separate safety valve for how many iterations may run. A loop-level `maxTimeSeconds` is a watchdog across all iterations. When elapsed loop time exceeds the limit, the current codon is stopped and the loop terminates. If a codon's time cap is longer than its parent loop's time cap, preflight warns because the loop will terminate first. One reporting caveat: budget exhaustion is not a `terminationReason` value on `loop.iteration.completed`. The allowed values are `iteration_limit`, `context_exceeded`, `sentinel_skip`, and `failure`; do not report `"budget exceeded"` as one of them. | id | category | journaled | sentinelRouted | payloadFields | receipts | | ------------------------ | ------------ | --------- | -------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | loop.iteration.completed | server-state | true | true | loopId, iteration, durationMs, costUsd, tokensUsed, isFinal, terminationReason? | schemas/event-schemas.ts:612, schemas/event-schemas.ts:976, schemas/event-schemas.ts:1245, hankweave-runtime.ts:6035 | ## What survives when a run resumes? A resumed run does not forget earlier spending. Dollar use is hydrated from prior run costs in the state file: if `$6` was spent against a `$10` budget, `$4` remains for the resumed run. Elapsed wall-clock time is hydrated from prior run timestamps too. If 45 minutes have elapsed against a 60-minute budget, 15 minutes remain; the runtime offsets the hank start time by the prior elapsed duration. Run-level budget settings are read again when a continuation run starts, but per-codon budgets in the persisted execution plan remain frozen. Raising the applicable run-level `maxDollars` can increase the remaining pool; editing a saved codon's cap in `hank.json` does not replace that cap on resume. Use the [change/resume table](/0.10.0/files/operate/resume-rollback-and-retry#resume-without-re-running-sealed-work) before choosing between another attempt and `--start-new`. Retry attempts during one codon execution add to that codon's running total. When the codon completes, the retry-only accumulator is cleared; that does not erase the completed codon's cost from the spending map. Costs from already completed codons remain immutable there and hydrate the next run. Budget state is persisted in `.hankweave/state.json`; see [State file](/0.10.0/files/reference/state-file) for its storage contract. The state manager writes the file atomically. A resumed run combines prior dollar and time usage, current run-level settings, and saved per-codon definitions. ## Catching budget misconfigurations before tokens are spent Everything so far can be inspected before it costs anything. `--validate` runs the budget checks before any tokens are spent and prints the resolved budget table. Errors block execution; warnings inform without blocking. The table gives the global ceiling, allocation mode, per-codon limits with source attribution, time limits, and `onExceeded` policies. The captured plan below starts with a `$0.20` hank ceiling and proportional allocation. Each codon has a `$0.05` hard cap against a `$0.10` share, and the table notes both the cap and the share it came from. ```text Budget ───────────────────────────────────────────────────────────────── Global ceiling: $0.20 (hank) Time limit: 600s (hank) Allocation: proportional (unspent flows to later codons) Codon Model Max Dollars Max Time On exceeded ───── ───── ─────────── ──────── ─────────── write-line Claude Ha… $0.05 (codon cap, share was $0.10)120s (cap) completes write-second Claude Ha… $0.05 (codon cap, share was $0.10)120s (cap) ⚠ fails run ``` A tighter operator ceiling changes the global limit to `$0.10` and shows each share as `50% of $0.10`; the codon caps and policies stay as the author set them. The operator-tightened plan is captured separately: ```text Budget ───────────────────────────────────────────────────────────────── Global ceiling: $0.10 (--max-cost, hank wanted $0.20) Time limit: 600s (hank) Allocation: proportional (unspent flows to later codons) Codon Model Max Dollars Max Time On exceeded ───── ───── ─────────── ──────── ─────────── write-line Claude Ha… $0.05 (50% of $0.10) 120s (cap) completes write-second Claude Ha… $0.05 (50% of $0.10) 120s (cap) ⚠ fails run ``` ### Errors block execution | Error | What it means | | -------------------------------------------- | ---------------------------------------------------------------- | | Shares sum to more than `1.0` | More than the whole budget is allocated. | | Shares reference unknown IDs | A share key names no codon or loop. | | Shares without proportional allocation | `shares` is set without `proportional` or `proportional-strict`. | | Proportional allocation without `maxDollars` | There is no total to divide. | ### Warnings inform without blocking The validator warns about cases such as: * A codon or loop cap is greater than its proportional allocation. * Budget remains unallocated and no child is available to receive it. * `exhaustWithPrompt` is combined with a time budget. * A dollar budget names a model without provider-registry pricing; tracking reports `$0` and the dollar budget never trips. * Shares total 100% but this codon has no share, so its effective budget is `$0` and it immediately trips. * `onExceeded: "fail"` is combined with `onFailure: "retry"`. * A fail-policy codon comes late in a shared pool that earlier codons may exhaust. * A codon time cap is greater than its loop time cap. These checks distinguish a configuration that blocks the run from one that is valid but likely to surprise you. The resolved plan is the pre-spend inspection point; the runtime's later summary is a state-based report, not an invoice.