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
outputtransform. The transform projects raw fields (text, checkboxes, signatures, timestamps, computed values) into a schema-typed payload. - Consumer — The blueprint's
onCompletereads that payload asdocOutputs.<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'sfunctions: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>— thekeyof 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.onCompleteonly seesdocOutputsfor that step's envelope, so collisions across envelopes aren't possible.<roleKey>— thekeyof the template role whose output just produced the payload (e.g.candidate,employer). This is whichever role the participant step'sassignmentstargeted.
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.acceptedSalaryis 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.
Related
- Templates — The primitive that declares roles + outputs
- Step
onComplete— Expression Context — WheredocOutputsgets consumed - The Workflow Engine — How entity context evolves across steps
