Generating Multiple Documents (Iterators)
Some agreements need N copies of the same document depending on data — one NDA per board member, one supplement per attached service line, one waiver per dependent. The iterator field on an envelope document tells the workflow engine to materialize the document once per element of an array, with each instance rendered against its own slice of data.
Declaring an Iterator
iterator lives on a document inside a blueprint's envelope. It's a JSONata expression evaluated against the entity context:
envelopes:
- key: onboarding
documents:
- key: nda
name: Mutual NDA
template: mutual_nda@1.0.0
iterator: '$.team.members'
When the workflow renders, the engine:
- Evaluates
iteratoragainst the merged entity context (every blueprint input is a top-level key —$.teamis theteamentity). - Coerces the result to an array —
nullbecomes[], a single value becomes[value], an array passes through. - Materializes one document instance per element.
Each instance is a real, signable document — participants sign each one separately.
What Each Iteration Sees
Inside the per-instance render, the engine injects a special __iterator__ variable into the JSONata context:
| Field | Type | Meaning |
|---|---|---|
__iterator__.current |
any | The array element for this iteration |
__iterator__.index |
number | Zero-based index |
__iterator__.total |
number | Total iterations |
So if you're iterating over $.team.members and a member has shape { name, email, role }, then __iterator__.current.role is the role of the current iteration's member.
documents:
- key: nda
template: mutual_nda@1.0.0
iterator: '$.team.members'
includeIf: '__iterator__.current.role = "executive"' # only execs sign
Document Key Uniqueness
When an iterator produces more than one instance, the engine appends the index to the document key and a (N) suffix to the name:
| Source declaration | Materialized instances |
|---|---|
key: nda, iterator: '$.team.members' (3 members) |
nda0, nda1, nda2 (named "Mutual NDA (1)", "(2)", "(3)") |
| Same, but only 1 member | nda (no suffix, single instance) |
Iterator returns [] |
No instances materialized |
Step assignments that reference the document by its declared key (assignments: [{ document: 'nda', role: 'signer' }]) match all materialized instances via prefix — you don't need to enumerate nda0 / nda1 / nda2 in your blueprint.
Per-Instance vs Shared Fields
Some fields on the iterated document are evaluated per-iteration; others are shared across all instances.
| Field | Per-instance? | Notes |
|---|---|---|
iterator |
once | Resolved once at envelope materialization |
includeIf |
per-instance | Evaluated with __iterator__ in scope; instance is dropped if it returns false |
fileId (expression form) |
per-instance | Reference __iterator__.current.<field> to point each instance at its own file |
templateKey / templateVersion |
shared | All instances render with the same template |
inputMapping |
shared | Same mapping applied to every instance |
A Real Example
From blueprint.example.yaml:
- key: supplement
name: Supplement Document
template: supplement_template@1.0.0
iterator: '$.position.supplements'
includeIf: '__iterator__.current.required'
fileId:
expr: '__iterator__.current.fileId'
If position.supplements is:
[
{ "required": true, "fileId": "file-a" },
{ "required": false, "fileId": "file-b" },
{ "required": true, "fileId": "file-c" }
]
…the workflow ends up with two supplement documents — supplement0 and supplement2, each loading its own file. The middle one is skipped because includeIf evaluated to false against __iterator__.current.required = false.
Edge Cases
- Empty array (
iteratorreturns[]ornull): no documents are materialized. The envelope simply has zero instances of this document — downstream steps that reference it by key will see nothing to sign. - Non-array return (a single object or scalar): coerced to a one-element array. Useful when the same expression sometimes returns one and sometimes many.
- Failing JSONata expression: throws and aborts the envelope render. Validate your iterator path against the entity schema during scenario testing.
- All instances filtered out by
includeIf: same outcome as an empty array — zero documents materialized.
Related
- Blueprints — Where envelope documents are declared
- JSONata Expressions — Expression language reference
- Step
onComplete— Expression Context — How signed iterated documents flow back into entity data viadocOutputs
