Updating Entities with Step Data
Guides: Evolving Workflow Data Between Steps (onCompleteMappers)
Workflows aren't static; they process information. As a workflow moves from one step to the next, data gathered in an earlier step often needs to be available or acted upon in a later step. For example, a client's name entered in Step 1 might need to appear on a document signed by a manager in Step 2.
Some systems handle this by simply passing raw field values directly between steps. SignStack uses a more robust, data-first approach. Your workflow revolves around clean, structured Entities (like ClientInfo or DealInfo). Information gathered from participants within a specific Step is transformed via template Outputs and used to formally update or evolve these central Entities. Subsequent steps then work with this consistent, up-to-date Entity data.
Think of it like updating a customer's official record in your database. When a customer service agent talks to the customer (completes a step) and gets a new phone number (the field input), they don't just pass a sticky note to the next agent. They use a formal process (onCompleteMapper) to update the central customer record (the Entity). Subsequent agents then work from that updated, official record.
Sometimes, this data evolution is simple (copying a value from an output to an entity). Other times, it can be more complex, requiring calculations or logic that uses multiple inputs – perhaps combining data from several template outputs and data from existing Entities to calculate a new value or status.
This guide explains how SignStack uses onCompleteMappers to handle this crucial data evolution step-by-step, ensuring your workflow always operates on consistent and reconciled information.
What are onCompleteMappers?
onCompleteMappers are the mechanism for evolving the EntityContext (the collection of all entities in your workflow). They are optional instructions you add to a BlueprintStep that tell the SignStack engine how to update the canonical entity data after that step successfully completes.
Think of an onCompleteMapper as the specific instruction used to update the central record after a task finishes. It's a JSONata expression whose job is to take the current state (input) and the computed outputs from templates (docOutputs) and produce the next valid state of a specific Entity.
- When they run: After a step successfully completes, before the next step begins.
- Their Purpose: To update the canonical EntityContext based on template outputs from the completed step, the existing entity state, or simply the completion event itself.
- The Engine: SignStack uses the powerful JSONata expression engine.
Defining onCompleteMappers
The onCompleteMappers property on a BlueprintStep is an array of objects. Each object defines the transformation for a single target entity.
Structure:
// Inside a BlueprintStep definition
onCompleteMappers?: StepDataMapper[];
// The StepDataMapper structure
interface StepDataMapper {
targetEntityKey: string; // Key of the entity slot to update (e.g., "client_info")
mapperExpression: string; // JSONata expression returning the NEW entity object
}
Blueprint Example:
// Inside a BlueprintStep object...
"onCompleteMappers": [
{
"targetEntityKey": "client_info", // Update the 'client_info' entity
"mapperExpression": "$merge([input.client_info, { 'lastContactDate': $now() }])"
},
{
"targetEntityKey": "deal_info", // Also update the 'deal_info' entity
"mapperExpression": "$merge([input.deal_info, { 'status': 'INFO_COLLECTED' }])"
}
]
Mapper Input Context
Your mapperExpression has access to a rich context object, including:
input: The current state of all entities in theEntityContextbefore this mapper runs. Access entity data viainput.entityKey.docOutputs: An object containing the evaluated outputs from templates used in participant steps. Outputs are structured bydocumentKeyand thenoutputKey. Only outputs for the role mapped to the participant are evaluated. (See the Templates guide on "Outputs" for how to define these.)
Example Context:
{
"input": {
"client_info": { "name": "Old Name", "status": "Pending" },
"deal_info": { "value": 50000 }
},
"docOutputs": {
"clientFormDoc": {
"clientSummary": {
"name": "Jane Doe",
"signedAt": "2024-01-15T10:30:00Z",
"dealValue": 50000
},
"approvalFlag": true
}
}
}
Using docOutputs in Mappers
Template Outputs provide a clean way to compute derived values after a participant completes their step. Instead of accessing raw field values, you can define outputs in your template that pre-compute the data you need:
"onCompleteMappers": [
{
"targetEntityKey": "clientInfo",
"mapperExpression": "$merge([ input.clientInfo, docOutputs.clientFormDoc.clientSummary ])"
}
]
💡 Tip: Define outputs in your templates to extract exactly the data you need from participant interactions. This keeps your mapper expressions clean and focused on entity evolution.
Example 1: Simple Entity Update with Outputs
After a client fills out their name and email in a form step (clientIntakeStep), save that data to the clientInfo entity using template outputs.
First, define an output in your template that captures the field values entered by the participant:
{
"outputs": [
{
"key": "clientData",
"roleKey": "client",
"schemaKey": "clientDataSchema",
"schemaVersion": "1.0.0",
"expression": "{ 'name': fields.clientNameField, 'email': fields.clientEmailField }"
}
]
}
Then use onCompleteMappers to update the entity:
"onCompleteMappers": [
{
"targetEntityKey": "client_info",
"mapperExpression": "$merge([ input.client_info, docOutputs.clientFormDoc.clientData, { 'status': 'Data Received' } ])"
}
]
- Explanation: The
$mergefunction takes the existingclientInfoentity frominputand merges it with the pre-computedclientDataoutput, plus sets the status.
Example 2: Updating Status with Computed Outputs
After a manager reviews and approves a deal (managerApprovalStep), update the dealInfo status using a template output that computes the approval result.
First, define an output in the approval template that captures the manager's decision:
{
"outputs": [
{
"key": "approvalResult",
"roleKey": "manager",
"schemaKey": "approvalResultSchema",
"schemaVersion": "1.0.0",
"expression": "{ 'status': fields.approvalCheckbox ? 'Approved' : 'Rejected', 'approvalDate': $now() }"
}
]
}
Then use onCompleteMappers:
"onCompleteMappers": [
{
"targetEntityKey": "deal_info",
"mapperExpression": "$merge([ input.deal_info, docOutputs.approvalDoc.approvalResult ])"
}
]
- Explanation: The template output pre-computes the approval status and date. The mapper simply merges this computed result into the existing
dealInfoentity.
Parallel Steps & Merging
When onCompleteMappers run within steps inside a PARALLEL container, they update their own isolated "branch" of the EntityContext. The onCompleteMapper on the parent PARALLEL container itself is then responsible for merging these branched states back together. (See the Advanced Topics guide on "Handling Parallel Data" for details).
Best Practices
- Keep Mappers Focused: Each mapper object in the array should target and update only one
Entity. - Use Functions for Complexity: If your transformation logic is complex or needs to be reused, encapsulate it within a Custom Function and call that function from your
mapperExpression. - Validate Before Mapping: Use the step's
onCompleteValidationExpression(a single expression that runs before all mappers) to ensure the output data or entity state is valid before attempting the transformations.
onCompleteMappers are the core mechanism for making your SignStack workflows intelligent and reactive, allowing the process state to evolve based on participant actions and business rules defined within your Blueprint.