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 @Format interpolation)
  • \} — 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 },
  ],
}

Last updated on March 19, 2026