Tutorial
In this tutorial, we'll create a Spring Boot application that utilizes Zeebe and Tasklist REST APIs for managing user tasks within Camunda.
Getting started
For this tutorial we'll need:
- JDK 17: Ensure you have Java Development Kit 17 installed on your machine.
- A Camunda 8 cluster on SaaS or Self-Managed.
- A set of API credentials for SaaS or Self-Managed. Check the Tasklist option when creating these.
Don't forget to save these credentials as you'll need them later.
Create a new Spring Boot application
Go to the Spring Initializr website to create a new Spring Boot project. Specify your project's details:
- Project: Maven
- Spring Boot: 3.2.4
- Java: 17
- Group: com.example
- Package name: com.example.demo
This will generate a Maven project.
Integrate spring-zeebe
spring-zeebe
is a community Spring integration library and integrates the Zeebe API within the Spring environment.
Add the following Maven dependency in the pom.xml
file to use spring-zeebe
:
<dependencies>
<!-- other dependencies -->
<dependency>
<groupId>io.camunda.spring</groupId>
<artifactId>spring-boot-starter-camunda</artifactId>
<version>8.5.0</version>
</dependency>
</dependencies>
Handle connection and authentication
Connection and authentication configuration depends on the client mode, whether it is simple (local dev), OIDC, or SaaS. For full details, review spring-zeebe under the Configuring Camunda 8 connection section.
For SaaS, create a file src/main/resources/application.yaml
and add the following configuration, replacing the API client details:
camunda:
client:
mode: saas
auth:
client-id: <your client id>
client-secret: <your client secret>
cluster-id: <your cluster id>
region: <your cluster region>
Generate the Tasklist API client
The Tasklist API is not part of the built-in client in the spring-zeebe
library. Let's generate one using openapi-generator-maven-plugin
:
- Download the open API spec
yaml
file${tasklistBaseUrl}/v3/api-docs.yaml
(for example,http://localhost:8082/v3/api-docs.yaml
if you run Tasklist in your local environment orhttps://${region}.tasklist.camunda.io/${clusterId}/v3/api-docs.yaml
if you are using Camunda 8 SaaS.) - Copy the downloaded spec file to
src/main/resources/api-docs.yaml
. - Add the following plugin in
pom.xml
:
<build>
<plugins>
<!-- other plugins -->
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.2.0</version>
<executions>
<execution>
<id>generate-client</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api-docs.yaml</inputSpec>
<generatorName>java</generatorName>
<apiPackage>com.example.tasklist.api</apiPackage>
<modelPackage>com.example.tasklist.model</modelPackage>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<library>apache-httpclient</library>
<configOptions>
<useJakartaEe>true</useJakartaEe>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
- The generated code requires some extra Maven dependencies, add them in
pom.xml
:
<dependencies>
<!-- other dependencies -->
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.6</version>
</dependency>
</dependencies>
- Execute
mvn clean install
. This will generate the data model classes alongside the API client classes, allowing you to invoke the Tasklist endpoints. The generated code is located under thetarget
folder. - Create a
TasklistClientConfiguration
configuration class under thecom.example.demo.config
package, and define thetaskApi
bean:
package com.example.demo.config;
import com.example.tasklist.ApiClient;
import com.example.tasklist.api.TaskApi;
import io.camunda.zeebe.spring.client.properties.CamundaClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TasklistClientConfiguration {
@Bean
public TaskApi taskApi(ApiClient apiClient) {
return new TaskApi(apiClient);
}
@Bean
public ApiClient apiClient(CamundaClientProperties camundaClientProperties) {
return new ApiClient()
.setBasePath(camundaClientProperties.getTasklist().getBaseUrl().toString());
}
}
Use the APIs
Now we're ready to make requests to the APIs.
We will use a simple process with a user task. The user task has the Zeebe user task
implementation type. Review migrate to Zeebe user tasks for more details.
This means we must use the Zeebe REST API for user task operations (assign, complete, etc.) and the Tasklist REST API for task querying (search, get by id, etc.).
For this demo, give the process the ID api-test-process
.
You can download the process here: api-test-process.bpmn
You can automate the deployment of the process in Zeebe upon application startup. Save your BPMN file under the src/main/resources/models
folder. Then, include the following annotation in your DemoApplication.java
class: @Deployment(resources = "classpath*:/models/*.*")
.
Let's create a service that:
- Starts a process instance
- Searches for the newly created Zeebe user tasks
- Picks one user task
- Assigns the task to a user
- Gets the user task by id
- Completes the user task
- Gets the completed task and checks the state
First, let's create a class annotated with Spring's @Service and autowire the required beans:
ZeebeClient
, which will be used for making calls to the Zeebe API.TaskApi
, the bean you configured inTasklistClientConfiguration
, which will be used to call the Tasklist Tasks API.Authentication
, which will be utilized for retrieving the authentication token header to be included in Tasklist API requests.
import io.camunda.common.auth.Authentication;
import com.example.tasklist.api.TaskApi;
import io.camunda.zeebe.client.ZeebeClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class UserTasksManagementService {
private static final Logger LOG = LoggerFactory.getLogger(UserTasksManagementService.class);
@Autowired private ZeebeClient zeebe;
@Autowired private TaskApi taskApi;
@Autowired private Authentication authentication;
@EventListener(ApplicationReadyEvent.class)
public void manageUserTasks() {
// your implementation here
}
}
Authentication
is an interface that provides abstraction over the authentication mechanism, adapting to different client modes. For instance, for local dev mode, it employs cookie-based authentication, whereas in OIDC or SaaS environments, it utilizes JWT token-based authentication. Let's create a method that fetches the token header for Tasklist.
private Map<String, String> getTasklistAuthTokenHeader() {
return Map.ofEntries(authentication.getTokenHeader(Product.TASKLIST));
}
In Camunda 8, data is asynchronously exported from Zeebe to Tasklist. To ensure the data is available in Tasklist after submitting Zeebe commands, we've incorporated an active polling helper method waitFor
, utilizing the Predicate
class from java.util.function
to validate the successful return of results:
private <T> T waitFor(Callable<T> responseSupplier, Predicate<T> responseTester)
throws Exception {
int maxRounds = 10;
int waitRound = 0;
int waitTime = 1000;
while (waitRound < maxRounds) {
T response = responseSupplier.call();
if (responseTester.test(response)) {
return response;
}
Thread.sleep(waitTime);
waitRound++;
}
throw new RuntimeException(
String.format("Test is not successful after %s attempts", waitRound));
}
We've applied the @EventListener(ApplicationReadyEvent.class)
annotation to the manageUserTasks()
method, ensuring its execution upon application startup and ready. Alternatively, you could trigger this method within a REST controller, invoked by a corresponding REST API endpoint.
Here is how the scenario implementation looks:
@EventListener(ApplicationReadyEvent.class)
public void manageUserTasks() throws Exception {
// 1. Start a process instance
long processInstanceKey =
zeebe
.newCreateInstanceCommand()
.bpmnProcessId("api-test-process")
.latestVersion()
.send()
.get()
.getProcessInstanceKey();
LOG.info("Started a 'api-test-process' process instance with key={}", processInstanceKey);
// 2. Search for user tasks (filter by processInstanceKey, implementation=ZEEBE_USER_TASK,
// state=CREATED and sort DESC by creationTime)
List<TaskSearchResponse> tasks =
waitFor(
() ->
taskApi.searchTasks(
new TaskSearchRequest()
.state(TaskSearchRequest.StateEnum.CREATED)
.implementation(TaskSearchRequest.ImplementationEnum.ZEEBE_USER_TASK)
.processInstanceKey(String.valueOf(processInstanceKey))
.sort(
List.of(
new TaskOrderBy()
.field(TaskOrderBy.FieldEnum.CREATIONTIME)
.order(TaskOrderBy.OrderEnum.DESC))),
getTasklistAuthTokenHeader()),
taskSearchResponses -> taskSearchResponses.size() > 0);
LOG.info("Found {} task(s) in Tasklist", tasks.size());
// 3. Pick the last created task
String taskId = tasks.get(0).getId();
LOG.info("The chosen task id={}", taskId);
// 4. Assign task to "demo" user
zeebe.newUserTaskAssignCommand(Long.valueOf(taskId)).assignee("demo").send().get();
// 5. Get the task by id
TaskResponse taskResponse =
waitFor(
() -> taskApi.getTaskById(taskId, getTasklistAuthTokenHeader()),
response -> "demo".equals(response.getAssignee()));
LOG.info("Task {} is assigned to {}", taskId, taskResponse.getAssignee());
// 6. Complete the task
zeebe.newUserTaskCompleteCommand(Long.valueOf(taskId)).send().get();
// 7. Get the task and check the state
taskResponse =
waitFor(
() -> taskApi.getTaskById(taskId, getTasklistAuthTokenHeader()),
response -> response.getTaskState() == TaskResponse.TaskStateEnum.COMPLETED);
LOG.info("Task {} has {} state", taskId, taskResponse.getTaskState());
}
To execute this, run the application:
./mvnw spring-boot:run
The application outputs logs similar to the following:
INFO com.example.demo.UserTasksManagementService : Started api-test-process instance with key=4503599627491527
INFO com.example.demo.UserTasksManagementService : Found 1 task(s) in Tasklist
INFO com.example.demo.UserTasksManagementService : The chosen task id=2251799813813402
INFO com.example.demo.UserTasksManagementService : Task 2251799813813402 is assigned to demo
INFO com.example.demo.UserTasksManagementService : Task 2251799813813402 has COMPLETED state
Next steps
Congratulations! You have successfully set up the basic structure for a custom task application based on Camunda APIs. These are your next steps:
- Familiarize yourself with the Tasklist and Zeebe APIs
- Learn how to build a custom task application frontend
- Understand Camunda Forms and how to leverage them to build interfaces for user tasks