Skip to main content
A binding is a ${…} token in a step’s fields. It is replaced with a value just before the step runs.
Bindings resolve in values, never in property names. A token in a key is refused at write time with invalid_key — in a key it would reach the provider as literal text.

Roots

A token names one of five roots, followed by an optional dotted path. A token whose first alternative names none of these roots is not a binding and is left as literal text. ${1+1}, ${VAR:-default} and an unterminated ${ all pass through untouched rather than raising.

Paths

A path is split on . and each segment is looked up verbatim.
There is no bracket form. ${steps.x.output[0].url} reads a key literally named output[0], resolves to undefined, and does not raise — the step still runs and is still billed with a missing input. output.0 is the only spelling for an array element.
A reference that resolves to nothing is undefined as a whole value, and blank when interpolated into surrounding text. It is never a run-time error, which is why the refusals that catch these at write time matter.

Whole token vs. interpolation

If the entire string is one token, the raw value is returned with its type intact — an array stays an array, a number stays a number. That is what lets foreach.items take "${steps.search.output.results}". If the token sits inside other text, its value is coerced to a string; a non-string is JSON-encoded first.

* — projection

${steps.loop.output.*.text} applies the rest of the path to every element and returns an array.
  • Array-only. Projecting over anything that is not an array yields undefined. This is why a parallel fan-out — which publishes an object keyed by branch — has no projection form.
  • One * per path. A second one yields undefined rather than guessing a level.
  • null and undefined holes are dropped, not kept — so a loop running under failure_mode: "continue" projects only the iterations that succeeded.
  • A trailing .join segment joins the projection into one block of text separated by blank lines: ${steps.loop.output.*.text.join}. Off a projection, join is just a key name.

| — fallbacks

A token may carry alternatives, resolved left to right; the first one that has a value wins.
  • Missing means undefined, null or the empty string — and nothing else. 0 and false are values and win.
  • A double-quoted literal always has a value, so it wins outright and must be last.
  • Inside a literal, | and } are ordinary characters: ${input.x | "a|b"} has two alternatives, and ${input.x | "a}b"} is one token. A literal cannot itself contain a " — there is no escape syntax.
Every alternative is checked at write time, not just the first. A fallback does not excuse an out-of-scope or forward reference in an earlier alternative — see Containers.

Step ids are path segments

A step id may contain only A–Z, a–z, 0–9, _ and -, and may be up to 120 characters. The reason is this page: an id is a path segment, so a . would split it, a ", | or } would end the token early, and a * is the projection operator. Each of those resolves to nothing silently, on a step that still bills. Ids must be unique across the whole definition — including steps nested inside a foreach body or a parallel branch.

Naming an artifact

When a step produces a file, the file handle rides beside the step’s output, not inside it. Bind it as outputRef: A container publishes no single artifact of its own, so ${steps.<loop>.outputRef} and ${steps.<par>.outputRef} name nothing. Which step types publish a ref at all: For a capability, the tell is its output_modality, published on every entry of GET /v1/tools:
document.extract_text is the exception, and the only conditional one. Its output_modality is text, but it does publish an outputRef whenever the extraction returns structured JSON — that is, whenever you set params.output_format: "json". With any other output_format it behaves as the table says.So for that capability the ref is a property of the request, not of the capability. Its output carries file_id, gs_url, outputRef, size_bytes and mime, plus an inline text field only when the extraction is small enough to inline — the JSON itself is behind the ref, not in output.

output.url is not a public URL

A media step and a file-producing capability both publish output.url, and it is a relative path of the form /v1/files/<fileId>/content. Pass it to a capability’s image_url and the capability refuses it, because it is not a URL.
  • Downstream, inside the workflow: use ${steps.<id>.outputRef}.
  • Outside, to fetch the bytes: prefix it with https://api.infery.ai and send your API key, or use the file_id with the Files API.

Spellings that look right and are not

That last one is why a foreach cannot drive video.extract_frame’s time_sec from ${item}.