Step onComplete — Expression Context
Every step in a blueprint can define an onComplete block that runs when the step finishes. Inside it, validate and updates[].transform are JSONata expressions. This page is the reference for what those expressions can see — the variable context available at that moment.
onComplete:
validate: 'docOutputs.offer_letter.candidate.accepted = true'
updates:
- key: employee
transform: |
$merge([
input.employee,
{
"status": "offer_accepted",
"acceptedSalary": docOutputs.offer_letter.candidate.acceptedSalary,
"acceptedAt": $now()
}
])
Fires On
- Participant steps — after the signing action completes
- Group steps — after the group's completion rule is satisfied (all / any / quorum)
Available in the Expression Context
The expression's root context is an object with three keys: input, docOutputs, childOutputs. Plus the JSONata built-ins and any custom functions referenced by the blueprint.
input.<entityKey>— every blueprint input, at its current value (which may already reflect prioronCompleteupdates earlier in the workflow).docOutputs.<documentKey>.<roleKey>— the payload produced by a template'sroles[].output.transformfor the role(s) this participant filled. Only populated for participant steps, and only for roles whose template declared anoutput. See Template Role Outputs.childOutputs.<childStepKey>.<entityKey>— only present when the current step is a parent group with children. Each child ran in a sandboxed copy of the entity context;childOutputsexposes each child's final entity snapshots so the parent can merge. See Handling Parallel Data for the pattern.#x3C;alias>()— custom functions referenced by the blueprint, invokable by alias.- JSONata built-ins —
$now(),$exists(),$count(), etc. See JSONata Expressions.
Because docOutputs is shaped by each template's roles[].output.transform, the onComplete expression is insulated from raw field layout — templates decide what structured payload to emit, and blueprints consume that payload.
Validate Expression
Runs first. If the expression evaluates to a falsy value, the step is marked as failed and no updates are applied.
Common patterns:
validate: '$exists(docOutputs.offer_letter.employee)' # Role output was produced (i.e. the role was signed)
validate: 'docOutputs.offer_letter.employee.accepted = true' # Signer accepted
validate: 'input.deal.value > 0' # Precondition on entity data
Update Transforms
Each update targets a blueprint inputKey. The transform's return value replaces the entity's data wholesale — whatever object you return becomes the new entity state. There is no automatic merge. If you want to preserve existing fields, you must fold them in yourself using $merge:
updates:
- key: employee
transform: |
$merge([
input.employee,
{
"acceptedAt": $now(),
"status": "offer_accepted",
"signedOfferId": docOutputs.offer_letter.employee.offerId
}
])
Without the $merge, the employee entity would be reduced to just those three fields — everything else (name, email, etc.) would be dropped.
If you're deriving a new value from the previous one, access it via input.<entityKey>:
updates:
- key: employee
transform: |
$merge([
input.employee,
{ "totalSigned": input.employee.totalSigned + 1 }
])
Multiple entities in one step
A single step's updates can target several entities. Each entry is independent — and each one replaces its entity wholesale, so each transform that wants to preserve existing fields needs its own $merge:
onComplete:
updates:
- key: employee
transform: '$merge([input.employee, { "status": "offer_accepted" }])'
- key: company
transform: '$merge([input.company, { "lastHireDate": $now() }])'
Group-level updates
Groups can carry onComplete too — it runs after the group's completion rule is satisfied. This is where you reconcile data from parallel branches by lifting fields out of each child's sandbox (childOutputs) onto the base entity:
- key: compliance_phase
type: group
execution: parallel
children:
- key: nda_step
...
- key: ip_agreement_step
...
onComplete:
updates:
- key: employee
transform: |
$merge([
input.employee,
{
"ndaSigned": childOutputs.nda_step.employee.ndaSigned,
"ipAgreementSigned": childOutputs.ip_agreement_step.employee.ipAgreementSigned,
"complianceCompletedAt": $now()
}
])
See Handling Parallel Data for the full sandbox-and-merge pattern, including conflict resolution and partial-completion handling.
Ordering Semantics
- Within a step,
updatesapply in array order. Each transform sees the entity state produced by the previous update. - Across parallel children writing the same entity, contention is real — prefer reconciling at the parent group's
onCompleterather than letting each child write the same field. See Handling Parallel Data for the sandbox-and-merge pattern. validateruns first, against entity state as it was when the step started. It does not see whatupdateswould produce.
Related
- The Workflow Engine (Steps) — The step model overview
- Handling Parallel Data — Reconciling entity state across parallel branches
- Template Role Outputs — What goes into
docOutputs - JSONata Expressions — Language reference
