Default flow in exclusive gateway

What happens if the default flow is above the others in the XML? In my case, process sometimes goes the wrong way

Is this a bug or bad practice?

Hi and welcome to the forum,

This should have no effect on the process execution. The default flow is only chosen if no other flow matches. In the following example, the default flow (upper one) is selected if the value is greater than 20:
image

For non-default flows, you should avoid non-exclusive conditions: The conditions will be evaluated in order. The first condition that matches will be selected. Since the order is not visually represented, this can lead to confusion.

1 Like

Sometimes it happens that the process still follows the default flow, although there is a suitable condition. I found this very suspicious.

Can you provide me an example model and a case for which you observed this behavior?
It does sound suspicious, and I’d like to reproduce it.
The bug report that you linked is concerned with Camunda 8 and not with Camunda 7.

This process is called from another process, and first it calls it so that the variable in the gateway will not exist, another call will already be with the variable, but in 1 out of 40 cases the process will go on the wrong branch

Thank you! :slight_smile:
Can you also provide the BPMN file?

Just for clarification:

  1. When you call the process without setting the variable and everything works fine
  2. When you set the variable, the unexpected behavior occurs in 1 out of 40 cases

Did I get that right?

Unfortunately, i will not be possible to provide BPMN, my example only visually shows how it looks.
1 and 2 - yes)

Hi,
I tried to reproduce your error but failed.
Therefore, I created this process model testConditions.bpmn (6.2 KB)

It has three flows: a default flow, a flow expecting variable “task==B”, and a flow expecting “task==C”.
I checked the XML and the default flow is the first one:

    <bpmn:exclusiveGateway id="Gateway_1ti8fvy" name="which task?" default="Flow_1e8oafd">
      <bpmn:incoming>Flow_16r1ev6</bpmn:incoming>
      <bpmn:outgoing>Flow_1e8oafd</bpmn:outgoing>
      <bpmn:outgoing>Flow_06htbde</bpmn:outgoing>
      <bpmn:outgoing>Flow_0d1r8mu</bpmn:outgoing>
    </bpmn:exclusiveGateway>

Furthermore, I created test cases using the Camunda 7.17 (Spring boot), Camunda BPM assert, and Camunda BPM Junit 5:


import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.ProcessEngineException;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.test.Deployment;
import org.camunda.bpm.engine.test.junit5.ProcessEngineExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.Map;
import java.util.stream.IntStream;

import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.assertThat;
import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.complete;
import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.task;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

@ExtendWith(ProcessEngineExtension.class)
@Deployment(resources = {"testConditions.bpmn"})
public class ProcessTest {
  ProcessEngine processEngine;
  private static final int NUMBER_OF_TEST_INSTANCES = 100;

  @Test
  public void defaultFlowIfVariableTaskIsA() {
    RuntimeService runtimeService = processEngine.getRuntimeService();
    ProcessInstance pi = runtimeService.startProcessInstanceByKey("testDefaultFlow", Map.of("task", "A"));
    assertThat(pi).isWaitingAt("Activity_A");
    complete(task());
    assertThat(pi)
        .isEnded()
        .hasNotPassed("Activity_B")
        .hasNotPassed("activity_C");
  }

  @Test
  public void batchTestDefaultFlowIfVariableTaskIsA() {
    IntStream.range(0, NUMBER_OF_TEST_INSTANCES)
        .forEach(i -> defaultFlowIfVariableTaskIsA());
  }

  @Test
  public void testExpectExceptionIfVariableTaskIsNotSet() {
    RuntimeService runtimeService = processEngine.getRuntimeService();
    ProcessEngineException exception = assertThrows(
        ProcessEngineException.class,
        ()-> runtimeService.startProcessInstanceByKey("testDefaultFlow"));
    assertTrue(exception.getMessage().contains("Unknown property used in expression"));
    assertTrue(exception.getMessage().contains("Cannot resolve identifier 'task'"));
  }

  @Test
  public void testExpectExceptionIfVariableTaskIsSetToNull() {
    RuntimeService runtimeService = processEngine.getRuntimeService();
    NullPointerException exception = assertThrows(
        NullPointerException.class,
        ()-> runtimeService.startProcessInstanceByKey("testDefaultFlow", Map.of("task", null)));
  }

}

All test pass:

  1. If I set the variable task to something different from B or C, only task A is enabled (default flow).
  2. This happens in hundreds of runs
  3. If I do not set the variable, I get a ProcessEngineException
  4. If I attempt to set the variable to null, I get a NullPointerException

Can you help me to understand how my setup deviates from your case?

1 Like

Hi, I’m sorry I didn’t say one more thing:
the process starts with “Message start event”

RuntimeService.setVariable is used first and then RuntimeService.messageEventReceived

An example process that calls testCondition.bpmn
diagram_1.bpmn (5.5 KB)

In the process itself, the variable looks like B or C, but sometimes it goes along the branch with the default value

Given your example, I updated the tests:

I added a process model with an event subprocess. When the event subprocess is triggered, the problematic process is called and the variable “task” is passed to said process.
callingProcess.bpmn (5.0 KB)

I added a test which runs the process a hundred times:

  @Test
  public void callingProcessSetsVariableToB() {
    RuntimeService runtimeService = processEngine.getRuntimeService();
    ProcessInstance pi = runtimeService.startProcessInstanceByKey("callingProcess", Map.of("task", "B"));
    assertThat(pi).isWaitingAt("Activity_justWait");
    for (int i = 0; i < NUMBER_OF_TEST_INSTANCES; i ++) {
      runtimeService.correlateMessage("msg");
      ProcessInstance pi2 = runtimeService.createProcessInstanceQuery().processDefinitionKey("testDefaultFlow").singleResult();
      assertThat(pi2).isActive();
      assertThat(pi2).isWaitingAt("Activity_B");
      complete(task("Activity_B"));
      assertThat(pi2)
          .isEnded()
          .hasNotPassed("Activity_A")
          .hasNotPassed("activity_C");
    }
  }

I was unable to replicate the problem that you explain.
Maybe you can

  1. double-check your variables and bindings (i.e., how variables are passed from the event sub process to the called process)
  2. check the implementation of the service tasks (maybe the variable is overwritten, leading to the unexpected behavior?)
  3. add some extensive logging to understand the behavior better

Hi, there is one more thing

It seems not related to the default flow, the variable is not overwritten in the database, and this happens very rarely. As I wrote before, basically this case works fine.

RuntimeService.setVariable was called, but the variable was not committed and there is only the old one in the HI_VARINST

What could be the problem or is it not recommended to overwrite?

Interesting. Can you please provide me some more context: Where do you call RuntimeService.setVariable?

Generally, overwriting variables is fine. However, this is usually done through messages and tasks, i.e., it is directly reflected in the process model. In these cases, you would not use the runtime service.
If you use a java delegate, you use delegateExecution.setVarieable(). If you use an External Task Client, you would use externalTaskService.complete(task, mapOfVariables)

Just like in the example I posted above.

The nested process starts the event using rest, first the runtimeService.setVariable(String executionId, String variableName, Object value) sets the variables, and then the runtimeService.messageEventReceived(String messageName, String executionId). Then the subprocess is called

And this problem really occurs very rarely and the new variable does not exist in the database (

In my case, it’s better to use externalTaskService.complete(task, mapOfVariables) for the variable, and start the event via runtimeService.messageEventReceived?

1 - runtimeService.setVariable(String executionId, String variableName, Object value)
2 - runtimeService.messageEventReceived(String messageName, String executionId)

camunda - 7.13.0

In BPMN, messages are used to communicate between different processes. So, assuming that Any logic 2 calls the event subprocess by sending a message, I’d remodel the process as follows (depending on whether the call activity must be executed once or multiple times):

If I misunderstood you, and you have another process that sends the message, I recommend using the correlateMessage command instead:

runtimeService.correlateMessage(msgName, correlationKey, mapOfVariables);

This single call triggers the process and sets the variables. If you separate the calls, the process may start before the variables have been updated, and runs into the described issues.