External Task
External Tasks in tSM delegate execution to an external worker instead of running logic inside the process engine. This enables clean separation of responsibility, independent scaling, and integration with systems outside tSM.
When to Use an External Task
Use an External Task when:
- the action should not run inside the process engine
- you want clear separation of responsibility
- the logic is implemented in a separate service (e.g. an integration connector or third-party system)
- retries, failures, and scaling should be handled outside the engine
Do not use External Tasks for:
- BPMN listeners (they are lifecycle hooks, not jobs)
- simple in-process logic (use Service Tasks with scripts or delegates instead)
- one bounded AI operation (use an Agent Task)
- collaboration with an independently operated agent (use an A2A Agent Task)
How It Works
An External Task is a standard Service Task configured with the type External. Instead of running logic directly:
- The tSM Process Engine creates an external task instance with a specific topic
- An external worker subscribes to that topic and fetches the task via REST (
fetchAndLock) - The worker performs the work
- The worker completes (or fails) the task, optionally returning output variables
The external worker is a separately implemented service — it defines a strict contract: which topic it listens to, what input variables it expects, and what output it produces.
Task Templates and External Tasks
Because external workers expect a specific contract (topic, input variables, structure), you cannot set arbitrary properties — the configuration in the BPMN model must match exactly what the worker understands.
For this reason, external task workers are typically delivered together with a Task Template. In most cases, you do not need to configure external tasks manually. Instead, you:
- Select a Task Template from the palette in the tSM Designer (e.g. "CRM Enrichment")
- Fill in the template form — the template guides you through the required parameters
- The template generates the correct BPMN configuration automatically (topic, input mapping, defaults)
This approach:
- prevents misconfiguration that would cause the worker to fail
- provides a guided UI so you don't need to know the worker's internal contract
- applies consistent defaults and validates inputs
- makes external tasks accessible to process designers without deep technical knowledge
If a Task Template is available for the external worker you want to use, always prefer the template over manual configuration. Manual setup is described below for reference and for cases where no template exists.
Manual Configuration
The rest of this page describes how to configure an External Task manually in the tSM Designer. This is the baseline — everything here also applies when a Task Template generates the configuration for you.
Step 1: Add a Service Task
Drag a Service Task into the process and give it a meaningful name (e.g. Enrich Customer).
An External Task uses the standard Service Task BPMN element — no custom elements are needed.
Step 2: Set execution type to External
In the Service Task properties:
- Set Implementation / Type to External Task
- Set Topic to a string identifying the operation
Example:
Topic: crm.customer.enrich
This topic is what the external worker will subscribe to.
Passing Input Data
External workers read process variables. All input for the external task must be provided as process variables.
Input Mapping
Use Input Mapping on the Service Task to:
- define variables explicitly
- compute values using expressions
- keep the process readable
Example
In the Service Task → Input/Output section:
<camunda:inputOutput>
<camunda:inputParameter name="customerRequest">
<camunda:map>
<camunda:entry key="customerId">#{customerId}</camunda:entry>
<camunda:entry key="includeContracts">#{true}</camunda:entry>
</camunda:map>
</camunda:inputParameter>
<camunda:inputParameter name="requestedBy">#{requestOwner}</camunda:inputParameter>
</camunda:inputOutput>
Notes:
- Expressions use SpEL syntax
#{...} - Maps and lists are allowed
- Values are evaluated when the task is reached
- Variables are available to the external worker
What the External Worker Receives
When the worker fetches the task, it receives:
- the topic
- the process instance context
- all mapped input variables
Example variables payload:
{
"customerRequest": {
"customerId": "CUS-123",
"includeContracts": true
},
"requestedBy": "support-l1"
}
Completing the External Task
After performing the work, the worker must:
- complete the task
- optionally set output variables
Example completion payload:
{
"customerEnrichment": {
"segment": "BUSINESS",
"activeContracts": 3
}
}
These variables become available to the rest of the process.
Error Handling and Retries
Recoverable errors
If the worker encounters a recoverable error:
- report failure
- let the process engine retry according to the retry policy
Non-recoverable errors
- set retries to
0 - optionally set error details
The process engine stops retrying and creates an incident.
Worker API (REST)
tSM exposes a Camunda 7 wire-compatible External Task API. The request and response payloads mirror the upstream Camunda REST contract, so an unmodified Camunda External Task Client works against tSM after changing only its base URL.
Base path: /api/v2/camunda on the service that hosts the process engine
(tsm-process-definition on the platform). Behind the gateway the full path includes the service
prefix:
https://<host>/tsm-process-definition/api/v2/camunda
Endpoints
| Method and path | Purpose |
|---|---|
POST /external-task/fetchAndLock | Atomically fetch and lock tasks; supports long polling |
POST /external-task/{id}/complete | Complete a locked task, optionally with output variables |
POST /external-task/{id}/failure | Report a technical failure with retries and retryTimeout |
POST /external-task/{id}/bpmnError | Report a BPMN error (errorCode) handled by the model |
POST /external-task/{id}/lock | Lock a task for a worker |
POST /external-task/{id}/extendLock | Extend an existing lock |
POST /external-task/{id}/unlock | Release a lock |
POST /process-instance/{id}/variables | Set or delete process variables (modifications, deletions) |
GET /process-instance/{id}/variables/{name}/data | Download a deferred File / Bytes variable |
fetchAndLock returns the locked tasks, the binary endpoint returns the raw content; everything
else returns 204 No Content. Errors use the Camunda-compatible body { "type": ..., "message": ... }.
Authentication and privileges
The worker calls the API as a regular tSM identity. Two credentials are supported:
X-API-Key— the recommended path. The gateway exchanges the key for a backend token.Authorization: Bearer <token>— when the worker already holds an access token.
Unlike internal service calls, this API does not accept the inter-service User-Agent bypass —
the credential must be directly verifiable. An unauthenticated call gets 401, a call without the
required privilege gets 403.
Required privileges:
| Privilege | Needed for |
|---|---|
!Process.ExternalTask.Execute | The whole worker API (fetch, complete, failure, lock, variables) |
!Process.ExternalTask.Variable.Read | Additionally for the binary variable download |
!Process.ExternalTask | Parent privilege covering the complete worker API |
Bind the API key to a dedicated service account with a minimal role that holds only the
!Process.ExternalTask* privileges — do not reuse an administrator account. Because the standalone
setVariables request carries only a workerId, tSM accepts the write only while that worker owns
an active lock in the target process instance.
Long polling
fetchAndLock is served asynchronously. Set asyncResponseTimeout (milliseconds) to keep the
request open until a task appears; the engine answers immediately once a matching task is created.
Omit it for a plain, non-blocking fetch.
Example
Request:
POST /tsm-process-definition/api/v2/camunda/external-task/fetchAndLock
X-API-Key: tsm_ak_…
Content-Type: application/json
{
"workerId": "invoice-worker",
"maxTasks": 5,
"usePriority": false,
"asyncResponseTimeout": 10000,
"topics": [
{
"topicName": "ai.agent.call",
"lockDuration": 60000,
"variables": ["aiAgentId", "aiInput"]
}
]
}
Response:
[
{
"id": "a1b2c3…",
"topicName": "ai.agent.call",
"workerId": "invoice-worker",
"processInstanceId": "…",
"lockExpirationTime": "2026-01-31T10:15:00.000+0000",
"retries": null,
"variables": {
"aiAgentId": { "value": "support-triage", "type": "String", "valueInfo": {} }
}
}
]
Completing the task:
POST /tsm-process-definition/api/v2/camunda/external-task/a1b2c3…/complete
{
"workerId": "invoice-worker",
"variables": {
"aiResponse": { "value": "{\"summary\":\"…\"}", "type": "Json" }
}
}
Writing a worker in Kotlin or Java
The module tsm-camunda-client is a thin factory over the standard Camunda External Task Client —
it keeps the whole upstream worker API and only points it at the tSM endpoint and adds the
credential header:
val client = TsmCamundaClient.externalTasks(
serviceBaseUrl = "https://tsm.example.com/tsm-process-definition",
apiKey = apiKey,
tenantId = "default", // optional, sent as X-Tenant-Id
)
.workerId("invoice-worker")
.asyncResponseTimeout(20_000)
.build()
client.subscribe("ai.agent.call")
.handler { task, service -> service.complete(task) }
.open()
Use TsmCamundaClient.externalTasksWithAccessToken(...) when the worker authenticates with a Bearer
token instead of an API key. serviceBaseUrl may be given with or without the /api/v2/camunda
suffix.
A plain Camunda client works too — point it at the full base URL and add the credential header yourself:
ExternalTaskClient.create()
.baseUrl("https://tsm.example.com/tsm-process-definition/api/v2/camunda")
Trying the API out
A ready-made Hoppscotch collection covering all endpoints (with automatic taskId capture)
lives in the tsm-process-engine repository under
tsm-camunda/src/test/resources/hoppscotch/, together with environment templates and a README.
Import it, fill in the base URL and the API key, and run the requests against a deployed process.
Best Practices
- Use Task Templates whenever available — they ensure correct configuration
- Use clear, stable topic names
- Keep input payloads reasonably small
- Prefer structured objects over large strings
- Treat External Tasks as integration boundaries
- Give each worker its own service account with a minimal role (
!Process.ExternalTask*only)
Summary
To use an External Task in tSM:
- Preferred: Select a Task Template for the external worker from the palette and fill in the form
- Manual: Create a Service Task, set it to External Task, define a topic, and configure input mapping
- An external worker fetches the task, performs the work, and completes it