I/O Specification
An I/O Specification declares the typed inputs and outputs of a specific task, independent of the process-level variable names. It is the local contract for one activity.
These typed constructs (itemDefinition, ioSpecification, typed dataInput/dataOutput) are
parsed only from BPMN 2.0 XML definitions. JSON/YAML process definitions express a subset: declare a
userTask form explicitly via formSchema, and note that activity outputs are not type-validated.
See Processes → Typed I/O & Form Schemas.
Why a Local Contract?
Without an I/O specification, a task implicitly reads and writes named variables. With one, you get:
- Name decoupling. The form shown to a user can use
amountwhile the process variable isapplication.amount. - Multiple completion paths. An approval task might accept either
{ decision, approvedLimit }or{ decision, rejectionReason }— each is a validoutputSet. - Automatic form generation. When the Frontend Gateway generates a task form, it reads the
ioSpecificationto know what fields to render and what types to enforce.
Elements Inside ioSpecification
| Element | Purpose |
|---|---|
<bpmn:dataInput> | Named input parameter with a type |
<bpmn:dataOutput> | Named output parameter with a type |
<bpmn:inputSet> | Groups required inputs for one mode of execution |
<bpmn:outputSet> | Groups required outputs for one completion mode |
Outside the spec, association elements wire parameters to process-level variables:
| Element | Purpose |
|---|---|
<bpmn:dataInputAssociation> | Maps a process dataObjectReference → task dataInput |
<bpmn:dataOutputAssociation> | Maps a task dataOutput → process dataObjectReference |
Minimal Example — Single Output
A review task that writes a typed decision:
<bpmn:userTask id="review" name="Review Application">
<bpmn:ioSpecification>
<bpmn:dataOutput id="out_decision" name="decision" itemSubjectRef="LoanStatus" />
<bpmn:outputSet>
<bpmn:dataOutputRefs>out_decision</bpmn:dataOutputRefs>
</bpmn:outputSet>
</bpmn:ioSpecification>
<bpmn:dataOutputAssociation>
<bpmn:sourceRef>out_decision</bpmn:sourceRef>
<bpmn:targetRef>StatusDataRef</bpmn:targetRef>
</bpmn:dataOutputAssociation>
</bpmn:userTask>
When POST /v1/tasks/:id/complete is called, the engine:
- Checks that
decisionis present in the payload. - Validates
decisionagainst theLoanStatusenum. - Merges the value into
instance.variables.loan_status(via the output association).
Multiple Output Sets — Different Completion Paths
An approval task that accepts two distinct completion modes:
<bpmn:userTask id="approve" name="Approve Loan">
<bpmn:ioSpecification>
<!-- Inputs: populated from process variables by the Frontend Gateway -->
<bpmn:dataInput id="in_amount" name="amount" itemSubjectRef="Money" />
<bpmn:dataInput id="in_applicant" name="applicant" itemSubjectRef="Applicant" />
<bpmn:inputSet name="Default">
<bpmn:dataInputRefs>in_amount</bpmn:dataInputRefs>
<bpmn:dataInputRefs>in_applicant</bpmn:dataInputRefs>
</bpmn:inputSet>
<!-- Outputs -->
<bpmn:dataOutput id="out_decision" name="decision" itemSubjectRef="LoanStatus" />
<bpmn:dataOutput id="out_limit" name="approvedLimit" itemSubjectRef="Money" />
<bpmn:dataOutput id="out_reason" name="rejectionReason" itemSubjectRef="xsd:string" />
<!-- Path 1: approved with a limit -->
<bpmn:outputSet name="Approval">
<bpmn:dataOutputRefs>out_decision</bpmn:dataOutputRefs>
<bpmn:dataOutputRefs>out_limit</bpmn:dataOutputRefs>
</bpmn:outputSet>
<!-- Path 2: rejected with a reason -->
<bpmn:outputSet name="Rejection">
<bpmn:dataOutputRefs>out_decision</bpmn:dataOutputRefs>
<bpmn:dataOutputRefs>out_reason</bpmn:dataOutputRefs>
</bpmn:outputSet>
</bpmn:ioSpecification>
<!-- Input association: map process variable field to formal input -->
<bpmn:dataInputAssociation>
<bpmn:sourceRef>ApplicationDataRef</bpmn:sourceRef>
<bpmn:targetRef>in_amount</bpmn:targetRef>
<bpmn:assignment>
<bpmn:from>application.amount</bpmn:from>
<bpmn:to>in_amount</bpmn:to>
</bpmn:assignment>
</bpmn:dataInputAssociation>
<!-- Output association: write formal output to process variable -->
<bpmn:dataOutputAssociation>
<bpmn:sourceRef>out_decision</bpmn:sourceRef>
<bpmn:targetRef>StatusDataRef</bpmn:targetRef>
</bpmn:dataOutputAssociation>
</bpmn:userTask>
The engine checks whether the submitted output satisfies at least one declared outputSet. If neither is satisfied, validation fails and the task stays in claimed state.
Completing with Path 1 (Approval)
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",
"approvedLimit": 50000
}'
Completing with Path 2 (Rejection)
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": "rejected",
"rejectionReason": "Insufficient income documentation"
}'
I/O Specification on Service Tasks
Service tasks can also have an ioSpecification. The dataOutput types validate the HTTP response body returned by the external service:
<bpmn:serviceTask id="credit-check" name="Credit Check">
<bpmn:ioSpecification>
<bpmn:dataOutput id="out_score" name="creditScore" itemSubjectRef="xsd:int" />
<bpmn:dataOutput id="out_approved" name="approved" itemSubjectRef="xsd:boolean" />
<bpmn:outputSet>
<bpmn:dataOutputRefs>out_score</bpmn:dataOutputRefs>
<bpmn:dataOutputRefs>out_approved</bpmn:dataOutputRefs>
</bpmn:outputSet>
</bpmn:ioSpecification>
</bpmn:serviceTask>
If the external service returns { "creditScore": "good", "approved": true }, validation fails because creditScore must be an integer. The service task's boundary error event can catch this, or the instance moves to error state.
What the Frontend Gateway Delivers
When a task has an ioSpecification, the Frontend Gateway's task snapshot includes only the declared dataInput fields — by their formal name, not the underlying process variable name:
{
"taskId": "task_01...",
"name": "Approve Loan",
"snapshot": {
"amount": 75000,
"applicant": { "fullName": "Ana Lima", "email": "ana@example.com" }
}
}
The SDK exposes these via work.snapshot.amount and work.snapshot.applicant. If no form_schema is present, the Gateway automatically generates a form from the dataInput types, rendering enums as dropdowns and objects as field groups. See Form Schema Migration for details on using both together.
Use multiple outputSet elements when a task can be completed in genuinely different ways (approve vs. reject, full vs. partial). Use a single outputSet with all optional fields when the completion payload is always the same shape but some fields may be absent.