Skip to main content

Service Tasks

Service tasks (serviceTask) make HTTP calls to external services, enabling integration with any REST API. They support configurable headers, input/output mapping, and two execution modes: synchronous and asynchronous.

Defining a Service Task

In JSON/YAML

Use taskDefinition, taskHeaders, and ioMapping (no namespace prefix needed in JSON/YAML):

{
"id": "charge-payment",
"type": "serviceTask",
"name": "Charge Payment",
"taskDefinition": { "type": "http" },
"taskHeaders": {
"url": "https://payments.example.com/charge",
"method": "POST",
"executionMode": "sync",
"timeout": "15000"
},
"ioMapping": {
"inputs": [
{ "source": "=variables.amount", "target": "amount" },
{ "source": "=variables.order_id", "target": "orderId" }
],
"outputs": [
{ "source": "paymentId", "target": "variables.paymentId" },
{ "source": "paymentStatus", "target": "variables.paymentStatus" }
]
},
"outgoing": ["flow-to-next"]
}

In BPMN XML

Use the stateway: namespace prefix:

<bpmn:serviceTask id="chargePayment" name="Charge Payment">
<bpmn:extensionElements>
<stateway:taskDefinition type="http" />
<stateway:taskHeaders>
<stateway:header key="url" value="https://payments.example.com/charge" />
<stateway:header key="method" value="POST" />
<stateway:header key="executionMode" value="sync" />
<stateway:header key="timeout" value="15000" />
</stateway:taskHeaders>
<stateway:ioMapping>
<stateway:input source="=variables.amount" target="amount" />
<stateway:input source="=variables.order_id" target="orderId" />
<stateway:output source="paymentId" target="variables.paymentId" />
<stateway:output source="paymentStatus" target="variables.paymentStatus" />
</stateway:ioMapping>
</bpmn:extensionElements>
</bpmn:serviceTask>

Supported Task Headers

HeaderDefaultDescription
urlrequiredTarget HTTP endpoint; supports {{variables.x}} interpolation
methodPOSTHTTP method: GET, POST, PUT, PATCH, DELETE
headersJSON string of additional HTTP headers: '{"X-Trace": "123"}'. Values support {{variables.x}} and {{credentials.name}} interpolation
authorizationShorthand for a single Authorization header; supports {{variables.x}} and {{credentials.name}} interpolation. Takes precedence over an Authorization key set via headers
executionModesyncsync or async (see below)
timeout30000Request timeout in ms or ISO 8601 duration (e.g., PT30M)

The following are planned, not yet implemented — the engine does not read them:

HeaderPlanned behavior
callbackEventWould validate that the async callback event name matches; today any event delivered via POST /instances/:id/events resumes the token
resultVariableWould store the full response body under a named variable; today use ioMapping.outputs instead

There is also no built-in retry policy — a failed or timed-out service task moves the instance directly to error on the first attempt (see Failure Behavior).

Using Credentials in Task Headers

Reference stored credentials with {{credentials.name}} instead of embedding secrets in the definition. Either the dedicated authorization header or the generic headers JSON works:

{
"taskHeaders": {
"url": "https://payments.example.com/charge",
"method": "POST",
"authorization": "Bearer {{credentials.payment_api_token}}"
}
}

In BPMN XML:

<stateway:header key="authorization" value="Bearer {{credentials.payment_api_token}}" />

The engine resolves the credential at execution time. The decrypted value exists only in memory during token execution and is never written to logs, the audit log, or webhook payloads.

See Credentials for how to create and rotate credentials.

Input/Output Mapping

Inputs

inputs are evaluated before the HTTP request is sent and merged into the request body:

FieldDescription
sourceExpression referencing instance variables (e.g., =variables.amount)
targetKey in the HTTP request body

Outputs

outputs are extracted from the HTTP response body and merged into instance variables:

FieldDescription
sourceProperty path in the response body (e.g., result.id or just id)
targetInstance variable name (e.g., variables.invoiceId)

Execution Modes

Synchronous (Default)

The engine sends the HTTP request and waits for a response. Outputs are mapped and the token advances only after the response is received.

If the request fails or times out (default 30 s, configurable via timeout), the instance moves directly to error — there is no automatic retry (see Failure Behavior).

{ "taskHeaders": { "executionMode": "sync", "timeout": "15000" } }

Asynchronous

The engine sends the HTTP request (fire-and-forget), puts the token in waiting status, and immediately returns. The token advances only when the external service sends a callback via POST /instances/:id/events.

{
"taskHeaders": {
"executionMode": "async",
"timeout": "PT30M"
}
}

The timeout sets how long the engine waits before the token moves to error. Any event delivered via the events endpoint resumes the token — the engine does not currently validate the event name against a configured value.

When the external service is done, it calls:

Request:

curl -X POST https://api.stateway.io/v1/instances/{instance_id}/events \
-H "X-API-Key: sw_live_..." \
-H "Content-Type: application/json" \
-d '{
"event_type": "payment_completed",
"variables": { "transactionId": "tx-123" }
}'

Response:

{
"data": {
"event_type": "payment_completed",
"processed": true
}
}

Service Task Lifecycle

Synchronous Mode:

token arrived


HTTP request sent

┌────┴──────────────┐
│ 2xx response │ 4xx / 5xx / timeout
▼ ▼
outputs mapped instance → error
token advances

Asynchronous Mode:

token arrived


HTTP request sent (fire-and-forget)

token → waiting for callback event

┌────┴─────────────────────────────┐
│ callback event received │ timeout expires
▼ ▼
outputs mapped instance → error
token advances

Failure Behavior

ScenarioBehavior
HTTP 2xxSuccess — outputs mapped, token advances
HTTP 4xx / 5xxFailure — instance moves to error status (no retry)
TimeoutFailure — instance moves to error status (no retry)
Response body over 5 MBFailure — the response is discarded (not stored) and the instance moves to error status (no retry)

The response body of a service task is capped at 5 MB. If an upstream service returns more than that, the response is not stored and the process does not advance. The cap is applied to the stored copy of the response as well as to what arrives on the wire — a response that is not valid JSON, or that contains control characters, is stored in an escaped form that is larger than the original, so it can cross the 5 MB line even when the body sent by the service was smaller.

Retry logic is planned but not implemented. If you need retry semantics today, catch the instance.error webhook and re-run the instance. An error boundaryEvent on a service task only catches output-validation failures (the response body failing the task's declared output contract); it does not catch transport failures — timeout, a non-2xx status, or a response over the size limit — which always route the instance directly to error (see Processes).

Best Practices

  • Use IoMapping — explicit variable mapping avoids naming collisions and makes the data flow readable
  • Store secrets in credentials — never embed API keys or tokens directly in taskHeaders; use {{credentials.name}}
  • Set explicit timeouts — don't rely on the 30-second default; set timeout to match the expected latency of the external API
  • Use ISO 8601 for long timeoutsPT2M is more readable than "120000" for durations over a minute
  • Use async for slow operations — if the external API takes more than a few seconds, use async mode to avoid blocking the process engine
  • Design for no automatic retry — a single failure moves the instance to error; make the downstream system idempotent and use instance.error webhooks to detect and recover
  • Handle failures — set up a webhook on instance.error and inspect the error field to diagnose which service task failed
  • Keep responses small — return only the fields you need and paginate large collections instead of returning the entire set; responses over 5 MB are rejected (see Failure Behavior)