Cost & Budget Tracking
Every workflow run tracks token consumption and estimated cost in USD. Budgets can be set at the workflow or agent level. The runner enforces them automatically and fails the workflow if limits are exceeded.
How costs are tracked
Section titled “How costs are tracked”Each time a node completes an LLM call, the action metadata includes a token usage breakdown, as inputTokens, outputTokens, and totalTokens. The reducer accumulates these into two fields on the workflow state:
total_tokens_used: cumulative tokens across all LLM calls in the runtotal_cost_usd: cumulative estimated cost, calculated using the pricing table
Every LLM call is accounted for, not just successful agent nodes:
- Supervisor routing calls attach
token_usageandmodelto their handoff or completion actions, so supervisor loops count toward all budgets. - Failed attempts are counted too. The agent executor attaches best-effort
partialUsageto its errors, and the runner records it, so a node that retries N times cannot hide the tokens it burned on the failed tries. - Composite nodes (evolution, voting, map, annealing) aggregate the token usage of every internal call into their returned action.
Calculating costs:
import { calculateCost } from '@cycgraph/orchestrator';
const cost = calculateCost('claude-sonnet-4-6', inputTokens, outputTokens);Refs:
- calculateCost: Estimate USD cost for a model and token counts.
- MODEL_PRICING: The static per-model pricing table.
- Workflow State: where
total_tokens_usedandtotal_cost_usdaccumulate.
Setting budgets
Section titled “Setting budgets”Token budget
Section titled “Token budget”Set maxTokenBudget on the initial workflow state. The runner throws BudgetExceededError when cumulative tokens exceed the limit:
state({ // ... maxTokenBudget: 100_000,});Cost budget (USD)
Section titled “Cost budget (USD)”Set budgetUsd on the initial workflow state. The runner enforces this with threshold alerts and a hard stop at 100%:
state({ // ... budgetUsd: 0.50,});Agent-level budget
Section titled “Agent-level budget”Individual agents can have their own cost cap via permissions.budgetUsd:
agent({ // ... permissions: { budgetUsd: 0.10, },});Per-node budget
Section titled “Per-node budget”Any node can carry its own budget. The runner enforces it after the node completes, and breaching either cap throws NodeBudgetExceededError with no retry.
For composite nodes that loop internally (evolution generations, annealing iterations), the post-completion check alone would let the whole population times generations spend happen before the cap is consulted. These nodes also run an incremental budget guard between iterations: once accumulated token or cost spend crosses the node’s budget or the remaining workflow budget, the loop stops early instead of running every remaining generation. Evolution surfaces a {nodeId}_budget_stopped flag in its output envelope. The runner’s hard NodeBudgetExceededError still fires if the aggregate exceeded the cap, so the guard bounds the overspend rather than suppressing the error.
Refs:
- Workflow State: the
maxTokenBudgetandbudgetUsdrun-level ceilings. - Nodes: the per-node
NodeBudgetshape (maxTokens,maxCostUsd). - Agents: the agent
permissions.budgetUsdcap. - Error Handling:
BudgetExceededErrorandNodeBudgetExceededError.
Budget threshold alerts
Section titled “Budget threshold alerts”When budgetUsd is set, the runner emits budget:threshold_reached events as cost crosses 50%, 75%, 90%, and 100% of the budget. Each threshold fires only once per run.
runner.on('budget:threshold_reached', ({ threshold_pct, cost_usd, budget_usd }) => { console.warn(`${threshold_pct}% of $${budget_usd} budget used ($${cost_usd.toFixed(4)})`);});When streaming, these arrive as budget:threshold_reached stream events:
for await (const event of runner.stream()) { if (event.type === 'budget:threshold_reached') { console.warn(`${event.threshold_pct}% budget used`); }}At 100%, the workflow is terminated with BudgetExceededError and status transitions to failed.
Refs:
- Streaming: the
budget:threshold_reachedevent and itsrun_id,workflow_id,threshold_pct,cost_usd,budget_usdpayload.
Budget-aware model resolution
Section titled “Budget-aware model resolution”When agents use modelPreference and a ModelResolver is configured, the engine automatically selects the most capable model that fits within the remaining budget. This works hand-in-hand with the budget system described above.
Before each agent execution, the resolver:
- Estimates the cost of the preferred tier using conservative token budgets
- Compares against remaining budget (
budgetUsd - total_cost_usd) - Downgrades to a cheaper model if estimated cost exceeds 50% of remaining budget
Each resolution emits a model:resolved stream event with one of three reasons:
| Reason | Meaning |
|---|---|
preferred |
Budget is healthy; agent got its requested tier |
budget_downgrade |
Stepped down one tier to conserve budget |
budget_critical |
Forced to the lowest tier; budget is nearly exhausted |
for await (const event of runner.stream()) { if (event.type === 'model:resolved') { console.log(`${event.node_id}: ${event.reason} → ${event.resolved_model}`); }}A workflow with budgetUsd: 0.50 might start by using claude-opus-4-8 for early tasks, then automatically switch to claude-sonnet-4-6 or claude-haiku-4-5-20251001 as the budget depletes, without any manual intervention.
Refs:
- Budget-Aware Model Selection: the full setup guide for tiers and resolvers.
- Agents: the
modelPreferencecapability tier. - Streaming: the
model:resolvedevent and its payload fields.
Usage recording
Section titled “Usage recording”For production billing and reporting, implement the UsageRecorder interface to persist per-run usage records. Each record captures token counts, USD cost, and duration for one run, with an optional per-model breakdown. The @cycgraph/orchestrator-postgres package provides DrizzleUsageRecorder for durable storage.
Refs:
- Persistence: the
UsageRecorderinterface and itssaveUsageRecordmethod. - Persistence: the
UsageRecordfield table.
Pricing helpers live in @cycgraph/orchestrator. They back the cost tracking reducer and budget enforcement, and are exposed so hosts can sync custom pricing at startup.
calculateCost
Section titled “calculateCost”Estimate the USD cost of an LLM call. Returns 0 for unknown models and logs a warning once per model. Token counts are coerced to finite, non-negative values first.
function calculateCost(model: string, inputTokens: number, outputTokens: number): number;getModelPricing
Section titled “getModelPricing”Resolve the effective pricing for a model, checking runtime overrides before the static table. Returns undefined for unknown models.
function getModelPricing(model: string): ModelPricing | undefined;setModelPricing
Section titled “setModelPricing”Register or update pricing for a single model at runtime. Overrides take precedence over MODEL_PRICING. Throws if the values are not finite non-negative numbers.
function setModelPricing(model: string, pricing: ModelPricing): void;loadPricingTable
Section titled “loadPricingTable”Bulk-register pricing for many models at once, for example a table synced from an external source at host startup. Validates every entry before applying any, so a partially invalid table is rejected atomically.
function loadPricingTable(table: Record<string, ModelPricing>): void;clearPricingOverrides
Section titled “clearPricingOverrides”Remove all runtime pricing overrides, leaving the static table in place.
function clearPricingOverrides(): void;MODEL_PRICING
Section titled “MODEL_PRICING”The static per-model pricing table, keyed by model id. Prices are in USD per one million tokens.
const MODEL_PRICING: Readonly<Record<string, ModelPricing>>;Interfaces
Section titled “Interfaces”ModelPricing
Section titled “ModelPricing”Per-model pricing in USD per one million tokens.
| Field | Type | Description |
|---|---|---|
inputPerMToken |
number |
Cost per 1M input (prompt) tokens. |
outputPerMToken |
number |
Cost per 1M output (completion) tokens. |
Cross-referenced types
Section titled “Cross-referenced types”These shapes are owned by other pages. Follow the links for full field tables.
| Type | Documented on |
|---|---|
UsageRecord |
Persistence: the per-run cost and token record. |
UsageRecorder |
Persistence: the recorder interface. |
WorkflowState |
Workflow State: maxTokenBudget, budgetUsd, total_tokens_used, total_cost_usd. |
NodeBudget |
Nodes: the per-node maxTokens and maxCostUsd caps. |
Next steps
Section titled “Next steps”- Workflow State: where
total_tokens_usedandtotal_cost_usdlive - Streaming: real-time budget threshold events
- Error Handling:
BudgetExceededErrorand recovery - Persistence: durable usage recording