> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ingestly.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Expressions and filters

> Reference upstream values, transform them with filters, and use built-in helpers in your pipeline.

Ingestly templates pull data from earlier nodes into the configuration of later ones. Wherever you see a field that accepts `{{ ... }}` (callback URL, HTTP body, email subject, transform output, validation rule), you can reference an upstream value, index into arrays, transform it with filters, or insert a built-in helper.

## Basic syntax

A template expression has three parts: a **path**, optional **filters**, and the surrounding `{{` `}}` markers.

```
{{ path | filter | filter }}
```

The path locates the value. Filters transform it left to right. The result replaces the whole `{{ ... }}` token.

## Path

A path is a dotted identifier, optionally with array indices.

| Form                                | What it resolves to                                               |
| ----------------------------------- | ----------------------------------------------------------------- |
| `{{nodeName}}`                      | The full output of the named upstream node, as JSON               |
| `{{nodeName.payload.field}}`        | A field inside that node's output                                 |
| `{{nodeName.payload.items[0].sku}}` | The first element of an array, then a field inside it             |
| `{{nodeName.payload.items[-1]}}`    | The last element of an array (negative index counts from the end) |

The first segment of every path must be the **name of an upstream node** (or a built-in placeholder or helper, see below). Subsequent segments index into that node's output, which has the shape `{ success, payload, metadata, error }`. Paths are case sensitive and may use letters, digits, and underscores.

<Note>
  Each node in the editor has a **Name** field. Default names are auto-assigned (`extract`, `extract2`, `vars`, etc.) but you can rename a node to anything that's unique within the pipeline. Whatever name you pick is the identifier used at the head of every template expression.
</Note>

## Built-in placeholders

These resolve from the run context:

| Placeholder          | Value                        |
| -------------------- | ---------------------------- |
| `{{documentId}}`     | The current document's ID    |
| `{{organizationId}}` | The owning organization's ID |
| `{{pipelineId}}`     | The current pipeline's ID    |
| `{{nodeId}}`         | The current node's ID        |

## Source document file

The `$document` head resolves from the run's **source document**: the original file that triggered the run, whether it was uploaded or emailed. It is a global head, valid in any template field, because a pipeline can have several triggers but only one fires per run.

| Path                             | Value                                                                |
| -------------------------------- | -------------------------------------------------------------------- |
| `{{$document.file.name}}`        | The original file name (falls back to `document` plus the extension) |
| `{{$document.file.contentType}}` | The MIME content type, for example `application/pdf`                 |
| `{{$document.file.size}}`        | The file size in bytes                                               |
| `{{$document.file.extension}}`   | The file extension, including the leading dot, for example `.pdf`    |
| `{{$document.file.base64}}`      | The raw file bytes, Base64-encoded                                   |

The metadata paths (`name`, `contentType`, `size`, `extension`) are always cheap to resolve. `base64` is different: the file bytes are fetched and encoded **lazily**, only when a template actually references `{{$document.file.base64}}`, and they are never persisted. The encoded value never enters run output, step history, or the live preview. Anywhere it would otherwise appear, Ingestly shows a `[binary N bytes, type]` placeholder instead.

Use `{{$document.file.base64}}` to send the source file as a [binary body](/nodes/callback#binary-body) on a callback or HTTP node, or to [attach it to a record](/guides/business-central#5-attach-the-source-document-to-a-record).

<Note>
  A node name can never start with `$`, so the `$document` head never collides with a node you have named `document`.
</Note>

## Built-in helpers

These produce a value at the time the template is evaluated.

| Helper        | Returns                                                   |
| ------------- | --------------------------------------------------------- |
| `{{now}}`     | Current UTC datetime as ISO 8601 (`yyyy-MM-ddTHH:mm:ssZ`) |
| `{{today}}`   | Current UTC date (`yyyy-MM-dd`)                           |
| `{{nowUnix}}` | Current Unix epoch seconds                                |
| `{{uuid}}`    | A new UUID v7 (time-ordered)                              |

You can chain filters onto helpers: `{{now | formatDate: 'yyyy-MM-dd'}}`.

## Filters

Add a filter with the pipe character: `{{ value | filter }}`. Filters with arguments use a colon: `{{ value | filter: 'arg1', 'arg2' }}`. You can chain as many filters as you need, applied left to right.

### Filter arguments

Each argument is one of:

| Form                    | Example                                     | Notes                                                             |
| ----------------------- | ------------------------------------------- | ----------------------------------------------------------------- |
| Number literal          | `{{count \| add: 5}}`                       | `-3.14`, `1e6`, etc.                                              |
| Quoted string           | `{{name \| concat: '!'}}`                   | Single or double quotes                                           |
| `true`, `false`, `null` | `{{value \| default: null}}`                | Bare keywords                                                     |
| Path reference          | `{{payload.terms \| div: payload.divisor}}` | Resolved against the same upstream context as the head expression |

Path references must satisfy the same shape as the head path (`identifier(.identifier|[N])*`). The runtime resolves them at filter-application time, so they can reference upstream node fields (`extract.payload.divisor`), built-in helpers (`{{end | dateDiff: 'days', start}}`), or array elements (`{{items[0] | concat: items[1]}}`). If the path resolves to a type that doesn't match what the filter expects (e.g., `div` argument resolves to a string), the editor flags it before the run starts.

### String filters

| Filter                          | Example                           | Result          |
| ------------------------------- | --------------------------------- | --------------- |
| `upper`                         | `{{name \| upper}}`               | `ALICE`         |
| `lower`                         | `{{name \| lower}}`               | `alice`         |
| `trim`                          | `{{ ' alice ' \| trim}}`          | `alice`         |
| `replace: 'from', 'to'`         | `{{name \| replace: 'a', '@'}}`   | `@lice`         |
| `substring: start, length?`     | `{{name \| substring: 0, 3}}`     | `Ali`           |
| `truncate: maxLength, postfix?` | `{{title \| truncate: 8, '...'}}` | `Invoi...`      |
| `split: separator`              | `{{tags \| split: ','}}`          | `["a","b","c"]` |
| `concat: 'suffix'`              | `{{name \| concat: '!'}}`         | `Alice!`        |
| `contains: 'needle'`            | `{{name \| contains: 'lic'}}`     | `true`          |
| `startsWith: 'prefix'`          | `{{name \| startsWith: 'Al'}}`    | `true`          |
| `endsWith: 'suffix'`            | `{{name \| endsWith: 'ce'}}`      | `true`          |
| `padStart: length, pad?`        | `{{code \| padStart: 5, '0'}}`    | `00042`         |
| `padEnd: length, pad?`          | `{{code \| padEnd: 5, '0'}}`      | `42000`         |
| `capitalize`                    | `{{name \| capitalize}}`          | `Alice`         |
| `camelCase`                     | `{{'order id' \| camelCase}}`     | `orderId`       |
| `kebabCase`                     | `{{'Order Id' \| kebabCase}}`     | `order-id`      |
| `snakeCase`                     | `{{'Order Id' \| snakeCase}}`     | `order_id`      |

`truncate` counts the postfix toward the limit; `padStart` and `padEnd` pad with a space when no pad string is given.

### Number filters

| Filter           | Example                   | Result |
| ---------------- | ------------------------- | ------ |
| `round: digits?` | `{{3.14159 \| round: 2}}` | `3.14` |
| `ceil`           | `{{3.2 \| ceil}}`         | `4`    |
| `floor`          | `{{3.8 \| floor}}`        | `3`    |
| `abs`            | `{{-5 \| abs}}`           | `5`    |
| `add: n`         | `{{5 \| add: 3}}`         | `8`    |
| `sub: n`         | `{{5 \| sub: 3}}`         | `2`    |
| `mul: n`         | `{{5 \| mul: 3}}`         | `15`   |
| `div: n`         | `{{6 \| div: 3}}`         | `2`    |

### Format filters

Turn a raw number into a display string.

| Filter                                        | Example                               | Result      |
| --------------------------------------------- | ------------------------------------- | ----------- |
| `formatNumber: decimals, culture?`            | `{{amount \| formatNumber: 2}}`       | `1,234.50`  |
| `formatCurrency: 'code', decimals?, culture?` | `{{amount \| formatCurrency: 'USD'}}` | `$1,234.50` |

`formatCurrency` takes an ISO 4217 currency code (for example `USD`, `EUR`, `GBP`) and pins the currency symbol from that code.

### Array filters

| Filter               | Example                                |
| -------------------- | -------------------------------------- |
| `length`             | `{{items \| length}}`                  |
| `first`              | `{{items \| first}}`                   |
| `last`               | `{{items \| last}}`                    |
| `join: separator`    | `{{tags \| join: ', '}}`               |
| `sum`                | `{{prices \| sum}}`                    |
| `avg`                | `{{prices \| avg}}`                    |
| `min`                | `{{prices \| min}}`                    |
| `max`                | `{{prices \| max}}`                    |
| `unique`             | `{{tags \| unique}}`                   |
| `sort`               | `{{prices \| sort}}`                   |
| `reverse`            | `{{items \| reverse}}`                 |
| `slice: start, end?` | `{{items \| slice: 0, 3}}`             |
| `groupBy: 'key'`     | `{{lineItems \| groupBy: 'category'}}` |

`slice` uses Python-style bounds (negative indices count from the end). `groupBy` groups an array of objects by the string value at `key`, returning an array of `{ key, items }` groups. `reverse` also reverses a string's characters.

### Date filters

Operate on ISO 8601 strings (or anything that can be parsed as one). The add/subtract and start/end filters return an ISO 8601 UTC string.

| Filter                      | Example                                    | Result                 |
| --------------------------- | ------------------------------------------ | ---------------------- |
| `formatDate: '...'`         | `{{now \| formatDate: 'yyyy-MM-dd'}}`      | `2026-04-28`           |
| `addDays: n`                | `{{today \| addDays: 7}}`                  | `2026-05-05T00:00:00Z` |
| `subDays: n`                | `{{today \| subDays: 1}}`                  | `2026-04-27T00:00:00Z` |
| `addHours: n`               | `{{now \| addHours: 6}}`                   | shifted ISO datetime   |
| `addMinutes: n`             | `{{now \| addMinutes: 90}}`                | shifted ISO datetime   |
| `addMonths: n`              | `{{today \| addMonths: 1}}`                | shifted ISO datetime   |
| `addYears: n`               | `{{today \| addYears: 1}}`                 | shifted ISO datetime   |
| `startOfDay`                | `{{now \| startOfDay}}`                    | `2026-04-28T00:00:00Z` |
| `endOfDay`                  | `{{now \| endOfDay}}`                      | `2026-04-28T23:59:59Z` |
| `startOfMonth`              | `{{now \| startOfMonth}}`                  | first day of the month |
| `endOfMonth`                | `{{now \| endOfMonth}}`                    | last day of the month  |
| `dayOfWeek`                 | `{{now \| dayOfWeek}}`                     | `Tuesday`              |
| `toUnix`                    | `{{now \| toUnix}}`                        | epoch seconds          |
| `fromUnix`                  | `{{ts \| fromUnix}}`                       | ISO datetime           |
| `toIso`                     | `{{rawDate \| toIso}}`                     | canonical ISO 8601 UTC |
| `dateDiff: unit, otherDate` | `{{end \| dateDiff: 'days', '{{start}}'}}` | `5`                    |

`formatDate` uses .NET-style format strings. Common patterns: `yyyy-MM-dd`, `yyyy-MM-ddTHH:mm:ssZ`, `MM/dd/yyyy`, `HH:mm`. Negative arguments to the `add*` filters subtract.

### Encoding filters

| Filter         | Example                     | Notes                           |
| -------------- | --------------------------- | ------------------------------- |
| `base64Encode` | `{{value \| base64Encode}}` | Base64-encode UTF-8 text        |
| `base64Decode` | `{{value \| base64Decode}}` | Decode Base64 back to text      |
| `urlEncode`    | `{{value \| urlEncode}}`    | Percent-encode for use in a URL |
| `urlDecode`    | `{{value \| urlDecode}}`    | Reverse percent-encoding        |

### General filters

| Filter                    | Example                                     | Notes                                                                                                         |
| ------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `default: 'fallback'`     | `{{maybeMissing \| default: 'n/a'}}`        | Substitutes when the value is null or empty                                                                   |
| `coalesce: fallback, ...` | `{{primary \| coalesce: secondary, 'n/a'}}` | Returns the first argument (starting with the input) that is not null or empty. Takes any number of fallbacks |
| `json`                    | `{{payload \| json}}`                       | Stringify the value as JSON                                                                                   |
| `escape`                  | `{{name \| escape}}`                        | HTML-escape a string                                                                                          |

## Array constructs

Beyond the filters above, four constructs transform arrays and pick branches. They use the same `|` syntax but are written by name.

| Construct | Syntax                                      | What it does                                                                                                                |
| --------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `map`     | `{{items \| map: field=value, ...}}`        | Project each array element into a new object. Use `$` for the current element and `$.field` for a field of it               |
| `filter`  | `{{items \| filter: $.qty > 0}}`            | Keep only the elements matching a condition. `$` is the current element                                                     |
| `select`  | `{{items \| select: $.sku}}`                | Pull one field from every element into a flat array                                                                         |
| `if`      | `{{value \| if: "$ > 100", 'high', 'low'}}` | Inline conditional. The condition is a quoted string; the else branch is optional and passes the input through when omitted |

```
{{extract.payload.lines | map: sku=$.sku, qty=$.quantity}}
{{extract.payload.lines | filter: $.amount > 0 | select: $.sku}}
```

## Live preview

When you edit a template field in a node editor, Ingestly shows a **live preview** of the resolved value. Select a document from the run toolbar so the preview evaluates against that document's most recent run data. This lets you confirm a path and its filter chain produce the value you expect before you run the pipeline. Without a selected document, the editor still validates the path and filter names but cannot show a resolved value.

## Type-aware autocomplete

When you type inside a `{{ ... }}` field in a node editor, Ingestly suggests:

* **Upstream node fields** at the start of the expression (after `{{`)
* **Built-in helpers** (`now`, `today`, `uuid`, `nowUnix`) alongside fields
* **Filters** after a `|`, scoped to the type of the preceding value (an array path shows `length`, `first`, `join`; a string path shows `upper`, `lower`, `trim`)
* **Filter arguments** as snippet placeholders you can tab through

Unknown filter names render with a red squiggle and surface as a validation error on the node.

## Inside JSON bodies

When you write a template inside a JSON value position, like `"qty": "{{payload.qty}}"`, Ingestly emits the resolved value as raw JSON so types are preserved. A number stays unquoted, an object stays structured, an array stays an array.

```json theme={null}
{
  "amount": "{{payload.amount | round: 2}}",
  "lines": "{{payload.lineItems}}"
}
```

After substitution the request body becomes:

```json theme={null}
{
  "amount": 3.14,
  "lines": [{ "sku": "A1", "qty": 2 }]
}
```

This applies to the **HTTP action** and **callback output** body fields when they parse as JSON. Outside JSON value positions (URLs, email subjects, plain-text fields), templates substitute as plain strings.

<Note>
  Filter arguments inside a JSON-position template should use single quotes (`'a'`) rather than double quotes, to avoid clashing with the surrounding JSON string.
</Note>

## Row expansion in JSON bodies ($each and $when)

A JSON body can fan out over a runtime array. Inside any JSON array, an object that carries a `$each` key is a row template: it is repeated once per element of the referenced array, and the `$each` key itself is removed from the output.

```json theme={null}
{
  "orderNumber": "{{transform1.payload.po_number}}",
  "salesOrderLines": [
    {
      "$each": "{{payload.lines}}",
      "lineObjectNumber": "{{item.mark}}",
      "quantity": "{{item.quantity}}"
    }
  ]
}
```

Inside a row template:

* `{{item}}` is the current array element, and `{{item.field}}` reads a field from it
* `{{index}}` is the zero-based position
* Values that are exactly one `{{item...}}` or `{{index}}` token keep their type (a number stays a number, an object stays structured)
* Tokens that mix text and bindings, like `"row {{index}}: {{item.mark}}"`, substitute as text
* Other `{{node.path}}` tokens are left for normal substitution

Add a `$when` key to filter rows. The condition supports `==`, `!=`, a bare truthiness test, and `!` for negation:

```json theme={null}
"salesOrderLines": [
  {
    "$each": "{{payload.lines}}",
    "$when": "!{{item.__label}}",
    "lineObjectNumber": "{{item.mark}}",
    "quantity": "{{item.quantity}}"
  },
  {
    "$each": "{{payload.lines}}",
    "$when": "{{item.__label}} == 'surcharge'",
    "description": "{{item.description}}",
    "unitPrice": "{{item.total}}"
  }
]
```

Several row templates in one array concatenate in order, which routes rows to different shapes: here regular lines (no `__label`) become item lines and reconcile-classified surcharge rows become a different line shape. A reconcile node stamps classified extra rows with `__label`, so this pairs naturally with [Expected extras](/nodes/reconcile).

An object with only `$when` (no `$each`) is included or dropped as a whole. Row templates nest: a field inside a row can hold another array with its own `$each` over `{{item.subArray}}`, and the inner template's `item` refers to the inner element.

Row expansion applies to the **HTTP action** and **callback** body fields when they parse as JSON. A `$each` reference that does not resolve to an array contributes no rows.

## Common recipes

**Last item from an extracted line items array, with a fallback:**

```
{{extract.payload.lineItems[-1].sku | default: 'no-sku'}}
```

**Total of a numeric array, rounded:**

```
{{extract.payload.amounts | sum | round: 2}}
```

**Today's date plus 30 days as a `yyyy-MM-dd` string:**

```
{{today | addDays: 30 | formatDate: 'yyyy-MM-dd'}}
```

**Stringified JSON of a structured value, for a plain-text field:**

```
{{extract.metadata | json}}
```

**Comma-joined tags, uppercased:**

```
{{extract.payload.tags | join: ', ' | upper}}
```

## When a template doesn't resolve

If the path can't be resolved (the field is missing, the upstream node didn't run, the index is out of bounds), Ingestly substitutes an empty string. Use `default: '...'` to provide a fallback.

If the **filter name is unknown** or a **filter call is malformed** (wrong arg count, bad argument type), the substitution yields an empty string and the node surfaces a validation error.
