Flow Language & JSON Path
Flow steps can dynamically reference, transform, and expand data from previous steps or from the user’s input. This is powered by a mini-language that combines JSON Path queries with special operators. You can use these anywhere a Flow step accepts parameters or in an ObjectTransformer’s result field.
Overview
Each step’s parameters can contain embedded strings like "$.input.description" (plain JSON Path) or advanced operators:
@Map(path, indexName)— loop over an array, producing transformed items@Index(indexName)— get the loop index from a@Map@Extend(path)— merge properties from a source object into the current object@Format(template, ...params)— build strings by interpolating values@Replace(target, search, replacement)— replace occurrences in a string
Flow Language processes these recursively before each step runs — letting you feed outputs from prior steps into new ones.
Currently, you can use Flow Language on step parameters and ObjectTransformer
result fields. We’re considering enabling it for the entire Flow
configuration — let us know if you’d like that!
Basic JSON Path
Referencing data
"$.input.someField"— references the top-level Flow input."$.results.previousStepId.someValue"— references a previous step’s results."$.store.someKey"— references a value from the Flow’s store."$$"— references the current array item inside a@Map(or the entire source if not in a map).
Any field that starts with $ is treated as a JSON Path expression and resolved at runtime.
{
someParameter: '$.input.userName',
}
// If the user typed "Alice", resolves to:
{
someParameter: 'Alice',
}
You can reference nested elements and array indices:
{
nested: '$.input.user.profile.name',
indexed: '$.input.users[5].profile.name',
}
Escaping
If you need literal strings that would otherwise be interpreted as operators or JSON Path, use backslash escaping:
\@— literal@(prevents operator parsing)\{— literal{(prevents@Formatinterpolation)\}— literal}\\— literal backslash
For example, if you want a string that starts with @ but isn’t an operator:
{
email: '\\@user.com',
template: 'Price is \\{not interpolated\\}',
}
Operators
@Format
Constructs a string by interpolating values into {} placeholders.
@Format(template, param1, param2, ...)
Example
Given the input:
{
pet: {
color: 'yellow',
age: 5,
},
}
And the schema:
{
description: "@Format('The cat is {} and she\\'s {} years old.', $.pet.color, $.pet.age)",
}
Result:
{
description: "The cat is yellow and she's 5 years old.",
}
{}marks where interpolation happens.- It can take as many parameters as you want.
- If a target isn’t a string, it’ll be stringified.
- The template itself can be a JSON Path:
@Format($.template, $.param1, $.param2).
@Replace
Returns the target string with every occurrence of search replaced by replacement.
@Replace(target, search, replacement)
Example
// Input:
{ user: { fullName: 'John Doe' } }
// Schema:
{ newName: "@Replace($.user.fullName, 'Doe', 'Smith')" }
// Result:
{ newName: 'John Smith' }
Inside a @Map loop
// Input:
{ words: ['foo_bar', 'baz_bar'] }
// Schema:
{ cleaned: ['@Map($.words)', { v: "@Replace($$, '_bar', '')" }] }
// Result:
{ cleaned: [{ v: 'foo' }, { v: 'baz' }] }
@Map
Iterates over a list, applying a sub-schema for each element.
@Map(path, optionalIndexName)
Basic example
Given the input:
{
recipes: [
{ name: 'Sushi rolls', timeToPrepare: 90 },
{ name: 'Kare bowl', timeToPrepare: 60 },
],
}
And the schema:
{
parsedList: [
'@Map($.recipes, idx)',
{
originalObject: '$$',
preparationTime: '$$.timeToPrepare',
index: '@Index(idx)',
},
],
}
Result:
{
parsedList: [
{
originalObject: { name: 'Sushi rolls', timeToPrepare: 90 },
preparationTime: 90,
index: 0,
},
{
originalObject: { name: 'Kare bowl', timeToPrepare: 60 },
preparationTime: 60,
index: 1,
},
],
}
$$is the current item from the array.@Index(idx)returns the current loop index.
Nested maps
You can nest as many Map operators as you need. Each retains its own index counter.
Given the input:
{
arrays: [{ prompts: ['a', 'b'] }, { prompts: ['c', 'd'] }],
}
And the schema:
['@Map($.arrays)', ['@Map($$.prompts)', { prompt: '$$' }]]
Result:
[{ prompt: 'a' }, { prompt: 'b' }, { prompt: 'c' }, { prompt: 'd' }]
Nested maps keeping array structure
Given the input:
{
arrays: [
[1, 2],
[3, 4],
],
}
And the schema:
{
flatArray: [
'@Map($.arrays, outerIndex)',
[
[
'@Map($$, innerIndex)',
{
value: '$$',
indexes: {
outer: '@Index(outerIndex)',
inner: '@Index(innerIndex)',
},
},
],
],
],
}
Result:
{
flatArray: [
[
{ value: 1, indexes: { outer: '0', inner: '0' } },
{ value: 2, indexes: { outer: '0', inner: '1' } },
],
[
{ value: 3, indexes: { outer: '1', inner: '0' } },
{ value: 4, indexes: { outer: '1', inner: '1' } },
],
],
}
Targeting other arrays with the current index
The index counter can be used to access elements in other arrays.
Given the input:
{
a: [1, 2, 3],
b: [{ v: 'aValue' }, { v: 'bValue' }, { v: 'cValue' }, { v: 'dValue' }],
}
And the schema:
{
mapped: [
'@Map($.a, index)',
{
value: '$.b[@Index(index)].v',
},
],
}
Result:
{
mapped: [{ value: 'aValue' }, { value: 'bValue' }, { value: 'cValue' }],
}
Note how there are only 3 items — the map iterates over a which has 3 elements. If a had 4 items but b only had 3, the fourth would resolve to null.
@Extend
Merges properties from a source object into the current object.
@Extend(path)
Basic example
Given the input:
{ user: { name: 'Alice', age: 30 } }
And the schema:
{
'@Extend': '$.user',
role: 'admin',
}
Result:
{ name: 'Alice', age: 30, role: 'admin' }
Inside a @Map
Say you want to iterate over a list and add a new field to each object:
Given the input:
{
users: [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 33 },
],
}
And the schema:
{
users: [
'@Map($.users, idx)',
{
'@Extend': '$$',
role: 'admin',
order: '@Index(idx)',
},
],
}
Result:
{
users: [
{ name: 'Alice', age: 30, role: 'admin', order: 0 },
{ name: 'Bob', age: 33, role: 'admin', order: 1 },
],
}