Workflow YAML Format
Complete schema reference for Binex workflow files.
Root — WorkflowSpec
| Field | Type | Required | Description |
|---|---|---|---|
version |
int |
no | Schema version (default: 1, must be >= 1). See Versioning below. |
name |
str |
yes | Workflow name |
description |
str |
no | Workflow description (default: "") |
nodes |
dict[str, NodeSpec] |
yes | Map of node_id to node definition |
defaults |
DefaultsSpec |
no | Default settings applied to all nodes |
budget |
BudgetConfig |
no | Budget constraints for the run (see below) |
webhook |
WebhookConfig |
no | Webhook notification target (see below) |
schedule |
str |
no | Cron expression (5-field) for scheduled execution |
concurrency |
int or dict[str, int] |
no | Cap on concurrent node execution (see below) |
mcp_servers |
dict[str, McpServerConfig] |
no | MCP server configurations (see below) |
Node — NodeSpec
| Field | Type | Required | Description |
|---|---|---|---|
id |
str |
no | Auto-set from the dict key |
agent |
str |
yes | Agent URI — local://, llm://, a2a://, human://, langchain://, crewai://, autogen://, or custom plugin prefix |
system_prompt |
str |
no | System prompt sent to the agent (supports file:// prefix) |
inputs |
dict[str, Any] |
no | Input key-value pairs; supports variable interpolation |
outputs |
list[str] |
yes | Artifact names this node produces |
depends_on |
list[str] |
no | Node IDs that must complete before this node runs |
config |
dict[str, Any] |
no | Per-node config forwarded to the adapter (see below) |
retry_policy |
RetryPolicy |
no | Override the default retry settings |
deadline_ms |
int |
no | Hard total-duration timeout for this node |
heartbeat_timeout_ms |
int |
no | Silence timeout for long nodes that report progress (see below) |
when |
str |
no | Conditional execution expression (see below) |
cost |
NodeCostHint |
no | Optional cost estimate for planning (see below) |
budget |
float or NodeBudget |
no | Per-node budget limit (shorthand: budget: 0.50, full: budget: { max_cost: 0.50 }) |
tools |
list[str] |
no | Tool URIs available to this node (see Tools below) |
output_schema |
dict |
no | JSON Schema for validating node output. Failed validation triggers auto-retry |
fallbacks |
list[str] |
no | Fallback models tried when the primary fails on an infrastructure error (see below) |
cache |
bool |
no | Reuse this node's cached result when its inputs are unchanged (see below) |
routing |
dict |
no | Per-node Gateway routing overrides (see below) |
config keys (LLM adapter)
| Key | Example | Effect |
|---|---|---|
api_base |
"http://localhost:11434" |
LiteLLM API base URL |
api_key |
"sk-..." |
Provider API key |
temperature |
0.7 |
Sampling temperature |
max_tokens |
4096 |
Max tokens in completion |
All config values are forwarded to litellm.acompletion() when not None.
External System Prompt — file://
The system_prompt field supports loading content from an external file using the file:// prefix.
Relative paths are resolved relative to the workflow YAML file's directory. Absolute paths are used as-is.
nodes:
researcher:
agent: "llm://openai/gpt-4"
system_prompt: "file://prompts/researcher.md"
outputs: [findings]
If the referenced file does not exist, workflow loading fails with a clear error message.
Conditional Execution — when
The when field enables conditional node execution based on upstream artifact values.
A node with a when condition is skipped (not failed) if the condition evaluates to false.
Skipped nodes count as resolved for downstream dependency purposes.
Operators:
| Operator | Example | Meaning |
|---|---|---|
== |
${review.decision} == approved |
Run only if artifact content equals "approved" |
!= |
${review.decision} != rejected |
Run only if artifact content does not equal "rejected" |
Example — approval gate with branching:
publish:
agent: "local://echo"
inputs:
final: "${revise.content}"
outputs: [result]
depends_on: [human_review]
when: "${human_review.decision} == approved"
discard:
agent: "local://echo"
inputs: {}
outputs: [notice]
depends_on: [human_review]
when: "${human_review.decision} == rejected"
The when field is commonly used with human://approve nodes but works with any artifact value.
Defaults — DefaultsSpec
| Field | Type | Default | Description |
|---|---|---|---|
deadline_ms |
int |
120000 |
Default deadline in milliseconds |
retry_policy |
RetryPolicy |
see below | Default retry policy for all nodes |
Retry — RetryPolicy
| Field | Type | Default | Description |
|---|---|---|---|
max_retries |
int |
1 |
Maximum retry attempts |
backoff |
"fixed" or "exponential" |
"exponential" |
Backoff strategy between retries |
Budget — BudgetConfig
Budget constraints limit the total cost of a workflow run. The orchestrator checks accumulated cost after each batch of nodes.
| Field | Type | Default | Description |
|---|---|---|---|
max_cost |
float |
— | Maximum allowed cost in the specified currency (must be > 0) |
currency |
str |
"USD" |
Currency code |
policy |
"stop" or "warn" |
"warn" |
What to do when budget is exceeded |
Budget Policies
| Policy | Behavior |
|---|---|
stop |
Skip all remaining nodes, set run status to "over_budget" |
warn |
Log a warning to stderr, continue execution |
Example:
name: budgeted-pipeline
budget:
max_cost: 5.0
policy: stop
nodes:
planner:
agent: "llm://gpt-4o"
outputs: [plan]
researcher:
agent: "llm://claude-sonnet-4-20250514"
outputs: [findings]
depends_on: [planner]
summarizer:
agent: "llm://gpt-4o"
outputs: [summary]
depends_on: [researcher]
If the accumulated cost exceeds $5.00 after the researcher node, the summarizer is skipped and the run status is "over_budget".
See the Budget & Cost Tracking Guide for more examples and patterns.
With --json, the run output includes budget information:
{
"status": "over_budget",
"total_cost": 5.23,
"budget": 5.0,
"remaining_budget": -0.23
}
Declaring Cost (non-LLM nodes)
Cost tracking isn't limited to LLM tokens. Cloud STT bills per minute, TTS per
character, image generation per request. A local:// / python:// handler
declares its own cost by accepting a report_cost parameter — the value flows
into the same cost records, dashboard, and budgets as token cost (budgets
operate on dollars, so enforcement is unchanged; only ingestion widens).
async def transcribe(task, inputs, report_cost):
audio_seconds = 7200
...
report_cost(seconds=audio_seconds, unit_price=0.0001) # $0.72
return [artifact]
report_cost accepts an explicit cost= in dollars, or a quantity +
unit_price (their product), with convenience unit keywords seconds=,
characters=, requests=. Records carry unit (tokens/seconds/
characters/requests/custom), quantity, unit_price, and provenance
(litellm/declared/manual). binex cost simulate leaves non-token costs
unchanged when swapping models (they don't scale with the model).
Long-Running Nodes & Progress
An LLM node streams tokens, so it's visibly alive. A local:// node running
Whisper on a two-hour track is silent for thirty minutes — and the default
deadline_ms (120 s) kills it. A node that reports progress is alive, so the
timeout can apply to silence rather than total duration.
heartbeat_timeout_ms— the node fails only if it produces no progress for this long.deadline_msstill applies as an optional hard total-duration cap.- A
local:///python://handler opts in by accepting areport_progressparameter and callingreport_progress(fraction, message)— e.g.report_progress(0.4, "transcribing 48/120 min"). Handlers that don't accept it are unchanged. - Subprocess /
a2a://agents report via the binex-trace SDK:trace.progress(fraction, message). - Progress surfaces as a
node:progressruntime event (per-node progress in the Web UI) and is captured as a trace event.
# local handler
async def transcribe(task, inputs, report_progress):
for i, chunk in enumerate(chunks):
...
report_progress(i / len(chunks), f"transcribing {i}/{len(chunks)}")
return [artifact]
nodes:
transcribe:
agent: "local://whisper"
outputs: [text]
heartbeat_timeout_ms: 120000 # fail only after 2 min of silence
deadline_ms: 7200000 # but never run longer than 2 h
Model Fallback Chains
A 40-minute run shouldn't die because of a single 429 or a provider outage.
fallbacks lists models to try, in order, when the primary fails:
nodes:
writer:
agent: "llm://gpt-4o"
outputs: [draft]
fallbacks: ["anthropic/claude-sonnet-4-5", "ollama/llama3.1"]
Order: retry the current model per its backoff policy → move to the next model →
its own retries. Fallback fires only on infrastructure/availability errors —
rate limit (429), 5xx, timeout, model-not-found, and auth (401, with a loud
warning, since the next provider uses a different key). It never fires on a model
that answered but poorly — that's auto-repair's job.
Reproducibility (silent model swaps would break diff/bisect/eval):
- Each execution records both
requested_modelandactual_model. - A
node:cache_hit-style fallback event is emitted (gpt-4o → claude: rate_limited) and stored on the artifact'smetadata.fallbacks. binex run --no-fallbackdisables the chain entirely, so a model benchmark can't be silently contaminated. Also available viaBINEX_NO_FALLBACK=1.binex validatewarns if a fallback has a smaller context window than the primary, or lacks function-calling while the node declares tools.
Streaming: if a stream dies mid-emission, the node restarts from scratch on the fallback model (no partial splicing).
Node Caching
Editing the prompt of node 7 shouldn't force re-running (and re-paying for)
nodes 1–6. With caching on, Binex reuses a node's stored result whenever nothing
that affects its output has changed — like make for pipelines.
The cache key is a content hash of the node's agent, resolved prompt, model
parameters, tool set, and the content of its input artifacts. Change any of
them and the node re-executes; leave them alone and it's served from cache at
$0, in a distinct trace event pointing back to the source run.
Caching is opt-in, because reusing a result isn't always safe (a
temperature > 0 model is intentionally nondeterministic; a local:// script may
have side effects). Two ways to enable it:
nodes:
transcribe:
agent: "local://whisper"
outputs: [text]
cache: true # always cache this node
binex run pipeline.yaml --cache # iteration mode: cache every node this run
binex run pipeline.yaml --offline # run ONLY from cache; a miss fails the node
--offline (implies --cache) is the VCR-style mode: record once, then iterate
on downstream logic for free and without network access. Clear the cache with
binex clean cache.
Concurrency
By default the orchestrator would dispatch every ready node at once. A wide
fan-out (e.g. a scatter pattern with 50 workers) then fires 50 simultaneous
LLM calls and trips provider rate limits. concurrency caps how many nodes run
at the same time.
Global cap (scalar):
name: wide-pipeline
concurrency: 8 # at most 8 nodes in flight at once
nodes:
...
Per-provider caps (mapping): the default key is the global cap; every
other key limits a single provider. A node holds a global slot and its
provider slot, so a local Ollama (one GPU) and a hosted API can coexist:
concurrency:
default: 8 # global cap (falls back to BINEX_MAX_CONCURRENCY, then 8)
openai: 5 # at most 5 concurrent openai calls
ollama: 1 # serialize the local model
The provider is derived from the agent URI: llm://openai/gpt-4o → openai,
llm://ollama/llama3 → ollama, and non-LLM agents fall back to their scheme
(local:// → local, a2a:// → a2a).
Precedence: the workflow concurrency field overrides the
BINEX_MAX_CONCURRENCY environment variable, which overrides the default of
8. All limits must be >= 1.
Node Cost Hint — NodeCostHint
Optional cost estimate for planning purposes. Does not affect execution — purely informational.
| Field | Type | Default | Description |
|---|---|---|---|
estimate |
float |
0.0 |
Estimated cost for this node (must be >= 0) |
nodes:
expensive_node:
agent: "llm://gpt-4o"
outputs: [result]
cost:
estimate: 2.50
Per-Node Budget — NodeBudget
Individual nodes can have their own budget limits. The policy is inherited from the workflow-level budget.policy (default: stop).
| Field | Type | Default | Description |
|---|---|---|---|
max_cost |
float |
— | Maximum allowed cost for this node (must be > 0) |
Shorthand: budget: 0.50 is equivalent to budget: { max_cost: 0.50 }.
When both workflow and node budgets are defined, the effective limit is min(node_budget, remaining_workflow_budget).
Pre-check before retry: If a node has a budget and fails, the orchestrator checks remaining budget before each retry attempt. With policy: stop, the retry is skipped if budget is exhausted. With policy: warn, the user is prompted via click.confirm().
Post-check after execution: After each execution, if the node's accumulated cost exceeds its budget, the policy determines behavior: stop discards the result and marks the node failed; warn keeps the result and logs a warning.
Example:
name: per-node-budget
budget:
max_cost: 10.00
policy: stop
nodes:
planner:
agent: "llm://gpt-4o-mini"
outputs: [plan]
budget: 0.50 # shorthand
researcher:
agent: "llm://gpt-4o"
outputs: [findings]
depends_on: [planner]
budget:
max_cost: 3.00 # full form
summarizer:
agent: "llm://gpt-4o"
outputs: [summary]
depends_on: [researcher]
budget: 2.00
If the planner costs $0.60 (exceeding its $0.50 limit), it is marked as failed and dependent nodes do not run.
Output Schema Validation
Nodes can define a JSON Schema to validate their output. If the output fails validation and the node has retries remaining, Binex automatically retries the node.
| Field | Type | Default | Description |
|---|---|---|---|
output_schema |
dict |
None |
Standard JSON Schema object |
Example:
nodes:
extractor:
agent: "llm://openai/gpt-4o"
system_prompt: "Extract structured data. Return valid JSON."
outputs: [result]
output_schema:
type: object
properties:
title:
type: string
score:
type: number
minimum: 0
maximum: 100
required:
- title
- score
retry_policy:
max_retries: 2
If the LLM returns output that doesn't match the schema (e.g., missing title field or score out of range), the node is retried automatically. After all retries are exhausted, the node fails with a validation error.
The validator handles both JSON string output (parsed first) and dict output (validated directly).
Auto-Repair
Blindly re-running a node on invalid output usually makes the model repeat the
same mistake — and you pay twice. When a node has an output_schema, Binex
applies a repair ladder, cheapest first:
- Deterministic repair (0 tokens, always on). Most "invalid JSON" is just a markdown code fence around the payload, prose before/after it, or a trailing comma. Binex strips the fence, extracts the first balanced JSON value, and re-parses — before any model call. Applies to every agent type, and the cleaned JSON replaces the artifact content so downstream nodes get valid data.
- Native structured output. For
llm://nodes whose model supports it, Binex passes the schema into the completion call (response_format), so malformed output mostly never happens. Detected per-model; silently skipped when unsupported. - Feedback loop (
repair.max_attempts). If output is still invalid, Binex appends the model's answer plus the validation errors to the conversation and re-asks up tomax_attemptstimes.local://anda2a://nodes keep fail-fast semantics — there's no model to ask.
nodes:
extract:
agent: "llm://openai/gpt-4o-mini"
outputs: [result]
output_schema: { type: object, required: [title] }
fallbacks: ["openai/gpt-4o"]
repair:
max_attempts: 2 # feedback-loop attempts; deterministic repair is always on
escalate: true # on repair exhaustion, retry the ladder on the next fallback model
| Field | Type | Default | Description |
|---|---|---|---|
repair.max_attempts |
int |
0 |
LLM feedback-loop attempts on schema-invalid output |
repair.escalate |
bool |
false |
On repair exhaustion, promote to the next fallback model and retry the ladder |
Every repair attempt's tokens are counted in the run cost. The produced
artifact records metadata.repair_attempts and which ladder step succeeded; on
exhaustion the node fails with the validation errors.
Escalation (repair.escalate). When the feedback loop is exhausted, that's
a signal the model can't handle the schema — not a transient error. With
escalate: true and a fallback chain, Binex promotes
to the next model and restarts the repair ladder there (trace event
escalated: schema_repair_exhausted, distinct from a transport-error fallback).
This turns repair + fallback into an automatic cost optimizer: route everything
through a cheap model and let the strong model catch only the hard tail.
--no-fallback disables escalation too.
Routing Overrides
When a Gateway is configured (either embedded via gateway.yaml or standalone via --gateway), individual nodes can override the default routing behavior using the routing field:
nodes:
critical_search:
agent: "a2a://research"
routing:
prefer: lowest_latency
timeout_ms: 10000
retry_count: 5
failover: true
| Key | Type | Default | Description |
|---|---|---|---|
prefer |
str |
"highest_priority" |
Selection strategy (highest_priority, lowest_latency, round_robin) |
timeout_ms |
int |
null |
Override request timeout for this node |
retry_count |
int |
null |
Override retry count for this node |
failover |
bool |
null |
Override failover setting for this node |
Routing overrides only apply when a Gateway is configured. Without a Gateway, the routing field is ignored.
Tools
Nodes can declare tools that are made available to the LLM during execution. Three URI schemes are supported:
| Scheme | Example | Description |
|---|---|---|
builtin:// |
builtin://web_search |
One of 10 built-in tools |
mcp:// |
mcp://my-server |
All tools from a configured MCP server |
python:// |
python://my_module.my_func |
Custom Python function decorated with @tool |
Built-in tools (10)
| Category | Tools |
|---|---|
| Data | calculator, json_parse, random_choice, dice_roll |
| Web | fetch_url, http_request, web_search |
| Files | read_file, write_file |
| System | shell_command |
Example:
nodes:
researcher:
agent: "llm://openai/gpt-4o"
system_prompt: "Research the topic using web search"
tools:
- "builtin://web_search"
- "builtin://fetch_url"
- "builtin://calculator"
outputs: [findings]
MCP Servers — McpServerConfig
MCP (Model Context Protocol) servers provide additional tools to LLM nodes. Configure them at the workflow level, then reference them in node tools lists.
| Field | Type | Required | Description |
|---|---|---|---|
command |
str |
stdio only | Command to launch the MCP server |
args |
list[str] |
no | Arguments for the command |
env |
dict[str, str] |
no | Environment variables |
url |
str |
HTTP only | URL of a running MCP server |
Example — stdio transport:
mcp_servers:
file-search:
command: npx
args: ["-y", "@anthropic/mcp-file-search"]
code-tools:
command: python
args: ["-m", "my_mcp_server"]
nodes:
coder:
agent: "llm://anthropic/claude-sonnet-4-20250514"
tools:
- "mcp://file-search"
- "mcp://code-tools"
- "builtin://shell_command"
outputs: [code]
Example — HTTP transport:
mcp_servers:
remote-api:
url: "http://localhost:3000/mcp"
Schedule — Cron Expression
The schedule field accepts a standard 5-field cron expression. Workflows with a schedule are automatically discovered and executed by binex scheduler start.
name: hourly-report
schedule: "0 * * * *"
nodes:
reporter:
agent: "llm://openai/gpt-4o-mini"
system_prompt: "Generate hourly status report"
outputs: [report]
Variable Interpolation
Two variable scopes are available inside inputs values:
| Syntax | Resolved | Description |
|---|---|---|
${user.<key>} |
Load time | Substituted from --var CLI arguments |
${<node_id>.<output>} |
Runtime | References an artifact produced by another node |
Example:
inputs:
query: "${user.query}" # --var query="LLM agents"
plan: "${planner.execution_plan}" # artifact from the planner node
Warning
Use ${planner.plan} (node ID + output name), not ${node.planner.plan}. The node. prefix is not supported.
Minimal Valid Workflow
name: minimal
nodes:
only_node:
agent: "local://echo"
system_prompt: ping
inputs:
msg: "hello"
outputs: [response]
No defaults, description, depends_on, or config required.
Versioning
Workflow files support schema versioning via the version field. This enables future schema changes with automatic migration.
version: 1
name: my-workflow
nodes:
step1:
agent: "local://echo"
outputs: [result]
Behavior
- Missing
versionfield: Defaults to version 1 (backward compatible with all existing workflows). A warning is logged. version: 1: Current version — no migration needed.version > CURRENT_VERSION: RaisesUnsupportedVersionErrorat load time. Upgrade Binex to use newer workflows.
Migration framework
When Binex upgrades its schema version, a migration chain transforms older workflow dicts step by step (v1 → v2 → ... → current). Migrations run in-memory at load time — the original YAML file is never modified.
Workflow snapshots
Every binex run stores a normalized, SHA256-deduplicated snapshot of the workflow definition in SQLite. This lets you:
- Inspect the exact workflow used in any past run via
binex debug <run-id> - Compare workflows between runs via
binex workflow diff <run1> <run2> - Reproduce runs even if the original YAML file has changed
Check the workflow version of any file:
binex workflow version examples/simple.yaml
## Webhook — `WebhookConfig`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `url` | `str` | yes | Webhook endpoint URL |
Webhook notifications are sent on run completion, failure, or budget exceeded. Can also be set via `BINEX_WEBHOOK_URL` environment variable.
```yaml
name: notified-pipeline
webhook:
url: "https://hooks.example.com/binex"
nodes:
step1:
agent: "local://echo"
outputs: [result]