Camunda Service Task via the REST API

Q&A: How Can I Complete a Service Task via the REST API?

*Camunda Platform 8, our cloud-native solution for process orchestration, launched in April 2022. Images and supporting documentation in this post may reflect an earlier version of our cloud and software solutions.


Often when integrating your business process with other services that you may not have complete control of, (like external payroll services, credit monitoring services, etc) you need an external service to handle part of the task, and maybe even complete it. This is a very common usage pattern. You have an external client you want to claim a task, complete it, and return control to your Camunda process.

Question: How can I complete a service task via the REST API?

The External Task 

Let’s execute this with an external task. Take the following BPMN diagram as an example:

The simplest BPMN you can imagine

As shown above, a call comes in, a service task handles the call and completes the task. Next, we’ll define the service task that handles this task.

Service Task definition in properties pane of Camunda Modeler

Notice that we defined the service task as an external implementation, and then added a topic of handle_call for that task. The topics should be unique as the external tasks will use them as the “key” for claiming tasks.


Now, we can implement the external service task. We’re going to do this using the JavaScript library for Camunda, which handles all of the actual REST plumbing (setting up and maintaining the http connection, certificate handling for TLS connections, etc.) for us. There are a number of officially supported clients, as well as community-contributed client libraries.


You’ll need to install the Camunda JavaScript Client with the following command:

npm install -s camunda-external-task-client-js

Next, create a Camunda client with the proper URL for your Camunda instance:

const { Client, logger } = require("camunda-external-task-client-js");
// configuration for the Client:
//  - 'baseUrl': url to the Process Engine
//  - 'logger': utility to automatically log important events
const config = { baseUrl: "http://localhost:8080/engine-rest", use: logger };

// create a Client instance with custom configuration
const client = new Client(config);

// subscribe to the topic: 'handle_call'
client.subscribe("handle_call", async function({ task, taskService }) {
  const caller_id = task.variables.get("caller_id");
  const outcome = handle_call_with_id(caller_id);
  // complete the task
  await taskService.complete(task);
});

With this setup, every time an instance arrives at that service task, an external task instance is created and is placed on the outgoing task list, waiting for an external task worker to come to claim it. The client.subscribe() function ensures that the external worker polls the queue, waiting for anything to show up on that queue, at which point it fetches the topic, locks it so no one else can claim it, and then calls the function: handle_call_with_call_id(caller_id).

Such a handler function could look like this:

function handle_call_with_id(caller_id) {
  console.log("Handling call with id: " + caller_id);
  // Implement your business logic here
  return "call_handled";
}

This function prints out the variable and returns a success message.

One thing to be aware of is that a lock is set when we “claim” the task, giving us exclusive access to that task. We can either explicitly release the lock, or simply complete the task and let the lock expire. If the execution time of your external task takes longer than the lock time, an error will appear when you try to set the task as “complete” because you no longer have a lock in the engine because the lock has timed out. Therefore, be aware of how long your external process may take, and set the appropriate timeouts. These timeouts can be set globally by the client when subscribing, or individually with each subscribe function. You can also extend your lock with a call back to the engine:

await taskService.extendLock(task, 5000);

This will provide another five seconds to complete the task.

If you run the above code, you can use the Task List in the Camunda Platform 7 Web Apps to start the process and define the `caller_id` variable. You should see output similar to the following: 

/usr/local/bin/node ./task-handler.js
✓ subscribed to topic handle_call
Handling call with id: David Simmons
✓ completed task db26f8f3-635f-11ec-a6ed-0242661ea8d3

The REST Method

Now, let’s say you don’t want to work with libraries at all. Instead, you want to do a straight REST call to grab a task. This is doable as well, but you’re going to need a little more information. The libraries that help you build external task workers handle a lot of the plumbing (like creating and managing the http connections, variable handling, etc.) for you but it’s possible to do all of this yourself if you’re so inclined. It’s also a great way to understand exactly what the external clients are doing for you. 

If you started your Camunda instance with the –- swaggerui flag, then you have a leg up. You can go here and have access to the entire REST API exposed by Camunda Platform 7.

In the above case where we have a process that has put some work on the `handle_call` task list, we can use the REST API to complete that task. 

First, we have to execute a REST call to find the process instance:

curl -X GET https://localhost:8080/engine-rest/process-instance?active=true

This will return a list of all active process instances. We can take a look at this list to find the one we’re interested in. In this example, it should look something like the following: 

{"links":[],"id":"26db7a7c-6364-11ec-952c-0242661ea8d3","definitionId":"Greenhouse:1:bc8f858f-6362-11ec-952c-0242661ea8d3","businessKey":null,"caseInstanceId":null,"ended":false,"suspended":false,"tenantId":null},

Or, if you pretty-print your JSON:

{
  "links": [],
  "id": "09970962-6366-11ec-952c-0242661ea8d3",
  "definitionId": "testjs:1:d77ea266-6365-11ec-952c-0242661ea8d3",
  "businessKey": null,
  "caseInstanceId": null,
  "ended": false,
  "suspended": false,
  "tenantId": null
}

In that JSON, we see the ID, which we can then use to get the external task:

curl -X GET https://localhost:8080/engine-rest/external-task/0997307a-6366-11ec-952c-0242661ea8d3

And that will give us the information.

{
  "activityId": "Activity_149r9v6",
  "activityInstanceId": "Activity_149r9v6:09973079-6366-11ec-952c-0242661ea8d3",
  "errorMessage": null,
  "executionId": "09973078-6366-11ec-952c-0242661ea8d3",
  "id": "0997307a-6366-11ec-952c-0242661ea8d3",
  "lockExpirationTime": null,
  "processDefinitionId": "testjs:1:d77ea266-6365-11ec-952c-0242661ea8d3",
  "processDefinitionKey": "testjs",
  "processDefinitionVersionTag": null,
  "processInstanceId": "09970962-6366-11ec-952c-0242661ea8d3",
  "retries": null,
  "suspended": false,
  "workerId": null,
  "topicName": "handle_call",
  "tenantId": null,
  "priority": 0,
  "businessKey": null
}

We know a lot about the process and from the returned data such as the ID, the process definition id, etc., but we also can see that the task has the `topic-id` of ‘handle_call`. Using the REST API, we can fetch and lock that task:

curl -X POST "https://localhost:8080/engine-rest/external-task/fetchAndLock" -H  "accept: application/json" -H  "Content-Type: application/json" -d "{\"workerId\":\"aWorkerId\",\"maxTasks\":1,\"usePriority\":true,\"topics\":[{\"topicName\":\"handle_call\",\"lockDuration\":600000,\"variables\":[\"orderId\"]}]}"

There’s a lot in there, so let’s break that down a bit. We are sending a JSON object with a bunch of information: 

{
  "workerId": "aWorkerId",
  "maxTasks": 1,
  "usePriority": true,
  "topics": [
    {
      "topicName": "handle_call",
      "lockDuration": 600000,
      "variables": [
        "caller_id"
      ]
    }
  ]
}

The important things to note are the workerId, the topics, and the lockDuration. The lockDuration in this example is 60 seconds because we need time to actually complete the task.

Now that the task is locked, we need to complete it. Make another post to complete it:

curl -X POST "https://davidgs.com:8443/engine-rest/external-task/0997307a-6366-11ec-952c-0242661ea8d3/complete" -H  "accept: */*" -H  "Content-Type: application/json" -d "{\"workerId\":\"aWorkerId\",\"variables\":{\"aVariable\":{\"value\":\"aStringValue\"},\"anotherVariable\":{\"value\":42},\"aThirdVariable\":{\"value\":true}},\"localVariables\":{\"aLocalVariable\":{\"value\":\"aStringValue\"}}}"

Again, that’s a mouthful, so let’s break down the JSON we’re sending back to complete the task: 

{
  "workerId": "aWorkerId",
  "variables": {
    "aVariable": {
      "value": "aStringValue"
    },
    "anotherVariable": {
      "value": 42
    },
    "aThirdVariable": {
      "value": true
    }
  }
}

Not all of the JSON we sent is required, but I thought it would be good to give a complete example. The really important bit is that the `workerId` must be the same as the `workerId` you used to fetch and lock the task. Otherwise, this’ll fail because someone else owns the task.

If you want to send any information back to the process, you can include variables in your response to complete the task. 

If you’re using Postman (or Paw, my personal favorite), you can have the client application generate the code for you in almost any language once you have the API call working properly. That way, you know the call is correct, and you can be certain the code to make the call is correct as well. Again, if there’s a client library available for your preferred language, you’re better served by using that rather than doing it all yourself. However, working with these REST calls is a great learning experience.

Now, you know two ways to complete external tasks. There’s a simpler way (using a client library) and a more complex way (using the REST API yourself). Using client libraries is recommended for production environments, but the REST client is a great learning tool to better understand how Camunda and the client libraries handle external tasks.