Skip to content
GitHub

Migrating Camunda 7 External Tasks

Camunda 7 external tasks let workers pull work over REST: a worker calls fetchAndLock for a topic, does the job, then calls complete, failure or bpmnError. The fleans-camunda-external-task plugin serves the same REST API on top of Fleans. You can move your processes to Fleans and keep your workers. In most cases you only change their base URL.

The engine has no Camunda-specific code. The plugin is an ordinary custom-task plugin running in its own plugin host that joins the engine’s cluster. It stores waiting tasks in its own table and reports workers’ results back to the workflow.

Plugin 0.8.0 requires Fleans 0.8.0 or later.

Replace Camunda’s camunda:type="external" camunda:topic="…" attribute pair with a Fleans task type that starts with camunda::

<!-- Camunda 7 -->
<bpmn:serviceTask id="charge" camunda:type="external" camunda:topic="charge-card" />
<!-- Fleans -->
<bpmn:serviceTask id="charge" type="camunda:charge-card" />

Workers keep subscribing to the topic (charge-card). Camunda’s <camunda:inputOutput> is not read. Use Fleans input/output mappings instead; the plugin README has a conversion table.

The task’s businessKey is the process instance’s business id, set when you start the instance ("BusinessId": "order-123" on POST /Execution/start or POST /Execution/message). You don’t need a mapping for it.

Other Camunda task metadata comes from reserved input targets, evaluated when the task is queued (except extension properties, evaluated at fetch time) and never sent as variables:

<zeebe:ioMapping>
<zeebe:input source="=order.priority" target="taskPriority" /> <!-- camunda:taskPriority -->
<zeebe:input source="acme" target="tenantId" />
<zeebe:input source="eu-west" target="extensionProperty_region" /> <!-- camunda:properties -->
</zeebe:ioMapping>

A businessKey input replaces the business id, even when it evaluates to null. Use it where the instance has no business id, such as a call activity’s child instance: <zeebe:input source="=order.id" target="businessKey" />.

The plugin repository ships a ready-to-run host image, ghcr.io/nightbaker/fleans-camunda-external-task. With the Docker Compose bundle:

Start the engine first. The bundle creates the fleans_default network that the plugin host joins, and the network only gets that name when the compose project is called fleans:

Terminal window
cd <unzipped-engine-compose-bundle>
echo "COMPOSE_PROJECT_NAME=fleans" >> .env # names the network fleans_default
docker compose up -d # engine first: creates the network

Then add a docker-compose.override.yaml next to the bundle’s docker-compose.yaml:

services:
camunda-external-task:
image: ghcr.io/nightbaker/fleans-camunda-external-task:<version>
networks: [fleans_default]
ports: ["8090:8080"]
environment:
ConnectionStrings__orleans-redis: "redis:6379,password=${ORLEANS_REDIS_PASSWORD}"
Orleans__ClusterId: "fleans"
Orleans__ServiceId: "fleans"
ConnectionStrings__camunda-external-tasks: "Host=postgres;Database=camunda_external_tasks;Username=postgres;Password=${POSTGRES_PASSWORD}"
depends_on: [redis, postgres]
networks:
fleans_default:
external: true
name: fleans_default

Then run docker compose up -d again from the bundle directory. Compose merges the override and starts the plugin host on the existing network. If you skip the engine-first step or the project name, it fails with network fleans_default declared as external, but could not be found.

Orleans__ClusterId and Orleans__ServiceId must match the bundle’s CLUSTER_CLUSTER_ID and CLUSTER_SERVICE_ID. The database and the camunda_external_tasks table are created at startup.

To run the plugin inside a plugin host you already have, add the Fleans.Plugins.CamundaExternalTask NuGet package and register it with AddCamundaExternalTasks(...) and MapCamundaExternalTaskApi(). See the repository README for details and every configuration setting.

ExternalTaskClient.create().baseUrl("http://camunda-external-task:8080/engine-rest").build();
new Client({ baseUrl: "http://camunda-external-task:8080/engine-rest" });

Every endpoint the official Java and JS clients call is supported: fetchAndLock (including long polling, usePriority, sorting, and the businessKey / tenant filters), complete, failure, bpmnError, lock, extendLock, unlock, and the file-variable download. The operator endpoints are there too: task queries and counts, topic-names, PUT /retries and PUT /priority. Errors use Camunda’s body and status codes. Set CamundaExternalTasks__BasicAuth__Username and __Password on the host to require HTTP basic authentication (the clients’ BasicAuthProvider / BasicAuthInterceptor).

  • Incidents are opt-in. By default, failure with retries = 0 fails the activity with error code 500, and an error boundary event can catch it. With CamundaExternalTasks__Incidents=true the task stays as an incident, as in Camunda, until PUT /external-task/{id}/retries resolves it. Incidents are not shown in the Fleans UI.
  • bpmnError fails the activity with the worker’s errorCode, so an error boundary event with that code catches it. Its variables are visible to the boundary or error event sub-process that catches it, but not to an error boundary on a parent workflow’s call activity.
  • Variables: primitives, Date (kept as a string), Json, Bytes, File, and Object with application/json are supported. Binary values live in the workflow state, so keep them small. Java-serialized objects are rejected.
  • Missing fields: processDefinitionVersionTag is always null, because Fleans has no equivalent.
  • Scope: the /external-task resource plus the file-variable download. Start processes and correlate messages through the Fleans REST API.

The repository README lists every difference.