Handling Parallel Data ("Sandbox & Merge")
Entity context evolves as steps complete (see Workflow Engine > Evolving Data Across Steps). Sequential execution handles this naturally — each step sees the state left by the previous one. Parallel is where coordination matters. This page covers the pattern.
The Problem with Parallel Updates
Imagine a parallel group with two children both writing to the same entity:
- key: review_phase
type: group
execution: parallel
children:
- key: finance_review
type: participant
participant: finance_reviewer
action:
type: sign
envelope: review_envelope
assignments:
- document: deal_doc
role: finance_reviewer
onComplete:
updates:
- key: deal_info
transform: '{ "financeNotes": "Approved" }'
- key: legal_review
type: participant
participant: legal_reviewer
action:
type: sign
envelope: review_envelope
assignments:
- document: deal_doc
role: legal_reviewer
onComplete:
updates:
- key: deal_info
transform: '{ "legalNotes": "Minor revisions needed" }'
If both branches updated the central deal_info directly, you'd have a race condition — the last one to finish could stomp on the other's changes, and the outcome would depend on timing, not logic.
The Solution: Sandbox & Merge
SignStack uses a Sandbox & Merge pattern to remove the race. Think of it like Git branching and merging — each parallel step is a branch, and the parent group is the merge commit:
- Branching — When the parallel group starts, each child gets its own copy of the Entity Context.
- Isolated updates — Each child's
onCompleteupdates only its sandboxed copy. Changes can't collide because they don't share state. - Merge — After the group completes (based on its
completionrule), the parent group'sonCompletereconciles the results into the main entity context.
Writing the Merge
The parent group's onComplete has access to:
input.deal_info— the entity state before the parallel group started (the "base")childOutputs.<childKey>.<entityKey>— each child's final sandboxed entity snapshot
- key: review_phase
type: group
execution: parallel
onComplete:
updates:
- key: deal_info
transform: |
$merge([
input.deal_info,
{ "financeNotes": childOutputs.finance_review.deal_info.financeNotes },
{ "legalNotes": childOutputs.legal_review.deal_info.legalNotes },
{ "reviewCompletedAt": $now() }
])
children:
- key: finance_review
...
- key: legal_review
...
The parent explicitly lifts each field out of childOutputs and layers it onto the base. Fields the parent doesn't reference don't carry over — you're always deliberate about what crosses the merge boundary.
Handling Conflicts
If two children write the same field in their sandboxes — say both reviewers set approvalStatus — the parent has to decide what the final value is. Use a conditional in the merge to encode the business rule. Here, approvalStatus is approved only when both reviewers agreed; otherwise it falls back to needs_revision:
transform: |
$merge([
input.deal_info,
{ "financeNotes": childOutputs.finance_review.deal_info.financeNotes },
{ "legalNotes": childOutputs.legal_review.deal_info.legalNotes },
{
"approvalStatus":
childOutputs.finance_review.deal_info.approvalStatus = "approved"
and childOutputs.legal_review.deal_info.approvalStatus = "approved"
? "approved"
: "needs_revision"
}
])
Simpler alternative: last write wins. If you don't need custom reconciliation, overlay whole sandboxes in order — later entries override earlier ones on any shared field:
transform: |
$merge([
input.deal_info, # base
childOutputs.finance_review.deal_info, # finance's full sandbox
childOutputs.legal_review.deal_info # legal wins any overlap
])
Less code, but you trade the explicit control over what crosses the boundary — every field each child touched in its sandbox comes through.
Partial completion: When using completion: any or completion: quorum, not all children may complete. Use $exists() to check:
transform: |
$merge([
input.deal_info,
$exists(childOutputs.finance_review)
? { "financeNotes": childOutputs.finance_review.deal_info.financeNotes }
: {},
$exists(childOutputs.legal_review)
? { "legalNotes": childOutputs.legal_review.deal_info.legalNotes }
: {}
])
Related Concepts
- Workflow Engine — Step types, parallel execution, completion rules
- Step
onComplete— Expression Context — How updates and validates work inonComplete
