loop
Iterate over an array field on each record and run a nested filter chain per item.
The loop filter walks an array field on each record and runs a nested
filter chain for every item. The current item is exposed under the
configured itemName alias; the root record stays available as
record. Use it to flatten records that carry an array of "cells",
"rows", or "lines" into top-level fields without dropping to a script
filter.
Minimal example
Extract a displayValue keyed by columnId into a flat
record.eventId:
filters:
- type: loop
field: cells
itemName: cell
filters:
- type: condition
expression: "cell.columnId == 8150579298996100"
then:
- type: mapping
mappings:
- source: cell.displayValue
target: record.eventIdOptions
| property | type | default | description |
|---|---|---|---|
typerequired | "loop" | — | Module type discriminator. Must be `loop` for this module. |
id | string | — | Unique identifier within the pipeline. |
name | string | — | Human-readable name. |
description | string | — | — |
enabled | boolean | true | Whether module is active. |
tags | array<string> | — | — |
onError | string | "fail" | Default error action. Case-insensitive; normalized to lowercase by the runtime. |
fieldrequired | string | — | Path to the array field, relative to the input record (root record for the outermost loop, parent scope for nested loops). Supports dot notation. |
itemNamerequired | string | — | Alias exposed to nested filters for the current item. Cannot be 'record', '_metadata', or 'loop', and must not duplicate an active parent loop alias. |
filtersrequired | array<object> | — | Nested filters executed for every item. |
Scope inside the loop
Nested filters operate on a scope — a synthetic record built once per item:
| Key | What it is |
|---|---|
record | The root record. Writes to record.* mutate the original record. |
<itemName> | The current item. Writes to <itemName>.* are persisted back into the array at the same position. |
| Parent aliases | Every active parent loop alias when loops are nested. |
_metadata | The root metadata. Loop iteration state lives at _metadata.loop.<itemName>.index (read-only). |
The scope is built fresh per item, but record and _metadata are
shared by reference — mutations are visible to subsequent items and
filters after the loop.
Because the nested filters see the scope and not the record, a set or a
mapping target without the record. prefix writes into the scope and is
discarded when the item finishes. The pipeline succeeds and the field simply
never appears downstream, so this is easy to miss — always write to
record.<field> when the value must outlive the iteration.
When field is absent or not an array
| Case | Behaviour |
|---|---|
field missing from the record, or null | No-op: the loop runs no inner filter and the record passes through unchanged. |
field present but empty array | Same no-op, no inner filter runs. |
field present but not an array (string, number, object) | Error — loop field is not an array. Handle it with the module's onError if a mixed-shape source is expected. |
A missing field is deliberately tolerated so an optional array does not need a
guard condition in front of the loop.
Loop metadata
The current index is exposed at _metadata.loop.<itemName>.index:
- type: loop
field: rows
itemName: row
filters:
- type: mapping
mappings:
- source: _metadata.loop.row.index
target: row.position_metadata.loop is read-only for nested filters. Any attempt to
write under that path is rejected with loop nested filters wrote to _metadata.loop which is read-only, including replacement or deletion
of _metadata itself.
Nested loops
Loops can be nested. Each inner loop adds its alias to the scope and keeps every parent alias available:
- type: loop
field: cells
itemName: cell
filters:
- type: loop
field: cell.children
itemName: x
filters:
- type: condition
expression: >-
_metadata.loop.cell.index == 2 &&
_metadata.loop.x.index == 0
then:
- type: mapping
mappings:
- source: x.label
target: record.firstConstraints on itemName:
- Cannot be
record,_metadata, orloop(reserved scope keys). - Cannot duplicate an active parent loop alias.
Item removal and expansion
| Nested filters return … | Effect on the array |
|---|---|
| Zero records for an item | Item is removed from the array. |
| Exactly one record | Item is updated with the result. |
| More than one record | Pipeline fails — item expansion is out of scope in v1. |
Use a nested condition + drop to remove items conditionally:
- type: loop
field: items
itemName: item
filters:
- type: condition
expression: "item.keep == false"
then:
- type: dropNon-object items
Scalar / array / null items pass through unchanged when nested filters
don't touch the alias. Sub-path writes such as item.foo = ... on a
non-object item are rejected to prevent silent map auto-creation by
the path engine.
Examples
name: loop-cells-extraction
version: 1.0.0
description: Iterate over a cells[] array and extract values into flat record fields by columnId.
tags:
- loop
- smartsheet
input:
type: httpPolling
schedule: "*/15 * * * *"
endpoint: https://source.example.com/api/rows
dataField: rows
filters:
- type: loop
field: cells
itemName: cell
filters:
- type: condition
expression: cell.columnId == 1
then:
- type: mapping
mappings:
- source: cell.displayValue
target: record.eventId
- type: condition
expression: cell.columnId == 2
then:
- type: mapping
mappings:
- source: cell.displayValue
target: record.coordinates
- type: condition
expression: cell.columnId == 3
then:
- type: mapping
mappings:
- source: cell.displayValue
target: record.customerName
- type: remove
target:
- cells
output:
type: httpRequest
endpoint: https://destination.example.com/api/events
method: POST
requestMode: single