${…} token in a step’s fields. It is replaced with a value just
before the step runs.
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.
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 letsforeach.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 aparallelfan-out — which publishes an object keyed by branch — has no projection form. - One
*per path. A second one yieldsundefinedrather than guessing a level. nullandundefinedholes are dropped, not kept — so a loop running underfailure_mode: "continue"projects only the iterations that succeeded.- A trailing
.joinsegment joins the projection into one block of text separated by blank lines:${steps.loop.output.*.text.join}. Off a projection,joinis 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,nullor the empty string — and nothing else.0andfalseare 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 onlyA–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 asoutputRef:
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.aiand send your API key, or use thefile_idwith 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}.