Template Role Outputs

Often a signer's input in one step needs to drive a later step — the deal amount they accepted becomes the contract value used downstream, a checkbox they ticked gates the next signing step, the salary a candidate confirmed gets written back onto the employee record.

Role outputs are how signed-document data crosses that boundary cleanly. Think producer and consumer:

  • Producer — The signer's action on a document emits a structured payload, shaped by the Template role's output transform. The transform projects raw fields (text, checkboxes, signatures, timestamps, computed values) into a schema-typed payload.
  • Consumer — The blueprint's onComplete reads that payload as docOutputs.<doc>.<role> and uses it to evolve the entity context for downstream steps.

The blueprint never touches raw fields; the template's internals stay private behind the output contract.

Declaring an Output

roles:
  - key: candidate
    name: Job Candidate
    required: true
    output:
      schema: accepted_offer@1.0.0              # Validates the output shape
      transform: |                              # Shape the payload
        {
          "employeeId":    $.employee.id,
          "acceptedSalary": $.position.salary,
          "acceptedAt":    $now(),
          "startDate":     $.position.startDate,
          "signedAs":      $.fields.signature_block.full_name
        }

The transform has access to:

  • $.<inputKey> — entity inputs passed to the template
  • $.fields.<fieldKey> — values the signer entered, captured from the signed document for this role's fields
  • Custom functions — #x3C;alias>() from the template's functions: block

How It Flows Into Workflows

When a participant step completes, the engine evaluates each role's output.transform for the documents the participant signed. The result lands at docOutputs.<documentKey>.<roleKey>, where:

  • <documentKey> — the key of a document inside the envelope the step acts on (e.g. offer_letter, nda). A workflow can have many envelopes, but a single participant step signs within exactly one of them — so the envelope key isn't needed in the path. onComplete only sees docOutputs for that step's envelope, so collisions across envelopes aren't possible.
  • <roleKey> — the key of the template role whose output just produced the payload (e.g. candidate, employer). This is whichever role the participant step's assignments targeted.

So docOutputs.offer_letter.candidate reads as: "the output payload from the candidate role on the offer_letter document this participant just signed."

onComplete:
  updates:
    - key: employee
      transform: |
        $merge([
          input.employee,
          docOutputs.offer_letter.candidate        # The role output payload
        ])

See Step onComplete — Expression Context for the full variable catalog.

Why This Exists: Encapsulation

Role outputs let a template encapsulate its internals. The template can rename fields, rearrange widgets, refactor computed values — as long as the output contract holds, blueprints keep working. No leaky abstractions between presentation and process.

This matters when:

  • The same template feeds multiple blueprints. Each blueprint depends on the output contract, not template internals. Refactor the template once; nothing downstream breaks.
  • You're publishing templates to the Library. Consumers rely on the output schema — treat it like a public API.
  • You want blueprint logic to stay clean. docOutputs.offer_letter.candidate.acceptedSalary is meaningful; digging into raw field values ($fields.salary_display_text) is not.

Without an Output

If a role has no output defined, docOutputs.<doc>.<role> is absent for that role. Blueprints that need per-role signed data have no way to access it — the only fix is to update the template to expose an output. Step-level completion itself still works (the step reaches onComplete normally), but there's no structured payload of what was signed.