This tutorial introduces you to Activiti BPMN and how to run it in a Spring Boot environment. We will demonstrate how to run a simple Process which includes user Tasks in it from a Spring Boot application..
Activiti is an open-source workflow engine written in Java that you can use to execute BPMN 2.0 compliant business processes. In this article we will mainly cover how run Activiti in a Spring Boot runtime.
Setting up the Project
Firstly, we will set up the project with Spring Boot initializr. In our example, we will use Spring Boot 2.6.11 API. Here is the list of dependencies we will need:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.11</version> <relativePath/> </parent> <groupId>org.activiti</groupId> <artifactId>activiti-example</artifactId> <version>7.4.1</version> <name>Activiti :: Examples :: API Basic Process and Tasks Example No Bean</name> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter</artifactId> <version>7.4.1</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>activiti-releases</id> <url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases</url> </repository> </repositories> </project>
As you can see, we are using Activiti version 7.4.1 through the activiti-spring-boot-starter. We also need to include in the repositories section the URL where Activiti releases are available.
In the next section, we will learn how to design the BPMN process using an online Process designer.
Designing the BPMN Process
In order to design the BPMN 2.0 process there are multiple options. You can try the an online Process designer http://demo.bpmn.io/
On the other hand, if you want to learn how to install a Business Process Designer as Eclipse plugin, then check this Activiti tutorial.
In our sample process, there’s an Human Task “Process Content” where the Task owner will choose if a Content is appropriate or not:

The decision will be taken from our example application as soon as the Task owner claims the Task.
Coding the Spring Boot application
The application logic is contained in the following @SpringBootApplication Class:
@SpringBootApplication @EnableScheduling public class DemoApplication implements CommandLineRunner { private Logger logger = LoggerFactory.getLogger(DemoApplication.class); private final ProcessRuntime processRuntime; private final TaskRuntime taskRuntime; private final SecurityUtil securityUtil; private final ObjectMapper objectMapper; public DemoApplication(ProcessRuntime processRuntime, TaskRuntime taskRuntime, SecurityUtil securityUtil, ObjectMapper objectMapper) { this.processRuntime = processRuntime; this.taskRuntime = taskRuntime; this.securityUtil = securityUtil; this.objectMapper = objectMapper; } public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Override public void run(String... args) { securityUtil.logInAs("system"); Page<ProcessDefinition> processDefinitionPage = processRuntime.processDefinitions(Pageable.of(0, 10)); logger.info("> Available Process definitions: " + processDefinitionPage.getTotalItems()); for (ProcessDefinition pd : processDefinitionPage.getContent()) { logger.info("\t > Process definition: " + pd); } } @Scheduled(initialDelay = 1000, fixedDelay = 5000) public void processText() { securityUtil.logInAs("system"); LinkedHashMap content = pickRandomString(); SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yy HH:mm:ss"); logger.info("> Starting process to process content: " + content + " at " + formatter.format(new Date())); ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder .start() .withProcessDefinitionKey("categorizeHumanProcess") .withName("Processing Content: " + content) .withVariable("content", objectMapper.convertValue(content, JsonNode.class)) .build()); logger.info(">>> Created Process Instance: " + processInstance); } @Scheduled(initialDelay = 1000, fixedDelay = 5000) public void checkAndWorkOnTasksWhenAvailable() { securityUtil.logInAs("bob"); Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 10)); if (tasks.getTotalItems() > 0) { for (Task t : tasks.getContent()) { logger.info("> Claiming task: " + t.getId()); taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(t.getId()).build()); List<VariableInstance> variables = taskRuntime.variables(TaskPayloadBuilder.variables().withTaskId(t.getId()).build()); VariableInstance variableInstance = variables.get(0); if (variableInstance.getName().equals("content")) { LinkedHashMap contentToProcess = objectMapper.convertValue(variableInstance.getValue(), LinkedHashMap.class); logger.info("> Content received inside the task to approve: " + contentToProcess); if (contentToProcess.get("body").toString().contains("activiti")) { logger.info("> User Approving content"); contentToProcess.put("approved",true); } else { logger.info("> User Discarding content"); contentToProcess.put("approved",false); } taskRuntime.complete(TaskPayloadBuilder.complete() .withTaskId(t.getId()).withVariable("content", contentToProcess).build()); } } } else { logger.info("> There are no task for me to work on."); } } @Bean public Connector tagTextConnector() { return integrationContext -> { LinkedHashMap contentToTag = (LinkedHashMap) integrationContext.getInBoundVariables().get("content"); contentToTag.put("tags", singletonList(" :) ")); integrationContext.addOutBoundVariable("content", contentToTag); logger.info("Final Content: " + contentToTag); return integrationContext; }; } @Bean public Connector discardTextConnector() { return integrationContext -> { LinkedHashMap contentToDiscard = (LinkedHashMap) integrationContext.getInBoundVariables().get("content"); contentToDiscard.put("tags", singletonList(" :( ")); integrationContext.addOutBoundVariable("content", contentToDiscard); logger.info("Final Content: " + contentToDiscard); return integrationContext; }; } private LinkedHashMap pickRandomString() { String[] texts = {"hello from london", "Hi there from activiti!", "all good news over here.", "I've tweeted about activiti today.", "other boring projects.", "activiti cloud - Cloud Native Java BPM"}; LinkedHashMap<Object,Object> content = new LinkedHashMap<>(); content.put("body",texts[new Random().nextInt(texts.length)]); return content; } }
As you can see, within this application there are two @Scheduled activities:
The method processText will create a new Process Instance with some random content for the process variable “content”
The method checkAndWorkOnTasksWhenAvailable will claim the active Tasks and complete them, based on the value of the “content” process variable.
We also need a Configuration Bean which contains the list of Users and Roles you can use to access the Spring Boot application:
@Configuration public class DemoApplicationConfiguration { private Logger logger = LoggerFactory.getLogger(DemoApplicationConfiguration.class); @Bean public UserDetailsService myUserDetailsService() { InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(); String[][] usersGroupsAndRoles = { {"bob", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"}, {"john", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"}, {"hannah", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"}, {"other", "password", "ROLE_ACTIVITI_USER", "GROUP_otherTeam"}, {"system", "password", "ROLE_ACTIVITI_USER"}, {"admin", "password", "ROLE_ACTIVITI_ADMIN"}, }; for (String[] user : usersGroupsAndRoles) { List<String> authoritiesStrings = asList(Arrays.copyOfRange(user, 2, user.length)); logger.info("> Registering new user: " + user[0] + " with the following Authorities[" + authoritiesStrings + "]"); inMemoryUserDetailsManager.createUser(new User(user[0], passwordEncoder().encode(user[1]), authoritiesStrings.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList()))); } return inMemoryUserDetailsManager; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
Finally, to set the SecurityContext in the Activiti Engine, we will add a Component Class, which contains a SecurityContextImpl:
@Component public class SecurityUtil { private Logger logger = LoggerFactory.getLogger(SecurityUtil.class); @Autowired private UserDetailsService userDetailsService; public void logInAs(String username) { UserDetails user = userDetailsService.loadUserByUsername(username); if (user == null) { throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user"); } logger.info("> Logged in as: " + username); SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() { @Override public Collection<? extends GrantedAuthority> getAuthorities() { return user.getAuthorities(); } @Override public Object getCredentials() { return user.getPassword(); } @Override public Object getDetails() { return user; } @Override public Object getPrincipal() { return user; } @Override public boolean isAuthenticated() { return true; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { } @Override public String getName() { return user.getUsername(); } })); org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username); } }
You can run the application as follows:
mvn install spring-boot:run
You will observe that every 5 seconds a new Process Instance starts and available Tasks are claimed accordingly:

The Spring Boot project is available under the Activiti examples: https://github.com/Activiti/Activiti/tree/develop/activiti-examples/activiti-api-basic-full-example-nobean