Skip to main content

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. _validation is always available after a failure for use in conditions, forms, and logs.
  • Multiple retries. The flow_retrysubmitApprovalflow_to_fix loop 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.