Recipe Specification
Declarative YAML pipelines for multi-step analysis workflows
Formal YAML schema reference for AIXBT recipes (v1.1). Recipes run on all surfaces: CLI, MCP, and REST API. Examples below use CLI syntax, but the YAML is the same everywhere.
1. Overview
A recipe is a declarative YAML file that defines a multi-step data pipeline. Recipes fetch data from the AIXBT API (and optionally external providers for enrichment), iterate over results, apply transforms to control output size, and yield execution to agents for inference. The CLI handles all deterministic work. The recipe author defines what data to gather and when to involve an agent.
2. Recipe Structure
Top-level fields:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Recipe identifier |
version | string | no | Recipe version (defaults to "1.0", number also accepted and coerced to string) |
description | string | no | Human-readable description (defaults to "") |
estimatedTokens | number | null | no | Author estimate of output token count. Informational only. |
params | object | no | Parameter definitions (see Parameters) |
requiredOneOf | string[] | no | Exactly one of these params must be provided (see Parameters) |
steps | array | yes | Step definitions (must be non-empty) |
hints | object | no | Structural hints for data consumers (see Hints) |
analysis | object | no | Analysis instructions for the consuming agent (see Analysis) |
Minimal valid recipe:
name: example
version: "1.0"
steps:
- id: projects
type: api
action: projects
params:
limit: 53. Parameters
Runtime parameters, provided via --<name> <value> flags:
| Field | Type | Required | Description |
|---|---|---|---|
type | "string" | "number" | "boolean" | yes | Parameter data type |
required | boolean | no | Whether the caller must provide this parameter |
description | string | no | Human-readable description |
default | string | number | boolean | no | Default value if not provided |
Parameters are referenced in steps via {params.<name>}. A required parameter with no default causes an error if omitted. Template resolution skips params that were not provided: "{params.tickers}" resolves to nothing if --tickers was not passed, so the API param is omitted from the request.
params:
chain:
type: string
required: true
description: Blockchain to scan
limit:
type: number
default: 10envelope (sampled-step budget lever)
Recipes with sampled steps (see Sample) can declare an envelope param — the caller-facing budget lever for the total tokens spent on sampled steps per item:
params:
envelope:
type: number
description: Total token budget for sampled steps; per-step budgets scale to envelope/N.
default: 150000- It is an ordinary
numberparam — not part ofrequiredOneOf— so it never affects the exactly-one validation. - The engine divides
envelopeby N (the number of items processed) and scales each sampled step'stokenBudgetproportionally, preserving the ratios the author declared and respecting each step's ceiling. See the two-lever model under Sample. - Default 150k covers direct callers (CLI, Indigo, Kimi, MCP). The Violet/Ultraviolet materialise path raises it to 500k for large-context answer models. Authors set the per-step ceilings + weights; callers (or the platform) set
envelope.
requiredOneOf
For params where the user must provide exactly one of several alternatives, use requiredOneOf at the top level:
params:
projectIds: { type: string, description: "Comma-separated project IDs" }
tickers: { type: string, description: "Comma-separated ticker symbols (e.g. SOL,ETH)" }
names: { type: string, description: "Comma-separated project names" }
address: { type: string, description: "Token contract address" }
requiredOneOf: [projectIds, tickers, names, address]Rules:
- The list must contain at least 2 param names
- Each name must be a defined param
- Params in the list must not have
required: true(conflicts with oneOf semantics) - At runtime, zero provided → error. Two or more provided → error.
The step passes all params through; template resolution skips the ones not provided:
- id: projects
type: api
action: projects
params:
projectIds: "{params.projectIds}"
tickers: "{params.tickers}"
names: "{params.names}"
address: "{params.address}"aixbt recipe run project_deep_dive --tickers SOL
aixbt recipe run project_deep_dive --names "solana"
aixbt recipe run project_deep_dive --address 0x...4. Step Types
Every step requires a type field: either api or agent.
API Steps (type: api)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique step identifier |
type | "api" | yes | Literal string identifying this as an API step |
action | string | yes | Action name (e.g., projects, intel, price) |
source | string | no | Provider name. Defaults to "aixbt". |
params | object | no | Parameters with template support |
transform | TransformBlock | no | Transform applied to the response data |
projection | string | no | Projection profile (e.g. signal.compact, project.broad) that reshapes AIXBT signal/project rows into a compact, model-visible shape before select runs. Either a literal profile name or a {params.X} template (see Caller-selectable projection). AIXBT-source steps only. See Projection Profiles. |
allowedProjections | string[] | no | Closed menu a template ({params.X}) projection may resolve to. Required (non-empty) when projection is a template; forbidden on a literal projection. Every entry must be a real profile valid for the step's action. See Caller-selectable projection. |
for | string | no | Iteration modifier (see Iteration with for:) |
fallback | string | no | Message for agent when step is skipped due to missing/insufficient provider key |
- id: projects
type: api
action: top-projects-climbing
params:
limit: 10
- id: price_data
type: api
action: chart
source: market
params:
geckoId: bitcoin
limit: 30
fallback: "Use publicly available price data instead."Agent Steps (type: agent)
Yield execution to an external agent for inference, analysis, or decision-making.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique step identifier |
type | "agent" | yes | Literal string identifying this as an agent step |
context | string[] | yes | Step IDs whose data to include |
instructions | string | yes | Detailed instructions for the agent |
returns | object | yes | Map of field names to type strings |
for | string | no | If present, creates a parallel agent step (see below) |
Pauses execution and yields to an external agent. The returns type strings: "string", "number", "boolean", "string[]", "object".
- id: picks
type: agent
context:
- recent_intel
- top_projects
- clusters
instructions: |
Select 3-5 projects with the strongest recent intel reinforcements.
Focus on projects with rising momentum and multiple intel reinforcements.
returns:
projectIds: "string[]"
rationale: "string"Iteration with for:
The for: modifier works on both API and agent steps. It iterates over an array, executing the step for each item.
The for value is a bare reference (no curly braces) to an array-valued step result. Within a for: step, {item} and {item.field} reference the current iteration item.
API step with for: executes an API call per item:
- id: momentum
type: api
for: projects.data
action: momentum
params:
id: "{item.id}"
start: "-7d"
includeClusters: "false"When individual items fail in a step with for: and fallback, those items degrade gracefully instead of failing the step. See Fallback Mechanism.
Agent step with for: creates a fan-out pattern where the agent processes items in parallel:
- id: analyze
type: agent
for: projects.data
context:
- projects
- momentum
- intel
instructions: |
For each project, analyze its momentum trajectory and intel reinforcements.
Produce a brief assessment of risk and opportunity.
returns:
assessment: "string"
risk_level: "string"Transforms on for: API steps run per-iteration.
5. Provider System
The action/source pair determines which provider handles a step.
Virtual Providers
Virtual providers are provider-agnostic entry points that route to the best concrete backend. Use these in recipes for portability and smart routing.
source value | Provider | What it wraps |
|---|---|---|
"market" | Market | CoinGecko/GeckoTerminal + DexPaprika: on-chain price, pools, OHLCV |
"security" | Security | GoPlus: token and address security analysis |
"defi" | DeFi | DeFiLlama: TVL, protocols, emissions, yields |
Dotted Syntax
For shared actions that exist on multiple backends, use dotted syntax to force a specific one:
- id: price_data
type: api
action: token-ohlcv
source: market.coingecko # forces CoinGecko instead of default DexPaprikaWithout the dot, the virtual provider picks its default backend.
Concrete Providers
You can also use concrete providers directly when you need a specific backend or actions not exposed through virtual providers:
source value | Provider |
|---|---|
omitted or "aixbt" | AIXBT API (default) |
"coingecko" | CoinGecko / GeckoTerminal |
"dexpaprika" | DexPaprika |
"defillama" | DeFiLlama |
"goplus" | GoPlus |
Tier Requirements
Each action has a minimum tier (free, demo, or pro). If the configured key's tier is too low, the step degrades gracefully. Add a fallback field to control the message the agent receives. See Fallback Mechanism.
6. Fallback Mechanism
The fallback field on API steps (including those with for:) provides graceful degradation when a provider key is missing or the tier is insufficient. The string you write becomes an instruction to the agent running the recipe, telling it what to do instead.
API steps: the agent receives a message prefixed with context:
Step "<id>" was skipped: no <source> API key configured. <your fallback text>- id: price_data
type: api
action: chart
source: market
params:
network: ethereum
address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
timeframe: day
limit: 30
fallback: "Use publicly available price data instead."The agent sees: Step "price_data" was skipped: no market API key configured. Use publicly available price data instead.
for: steps: items that succeed return data normally. Items that fail degrade gracefully, and the agent receives a collated note listing which items need alternative handling:
<your fallback text> for: <item1>, <item2>, <item3>- id: tvl
type: api
for: projects.data
action: protocol
source: defi
params: { protocol: "{item.name}" }
fallback: "Look up TVL data using available tools."If 3 of 10 items fail, the agent sees: Look up TVL data using available tools for: Uniswap, Aave, Compound
Without fallback, a provider unavailability (missing key, tier too low) still degrades gracefully rather than failing the recipe, but you lose control over the instruction.
7. Transforms
Transforms reduce and reshape data before it reaches agents, controlling token consumption.
Transform Block
| Field | Type | Description |
|---|---|---|
select | string[] | Field projection: keep only these fields |
sample | SampleTransform | Weighted random sampling |
At least one of select or sample should be present (both can be used together).
Execution order: sample → projection → select. Sampling runs first (on the full rows, so weight fields are available even if later dropped); a step's projection profile then reshapes each row; select narrows the projected shape last. Without a projection, the order is sample → select.
Select
Selects specified fields from each item. Supports dot notation for nested fields.
transform:
select:
- id
- name
- metrics.usd
- metrics.volumeProduces: { id: "...", name: "...", metrics: { usd: ..., volume: ... } }. Nested structure is preserved.
Sample
Weighted random sampling that controls output size by item count or token budget.
| Field | Type | Required | Description |
|---|---|---|---|
count | number | no* | Fixed number of items to sample |
tokenBudget | number | no* | Per-step token ceiling and relative weight, not a fixed allocation. When a recipe declares an envelope param and runs over multiple items (N), each step's budget is scaled to envelope / N while preserving the ratios between steps and never exceeding the declared tokenBudget ceiling. With no envelope, the declared value is used as-is. |
guaranteePercent | number | no | Fraction (0-1) of top-weighted items always included (default: 0.3) |
guaranteeCount | number | no | Fixed number of top-weighted items always included, clamped to the sample's targetCount. At high N the scaled budget can shrink the sample below the requested guarantee, so the effective guarantee never exceeds what the budget admits. |
weight_by | string | no | Field path for custom weights (dot notation supported) |
Rules:
- At least one of
countortokenBudgetis required - If both are specified,
counttakes precedence guaranteePercentandguaranteeCountare mutually exclusive- Guaranteed items are selected first (top by weight), then remaining slots are filled by weighted random sampling without replacement
guaranteeCountis clamped to both the available item count and the sample'stargetCount— an absolute guarantee cannot force more items than the (possibly scaled-down) budget allows- Default weighting (when
weight_byis omitted): favors recent, high-activity items
Two-lever budget model. Sampled steps have two independent budget levers:
- Author lever — the per-step
tokenBudgetsets a ceiling and a relative weight. Two sampled steps with50000each split any shared envelope equally;60000vs40000split it 60/40. - Caller lever — the
enveloperecipe param (see Parameters) sets the total budget across the sampled steps for a single item, divided by N (the number of items being processed). At low N the per-step ceilings apply unchanged; at high N every sampled step scales proportionally toenvelope / N(with a small per-item floor) so a wide fan-out stays bounded instead of multiplying the per-step ceilings by N.
transform:
sample:
tokenBudget: 25000
guaranteePercent: 0.3
weight_by: spikingScore
select:
- id
- name
- description
- spikingScore8. Projection Profiles
A projection profile is a named, model-visible shape for AIXBT signal or project data. Instead of hand-authoring a long transform.select on every signal/project step, set projection: <profile> on the step. The engine reshapes each row into that profile's compact form, dropping the largest fields (full cluster lists, activity ledgers, raw CoinGecko blobs, embedded intel). A transform.select can still narrow further.
Projection is AIXBT-only: it applies to steps whose source is aixbt (the default). It is not valid on external-provider steps or raw-path actions.
Where projection runs
Projection slots between sample and select (see Transforms):
sample → projection → select
sampleruns first on the full rows, so weighting still sees pre-projection fields (e.g.observationCount).projectionthen reshapes each surviving row to the profile's shape.select, if present, narrows the projected shape further.
Because of this order, a select can only keep fields the profile emits; see the select rule.
Signal profiles
Valid on the intel and signals actions. Every signal profile carries the same base scalars (id, detectedAt, reinforcedAt, description, projectName, projectId, category, observationCount, hasOfficialSource, sentiment, plus metrics when enrichment attached it). They differ only in how they represent clusters, citations, and the activity ledger:
| Profile | Cluster / citation / activity shape | Drops | Fetch default |
|---|---|---|---|
signal.compact | clusterCount (count only) | clusters, activity, citations, headline | activity: none |
signal.clustered | full clusters[] (cluster identities) | clusterCount, activity, citations, headline | activity: none |
signal.cited | clusterCount + citations[] | clusters, activity, headline | activity: none |
signal.timeline | minimal row: id, detectedAt, description, category only | everything else | activity: none |
signal.activity | full clusters[] + full activity[] ledger | citations, headline | activity: observations |
Project profiles
Valid on the project, projects, and top-projects-climbing actions (each profile lists the subset it allows). All four compact coingeckoData to { apiId, symbol, slug, categories }, always dropping image and contractAddress. Every other top-level project field (name, rank, the score fields, momentumContext, metrics, tokens, and so on) passes through unchanged:
| Profile | coingeckoData | Embedded signals/intel | Valid actions | Fetch default |
|---|---|---|---|---|
project.broad | compact (no description) | dropped | projects, top-projects-climbing | intelActivity: none |
project.market | compact (no description) | dropped | project, projects, top-projects-climbing | intelActivity: none |
project.narrow | compact, keeps description | dropped | project, projects | intelActivity: none |
project.leaderboard | compact (no description) | kept, each compacted to the signal.compact shape | projects | intelActivity: none |
Fetch defaults (activity mode)
Each profile also sets a fetch default (the "Fetch default" column above) so the API skips data the shape discards. These are defaults only: an explicit params.activity or params.intelActivity on the step overrides them. So projection: signal.compact with params: { activity: all } keeps the compact shape but still fetches the full ledger.
Caller-selectable projection
Instead of fixing the profile, a step can let the caller choose it at invocation by templating the field — projection: "{params.<name>}" — bounded by a closed allowedProjections menu:
params:
detail:
type: string
default: signal.clustered # must be a member of allowedProjections
steps:
- id: feed
type: api
action: intel
projection: "{params.detail}"
allowedProjections: [signal.clustered, signal.activity, signal.cited]The engine resolves the template once per run, then checks the result against allowedProjections. Pass --detail signal.activity to fetch the activity ledger; omit it and the param default (signal.clustered) applies. A value outside the menu — e.g. --detail signal.compact — aborts the run with an error naming the rejected value and the allowed set, so a caller (or planner) can self-correct in one shot.
Authoring rules (enforced by aixbt recipe validate):
- A template
projectionrequires a non-emptyallowedProjections; every entry must be a real profile valid for the step'saction. - The referenced param must exist, be
type: string, anddefaultto a member ofallowedProjections(so an un-passed param still resolves in-menu). - A literal
projectionmust not carryallowedProjections(there is no menu to constrain). allowedProjectionsrequires a templateprojection; a menu with no template — or on a raw-path action — is rejected.- Only the whole-field
{params.X}form is allowed: no partial strings (signal.{params.x}), no{item.*}.
select must match the profile
Because projection runs before select, and select can only keep fields that already exist, a transform.select on a projected step may only name fields the profile emits. Naming a field the profile dropped silently yields nothing, and there is no validation error for it.
Common pitfalls:
- On
signal.compact/signal.cited, selectclusterCount, notclusters: those profiles emit the count, not the list. Usesignal.clusteredif you need full cluster identities. signal.citedkeepscitationsonly if yourselectincludescitations.signal.timelineemits onlyid,detectedAt,description,category; selecting anything else drops it.- Embedded signals survive only under
project.leaderboard; selectingsignalsonproject.broad/narrow/marketyields nothing.
A step with a projection and no select passes the whole profile shape through.
The built-in recipe registry is guarded by a test that enforces this select-matches-profile rule. Saved (user-authored) recipes are not, and a mismatched select fails silently, so follow the rules above when authoring your own.
Validation
aixbt recipe validate rejects:
- an unknown profile name;
- a profile used on an action it does not support (the error lists the allowed actions);
- a
projectionon a raw-path action (e.g./v2/...); - a
projectionon a non-aixbtsource; - a template
projection({params.X}) without a non-emptyallowedProjections, or with a menu entry that is unknown or invalid for the action; - a literal
projectionthat carriesallowedProjections, or anallowedProjectionsmenu with no templateprojection; - a malformed
projectiontemplate (anything other than a whole-field{params.X}— e.g. partial strings or{item.*}); - a referenced projection param that is missing, not
type: string, or whosedefaultis not inallowedProjections.
Examples
# Broad intel scan: compact signals, cluster breadth as a count.
- id: intel
type: api
action: intel
params: { reinforcedAfter: "-48h", limit: 50 }
projection: signal.compact
# Verification-focused: keep citations (must be named in select).
- id: catalysts
type: api
action: intel
projection: signal.cited
transform:
select: [description, category, citations]
# Project list: compact CoinGecko, no embedded signals.
- id: top_projects
type: api
action: projects
params: { limit: 10 }
projection: project.broad9. Variable Templating
Template expressions use {...} syntax and are resolved in action, params, for, fallback, and analysis fields. They are not resolved in hints (passed through verbatim).
Expression Reference
| Expression | Resolves to |
|---|---|
{params.name} | Value of recipe parameter name |
{step_id} | Step result data (shorthand for {step_id.data}) |
{step_id.data} | Step result data (explicit) |
{step_id.field} | Shorthand for {step_id.data.field} |
{step_id.data.nested.path} | Nested field access |
{step_id.data[*].field} | Pluck: extract field from every array item |
{step_id[*].field} | Shorthand pluck (equivalent to above) |
{item} | Current for: iteration item |
{item.field} | Property of the current for: item |
Relative Time Expressions
Bare relative time strings are converted to ISO 8601 timestamps at execution time:
| Expression | Meaning |
|---|---|
-30m | 30 minutes ago |
-24h | 24 hours ago |
-7d | 7 days ago |
params:
detectedAfter: "-48h" # direct, resolved at execution
start: "{params.lookback}" # template: if lookback="-7d", resolved to ISO timestampType Preservation
When an entire param value is a single template expression ("{step_id.field}"), the resolved type is preserved (arrays stay arrays, numbers stay numbers). When the expression is part of a larger string ("prefix {value} suffix"), it's interpolated as a string.
10. Segment Boundary Rule
Recipes are divided into segments by agent steps. This is the most important structural constraint to understand when writing recipes.
How segments work
Segment 0: [api_step_1] → [api_step_with_for] → [agent_step_1]
| yield
Segment 1: [api_step_2] → [api_step_3]
^ can access: agent_step_1 output + own segment steps
x cannot access: api_step_1, api_step_with_forAn agent step ends a segment. The next segment begins after the agent step, and can access:
- The preceding agent step's output (the
returnsdata provided at resume) - Steps within its own segment that precede it
It cannot access steps from earlier segments (before the agent step). This is enforced at parse time.
Carry-forward context
When a recipe uses --agent, each spawned agent subprocess only sees the current yield's data. It has no memory of prior segments. If a downstream agent step or the analysis block references data from a pre-yield step, that data would be missing.
The engine solves this automatically: at each yield point, it scans downstream agent context arrays and analysis.context to identify which pre-yield step results are needed, bundles them into the yield output, and restores them on resume. Just reference the step ID in a context array.
Segment 0: [projects] → [intel] → [picks (agent)]
| yield (carries: projects, intel)
Segment 1: [details] → [final (agent, context: [projects, picks])]
^ projects restored from carry-forwardWithout --agent (yield/resume mode), the orchestrating agent already has full conversation context across all yields. Carry-forward data appears in the yield output as a hint about which prior steps matter downstream, but the agent doesn't need to pass it back. It can reference prior data from its own context.
Best practice: Always include accurate context arrays on agent steps and the analysis block, even if you only use yield/resume mode today. This ensures your recipes work correctly with --agent and makes the data dependencies explicit.
The segment boundary rule still applies to API step params references. An API step in segment 1 cannot template from an API step in segment 0. Carry-forward only transports data for agent context arrays and analysis.context.
11. Hints and Analysis Blocks
Hints
Passed through verbatim to the consuming agent; the CLI does not act on hints. They tell the agent how to interpret the data.
| Field | Type | Description |
|---|---|---|
combine | string[] | Step IDs whose data represents the same entities |
key | string | Shared field that relates the combined datasets |
include | string[] | Step IDs to include as reference data alongside combined |
hints:
combine:
- projects
- momentum
key: id
include:
- clustersAnalysis
Instructions for the consuming agent's final analysis. Template expressions ({params.*}) are resolved at execution time.
| Field | Type | Description |
|---|---|---|
instructions | string | Main analysis instructions |
context | string[] | Step IDs whose data is needed for final analysis. Triggers carry-forward across yield boundaries in --agent mode. In yield/resume mode, indicates which prior data the agent should reference. |
output | string | Output format directive |
analysis:
instructions: |
Analyze the momentum patterns for {params.chain} projects.
Focus on divergences between momentum score and price action.
context: [projects, momentum]
output: markdown12. Recipe Writing Guide
Composition patterns
Single-segment (no agent): Pure data pipeline. All steps execute and the result is returned directly. Good for data collection and enrichment without inference.
steps:
- id: protocols
type: api
action: protocols
source: defi
transform:
sample:
count: 20
select: [name, tvl, chain, category]
- id: chains
type: api
action: chains
source: defiAgent gate (two segments): Broad data fetch → agent picks → deep enrichment on picks. The most common pattern.
steps:
# Segment 0: broad scan
- id: intel
type: api
action: intel
params: { reinforcedAfter: "-48h", limit: 50 }
- id: top_projects
type: api
action: top-projects-climbing
params: { limit: 10 }
- id: picks
type: agent
context: [intel, top_projects]
instructions: "Select 3-5 standout projects..."
returns: { projectIds: "string[]" }
# Segment 1: targeted enrichment
- id: details
type: api
action: projects
params: { projectIds: "{picks.projectIds}" }
- id: narrative
type: api
for: details.data
action: intel
params:
projectIds: "{item.id}"
detectedAfter: "-30d"
transform:
sample: { tokenBudget: 25000, guaranteePercent: 0.3 }
select: [description, detectedAt, category]Multi-provider enrichment: Combine AIXBT data with virtual providers in a single pipeline.
steps:
- id: projects
type: api
action: projects
params: { limit: 10, hasToken: "true" }
- id: tvl
type: api
for: projects.data
action: protocol
source: defi
params: { protocol: "{item.name}" }
fallback: "TVL data unavailable for this project."
transform:
select: [name, tvl, chainTvls]Appendix: Validation
Run aixbt recipe validate <file> to check a recipe for errors without executing it.