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:

  1. Evaluates iterator against the merged entity context (every blueprint input is a top-level key — $.team is the team entity).
  2. Coerces the result to an array — null becomes [], a single value becomes [value], an array passes through.
  3. 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 (iterator returns [] or null): 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.