The Workflow Engine (Steps)

The Workflow Engine orchestrates signing workflows through a recursive Step tree. If a Blueprint is the plan, the orchestration steps are the detailed instructions within that plan.

The Step Model

A workflow is a tree of Steps. A step is either a grouping of child steps (e.g. "Compliance Phase") or a single action like a participant signing a document. Group steps can nest to any depth.

Two primitives — groups and participants — combined with sequential/parallel execution, completion rules, and data-driven includeIf conditions, can express nearly any signing workflow you'd want to model: single-signer NDAs, multi-party contracts, approval chains with conditional routing, quorum-based approvals, phases that merge after parallel work, workflows that update data mid-flight.

Two Step Types

Group — contains child steps and controls their execution:

- key: compliance_phase
  type: group
  name: Compliance Phase
  execution: parallel         # Children execute concurrently
  completion: all             # Wait for all children
  children:
    - ...child steps...

Participant — a single signing task performed by a person:

- key: new_hire_signs_nda
  type: participant
  participant: new_hire
  action:
    type: sign
    envelope: compliance_envelope
    assignments:
      - document: nda
        role: signer

Execution Modes

The execution property on a group controls when children start:

Mode Behavior
sequential Children run one at a time, in order. Like a checklist.
parallel All children start at once. Like a team assignment.

Completion Rules

The completion property on a group controls when the group is done:

Rule Behavior
all Wait for every child (default)
any Complete as soon as any one child finishes
quorum Complete when N children finish (set quorum: N)

Common Patterns

Sequential Signing (Checklist)

HR signs first, then the new hire:

orchestration:
  key: root
  type: group
  execution: sequential
  children:
    - key: hr_sends_offer
      type: participant
      participant: hr_manager
      action:
        type: sign
        envelope: offer_envelope
        assignments:
          - document: offer_letter
            role: hr_representative

    - key: new_hire_accepts
      type: participant
      participant: new_hire
      action:
        type: sign
        envelope: offer_envelope
        assignments:
          - document: offer_letter
            role: employee

Parallel Signing (Everyone at Once)

Both parties sign concurrently, then the witness:

orchestration:
  key: root
  type: group
  execution: sequential
  children:
    - key: signing_phase
      type: group
      execution: parallel
      children:
        - key: party_a_signs
          type: participant
          participant: party_a_signer
          action:
            type: sign
            envelope: contract_envelope
            assignments:
              - document: contract
                role: party_a
        - key: party_b_signs
          type: participant
          participant: party_b_signer
          action:
            type: sign
            envelope: contract_envelope
            assignments:
              - document: contract
                role: party_b

    - key: witness_signs
      type: participant
      participant: witness
      action:
        type: sign
        envelope: contract_envelope
        assignments:
          - document: contract
            role: witness

Quorum (2 of 3 Board Members)

- key: board_approval
  type: group
  execution: parallel
  completion: quorum
  quorum: 2
  children:
    - key: member_1_signs
      type: participant
      participant: board_member_1
      action:
        type: sign
        envelope: resolution_envelope
        assignments:
          - document: resolution_doc
            role: board_member_1       # Each member needs a distinct role so
    - key: member_2_signs              # their signatures land on distinct fields
      type: participant
      participant: board_member_2
      action:
        type: sign
        envelope: resolution_envelope
        assignments:
          - document: resolution_doc
            role: board_member_2
    - key: member_3_signs
      type: participant
      participant: board_member_3
      action:
        type: sign
        envelope: resolution_envelope
        assignments:
          - document: resolution_doc
            role: board_member_3

Conditional Steps

Use includeIf to skip steps based on data:

- key: manager_approves
  type: participant
  participant: hiring_manager
  includeIf: '$.position.level > 3'
  action:
    type: sign
    envelope: offer_envelope
    assignments:
      - document: offer_letter
        role: manager

Evolving Data Across Steps

Often you'll want data captured or decided in one step to drive a later step — a reviewer picks a discount tier that the next document reflects; an approver's notes carry into the final contract; a status set in step 3 gates whether step 4 runs.

The workflow's entity context — the running workflow's entity data — is seeded at creation from the data you pass in (see Workflows > Creating a Workflow). Every step's expressions read from that context. The mechanism for evolving it is the onComplete block: each step can update the context when it finishes, and every downstream step sees the new state.

Sequential execution handles this naturally. Each step sees the state left by the step before it; tree order is update order. You don't think about it — the engine just carries changes forward.

Parallel execution is where things get interesting: two children can't both write to the same entity without colliding. SignStack uses a sandbox-and-merge pattern — each branch works in isolation, then the parent group's onComplete reconciles. See Handling Parallel Data for the full pattern.

The onComplete Block

Works on both group and participant steps — on a group, it runs after all required children complete; on a participant step, after the signing action:

- key: new_hire_accepts
  type: participant
  participant: new_hire
  action:
    type: sign
    envelope: offer_envelope
    assignments:
      - document: offer_letter
        role: employee
  onComplete:
    validate: '$exists($envelope.signedAt)'          # Step fails if expression is falsy
    updates:
      - key: employee                                # Blueprint input to update
        transform: |                                 # Output must conform to the employee schema
          {
            "acceptedAt": $now(),
            "status": "offer_accepted"
          }
      - key: position                                # Update another entity in the same step
        transform: |
          {
            "filledAt": $now(),
            "status": "filled"
          }

What validate and transform can see at runtime is covered in Step onComplete — Expression Context.

Why a Tree Structure?

  • Structured and predictable — Enforces clean hierarchy, prevents spaghetti logic
  • Encapsulation — A group cleanly bundles a sub-process
  • Clear data flow — Entity evolution follows the tree, making it auditable and debuggable