Recipe: Validate on Submit with Rework
This recipe shows a user task that validates typed output on completion and routes the user back to a correction task if validation fails — no custom error handling code required.
Scenario: An analyst submits a loan approval. The engine validates the submitted data against the declared types. If validation fails, a boundary event routes to a "fix submission" task where the analyst can correct the data and retry.
The Flow
Start → Submit Approval → [valid] → End
↓ [ValidationError]
Fix Submission → (retry) → Submit Approval
Complete BPMN
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions
xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:stateway="https://stateway.io/schema/bpmn/1.0"
targetNamespace="https://stateway.io/processes/loan-approval">
<bpmn:error id="ValidationError" name="Validation Error" errorCode="ValidationError" />
<bpmn:itemDefinition id="Decision" structureRef="stateway:inline">
<bpmn:extensionElements>
<stateway:schema kind="enum"><![CDATA[
{ "type": "string", "enum": ["approved", "rejected"] }
]]></stateway:schema>
</bpmn:extensionElements>
</bpmn:itemDefinition>
<!-- Money: non-negative number -->
<bpmn:itemDefinition id="Money" structureRef="stateway:inline">
<bpmn:extensionElements>
<stateway:schema kind="primitive"><![CDATA[
{ "type": "number", "minimum": 0 }
]]></stateway:schema>
</bpmn:extensionElements>
</bpmn:itemDefinition>
<bpmn:process id="loan-approval" isExecutable="true">
<bpmn:dataObject id="DecisionData" name="decision" itemSubjectRef="Decision" />
<bpmn:dataObject id="AmountData" name="approvedAmount" itemSubjectRef="Money" />
<bpmn:dataObjectReference id="DecisionRef" dataObjectRef="DecisionData" />
<bpmn:dataObjectReference id="AmountRef" dataObjectRef="AmountData" />
<bpmn:startEvent id="start">
<bpmn:outgoing>flow1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="submitApproval" name="Submit Approval">
<bpmn:incoming>flow1</bpmn:incoming>
<bpmn:incoming>flow_retry</bpmn:incoming>
<bpmn:outgoing>flow2</bpmn:outgoing>
<bpmn:ioSpecification>
<bpmn:dataOutput id="out_decision" name="decision" itemSubjectRef="Decision" />
<bpmn:dataOutput id="out_amount" name="approvedAmount" itemSubjectRef="Money" />
<bpmn:outputSet name="ApprovalOutput">
<bpmn:dataOutputRefs>out_decision</bpmn:dataOutputRefs>
<bpmn:dataOutputRefs>out_amount</bpmn:dataOutputRefs>
</bpmn:outputSet>
</bpmn:ioSpecification>
<bpmn:dataOutputAssociation>
<bpmn:sourceRef>out_decision</bpmn:sourceRef>
<bpmn:targetRef>DecisionRef</bpmn:targetRef>
</bpmn:dataOutputAssociation>
<bpmn:dataOutputAssociation>
<bpmn:sourceRef>out_amount</bpmn:sourceRef>
<bpmn:targetRef>AmountRef</bpmn:targetRef>
</bpmn:dataOutputAssociation>
</bpmn:userTask>
<!-- Catches ValidationError from submitApproval -->
<bpmn:boundaryEvent id="approvalError" attachedToRef="submitApproval" cancelActivity="true">
<bpmn:errorEventDefinition errorRef="ValidationError" />
<bpmn:outgoing>flow_to_fix</bpmn:outgoing>
</bpmn:boundaryEvent>
<!-- Correction task: user sees error details and corrects -->
<bpmn:userTask id="fixSubmission" name="Fix Submission">
<bpmn:incoming>flow_to_fix</bpmn:incoming>
<bpmn:outgoing>flow_retry</bpmn:outgoing>
</bpmn:userTask>
<bpmn:endEvent id="end">
<bpmn:incoming>flow2</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="flow1" sourceRef="start" targetRef="submitApproval" />
<bpmn:sequenceFlow id="flow2" sourceRef="submitApproval" targetRef="end" />
<bpmn:sequenceFlow id="flow_to_fix" sourceRef="approvalError" targetRef="fixSubmission" />
<bpmn:sequenceFlow id="flow_retry" sourceRef="fixSubmission" targetRef="submitApproval" />
</bpmn:process>
</bpmn:definitions>
Testing the Happy Path
Start an instance and complete with valid data:
# Start
curl -X POST https://api.stateway.io/v1/instances \
-H "X-API-Key: sw_live_your_key" \
-d '{ "definition_key": "loan-approval" }'
# Complete submitApproval with valid data
curl -X POST https://api.stateway.io/v1/tasks/task_01.../complete \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "decision": "approved", "approvedAmount": 50000 }'
# → 200 OK, instance reaches the end event
Testing Invalid Enum
curl -X POST https://api.stateway.io/v1/tasks/task_01.../complete \
-H "X-API-Key: sw_live_your_key" \
-d '{ "decision": "APPROVED", "approvedAmount": 50000 }'
Response 400:
{
"error": "ValidationError",
"errors": [
{
"path": "decision",
"value": "APPROVED",
"message": "must be equal to one of the allowed values: approved, rejected"
}
]
}
The boundary event approvalError fires, fixSubmission task becomes active. The _validation context is injected into instance.variables:
curl https://api.stateway.io/v1/instances/inst_01... \
-H "X-API-Key: sw_live_your_key"
{
"data": {
"status": "active",
"variables": {
"_validation": {
"path": "decision",
"value": "APPROVED",
"expected": "one of: approved, rejected",
"errors": [
{ "path": "decision", "value": "APPROVED", "message": "must be equal to one of the allowed values" }
]
}
}
}
}
Your fixSubmission form can display _validation.expected to guide the analyst. Complete fixSubmission to route back to submitApproval for a clean retry.
Testing a Missing Required Field
curl -X POST https://api.stateway.io/v1/tasks/task_01.../complete \
-H "X-API-Key: sw_live_your_key" \
-d '{ "decision": "approved" }'
Response 400:
{
"error": "ValidationError",
"message": "No outputSet satisfied. Tried: ApprovalOutput (missing: approvedAmount)"
}
The boundary event fires again. The rework loop is self-sufficient: the analyst keeps retrying until a valid complete payload is submitted.
Testing a Negative Amount
curl -X POST https://api.stateway.io/v1/tasks/task_01.../complete \
-H "X-API-Key: sw_live_your_key" \
-d '{ "decision": "approved", "approvedAmount": -500 }'
Response 400:
{
"error": "ValidationError",
"errors": [
{ "path": "approvedAmount", "value": -500, "message": "must be >= 0" }
]
}
Key Points
- No code required. The rework loop is fully declared in the BPMN diagram.
- Error context in variables.
_validationis always available after a failure for use in conditions, forms, and logs. - Multiple retries. The
flow_retry→submitApproval→flow_to_fixloop can execute as many times as needed. - Boundary always cancels. With
cancelActivity="true", the original task is cancelled before the boundary path is taken. The retry re-creates a fresh task.