flowable/flowable-engine · error · ActivitiException

UserTask should not be signalled before complete

Error message

UserTask should not be signalled before complete

What it means

Thrown in UserTaskActivityBehavior.signal() when a signal is delivered to an execution that still has active tasks. A user task must be completed (via TaskService.completeTask), which removes the task and leaves the activity; signalling the activity directly before completion is an invalid state transition and always a bug in calling code.

Solutions

  1. Complete the user task via TaskService.complete(taskId) instead of triggering/signalling the execution
  2. Before triggering, check the current activity type and route user tasks through the task-completion path
  3. Query the pending task (TaskService.createTaskQuery().executionId(executionId)) and complete it programmatically
  4. Remove erroneous signal/trigger calls on user-task executions in custom process-interaction code

Example fix

// before
runtimeService.trigger(executionId);
// after
Task task = taskService.createTaskQuery().executionId(executionId).singleResult();
taskService.complete(task.getId());
Defensive patterns

Strategy: validation

Validate before calling

Task pending = taskService.createTaskQuery().executionId(executionId).singleResult();
if (pending != null) {
    taskService.complete(pending.getId());
} else {
    runtimeService.trigger(executionId);
}

Try / catch

try {
    runtimeService.trigger(executionId);
} catch (ActivitiException e) {
    if ("UserTask should not be signalled before complete".equals(e.getMessage())) {
        // fall back to completing the pending task
    }
}

Prevention

When it happens

Trigger: Calling execution.signal(...) / RuntimeService.trigger(executionId) on the execution of an active user task whose tasks list is non-empty, instead of completing the task first.

Common situations: Custom code that drives the process with signal/trigger instead of TaskService; generic activity-completion wrappers that trigger executions without checking the current activity type; migration of code written for receive tasks to user tasks.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/a5e31f21bc591260. Report an issue: GitHub.

Appendix: source

Thrown at modules/flowable5-engine/src/main/java/org/activiti/engine/impl/bpmn/behavior/UserTaskActivityBehavior.java:234

            task.fireEvent(TaskListener.EVENTNAME_CREATE);

            // All properties set, now firing 'create' events
            if (Context.getProcessEngineConfiguration().getEventDispatcher().isEnabled()) {
                Context.getProcessEngineConfiguration().getEventDispatcher().dispatchEvent(
                        ActivitiEventBuilder.createEntityEvent(FlowableEngineEventType.TASK_CREATED, task),
                        EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
            }
        }

        if (skipUserTask) {
            task.complete(null, false, false);
        }
    }

    @Override
    public void signal(ActivityExecution execution, String signalName, Object signalData) throws Exception {
        if (!((ExecutionEntity) execution).getTasks().isEmpty())
            throw new ActivitiException("UserTask should not be signalled before complete");
        leave(execution);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    protected void handleAssignments(Expression assigneeExpression, Expression ownerExpression, Set<Expression> candidateUserExpressions,
                                     Set<Expression> candidateGroupExpressions, TaskEntity task, ActivityExecution execution) {

        if (assigneeExpression != null) {
            Object assigneeExpressionValue = assigneeExpression.getValue(execution);
            String assigneeValue = null;
            if (assigneeExpressionValue != null) {
                assigneeValue = assigneeExpressionValue.toString();
            }
            task.setAssignee(assigneeValue, true, false);
        }

        if (ownerExpression != null) {
            Object ownerExpressionValue = ownerExpression.getValue(execution);

View on GitHub (pinned to d6d39ce1c6)