Business Events
Community Edition since 1.4.0
Business Events were introduced in EximeeBPMS 1.2.16-ee (Enterprise Edition), with the set of covered entities substantially expanded — and the default event type prefix changed — in 1.3.1-ee. Starting with EximeeBPMS 1.4.0, the same native business events with transactional outbox mechanism, covering the full set of entities documented below, ships in the Community Edition as well — see the notes on the prefix configuration property below if you’re upgrading a pre-1.3.1-ee Enterprise Edition environment.
Business Events let the process engine publish a stream of domain-level occurrences — a task was completed, a variable changed, a process instance ended — to systems outside the engine, without coupling the engine’s own transaction to the availability of those systems.
For the exact fields carried by each event type’s payload, see Business Event Field Reference.
How It Works
Business Events use the transactional outbox pattern:
- When an event-worthy change happens (e.g., a task is completed), the engine writes a row describing it to the
ACT_RU_BUS_EVT_OBXtable, in the same database transaction as the change itself. - A background dispatcher periodically reads batches of undelivered rows from the outbox and hands them to the configured publisher.
- Once a publisher confirms delivery, the corresponding outbox rows are marked delivered; a separate cleanup job removes delivered rows past their retention period.
Because the outbox write is part of the same transaction as the business change, an event is never recorded for a change that didn’t commit, and a committed change never silently fails to produce its event — delivery to the publisher is a separate, retried concern. This gives at-least-once delivery to downstream systems: consumers should treat delivery as idempotent (the metadata.uuid field described below can be used for deduplication).
When Publishing Fails
Rows are handed to the publisher in the order they were written. If publishing a row fails, the dispatcher stops the cycle there. The row stays undelivered and is retried first on the next cycle. The rows behind it wait, so they are never delivered out of order. A failure is either an exception thrown by the publisher or a failed BusinessEventPublishResult it returns. During an outage of the receiving system, the outbox therefore grows until the system is back, and then drains in order. Each cycle that stops on a row logs ENGINE-00019, with the row id and how long it has been waiting.
A row the receiving system can never accept therefore holds back every business event behind it, across all processes, until it goes through. This is deliberate. Skipping it automatically would leave consumers with a gap they cannot see, such as a process-instance end for an instance whose start never arrived. Alert on ENGINE-00019, or on the eximeebpms.business.events.outbox.pending.age.oldest.seconds gauge of the monitoring extension. Fix the cause, and the outbox drains in order. Skipping a single row is an operator decision, taken knowing that its consumers will miss that event:
UPDATE ACT_RU_BUS_EVT_OBX SET PROCESSED_ = true, PROCESSED_DATE_ = CURRENT_TIMESTAMP WHERE ID_ = <row id from ENGINE-00019>;
PROCESSED_ is a boolean on PostgreSQL and H2. On Oracle, SQL Server, MySQL/MariaDB and DB2 it is numeric, so use 1 instead of true there.
Enterprise Edition only
Retrying a failed BusinessEventPublishResult is Enterprise Edition behavior. Community Edition 1.4.0 retries only a publisher that throws. A failure the publisher returns is marked delivered, and that event is not retried. This includes a Kafka broker outage, which the shipped kafka publisher reports this way.
Event Order
The dispatcher reads the outbox in the order rows were written, so a publisher receives the business events of a change in the order the engine recorded them. From 1.4.1-ee onward, that order matches the order in which the engine records the same occurrences in history. In particular:
process-instance:startis always the first event of a process instance. It comes before thevariable-instance:createevents for the variables passed at start, theform-property:form-property-updateevents of a submitted start form, thejob:createevents of process-level timers, and anything a process-levelstartexecution listener does. This also holds when the start event isasyncBefore: the start event is written in the transaction that starts the instance, not when the asynchronous continuation runs.task-instance:completeandtask-instance:deletecome after anything the task’s owncomplete/deletetask listeners do (for example, setting variables). They also come after theidentity-linkandvariable-instance:deleteevents for the task’s own identity links and local variables.- Changing a task’s assignee or owner publishes
identity-link-add:add-identity-link/identity-link-delete:delete-identity-linkwithtypeassignee/owner, just as candidate users and groups do. - Standalone tasks (created with
TaskService#newTask, not part of a process) publish the sametask-instance:*lifecycle as user tasks, withmetadata.noProcessContextset totrue.
Before 1.4.1-ee
Up to and including 1.4.0 and 1.3.3-ee, process-instance:start was written after the start variables, start-form properties and process-level timers. With an asyncBefore start event, it was only written once the asynchronous continuation ran. task-instance:complete/task-instance:delete were written before the task’s complete/delete listeners ran. process-instance-update:update was listed below but never actually published. Assignee/owner changes published no identity-link events (only candidate links did), and standalone tasks published no task-instance:* events. A consumer that creates its own record on process-instance:start and discards events for instances it doesn’t know yet lost the start variables on those releases.
A few events deliberately differ from history:
job:failis published once, when a job has exhausted its retries. History records every failed attempt.task-instance:updateis published for every task update as it happens. History records one combined update at the end of the command.- Ending a task publishes
identity-link-delete:delete-identity-linkfor each of its identity links. History keeps them.
This order holds within one engine transaction and between transactions that follow one another, such as a process start and a later task completion. Events of different transactions that run concurrently are not ordered relative to each other.
Event Envelope
What a publisher (e.g. the kafka publisher, or a custom BusinessEventPublisher) actually receives is an Event envelope wrapping the business event, not the business event object directly:
{
"metadata": {
"timestamp": "2026-07-29T10:15:23.456+00:00",
"uuid": "1c1a9e2e-2a34-4b7d-9f0a-6e3d3a2b9c11",
"type": "bpms:task-instance:complete",
"version": "1.0",
"origin": "bpms",
"correlationId": null,
"processInstanceId": "3f2c...",
"processDefinitionKey": "invoice-approval",
"noProcessContext": false
},
"payload": "{\"id\":\"...\",\"processInstanceId\":\"3f2c...\", ... }"
}
| Field | Type | Description |
|---|---|---|
metadata.timestamp | Instant | When the outbox row was written (i.e. when the underlying business change committed), not when it was dispatched. |
metadata.uuid | String | A fresh random UUID generated at dispatch time. Not the same as the business event's own id field — use this for de-duplicating retried deliveries of the same dispatch attempt. |
metadata.type | String | The fully-qualified business event type, in the form <prefix>:<entity>:<event> — see Business Event Types below. Mirrors the businessEventType field inside payload. |
metadata.version | String | Envelope schema version. Currently always "1.0". |
metadata.origin | String | Always the literal "bpms". Unlike metadata.type, this is not affected by the configured prefix — don't use it to distinguish between engines that are configured with different prefixes. |
metadata.correlationId | String | Reserved for future use. Currently always null. |
metadata.processInstanceId | String | Root process instance id if the event has a process context; otherwise the literal "no-process-context" (see noProcessContext). |
metadata.processDefinitionKey | String | Process definition key if the event has a process context; otherwise "no-process-context". |
metadata.noProcessContext | boolean | true when the underlying change has no associated process instance (e.g. a job or user operation log entry that isn't tied to a running instance). |
payload | String | The business event itself, serialized to a JSON string (not a nested JSON object) using Gson. Its own field names match the Java entity's field names exactly — one-to-one, no getter/property renaming. Deserialize this string separately to access the fields documented in Business Event Field Reference. |
Business Event Types
Each business event carries its fully-qualified type in the businessEventType field of its payload (and in metadata.type of the envelope), in the form <prefix>:<entity>:<event>. The default prefix is bpms and is configurable; examples below use the default.
For the exact payload fields behind each row, follow the links to the Business Event Field Reference.
Process & Task Lifecycle
| Fired when | Event type |
|---|---|
| Process instance started | bpms:process-instance:start |
| Process instance updated | bpms:process-instance-update:update |
| Process instance ended | bpms:process-instance:end |
| Process instance migrated to another process definition version | bpms:process-instance:migrate |
| Activity instance started | bpms:activity-instance:start |
| Activity instance updated | bpms:activity-instance:update |
| Activity instance migrated | bpms:activity-instance:migrate |
| Activity instance ended | bpms:activity-instance:end |
| Task instance created | bpms:task-instance:create |
| Task instance updated | bpms:task-instance:update |
| Task instance migrated | bpms:task-instance:migrate |
| Task instance completed | bpms:task-instance:complete |
| Task instance deleted | bpms:task-instance:delete |
Variables & Identity Links
| Fired when | Event type |
|---|---|
| Variable created | bpms:variable-instance:create |
| Variable updated | bpms:variable-instance:update |
| Variable migrated | bpms:variable-instance:migrate |
| Variable deleted | bpms:variable-instance:delete |
| Identity link added (candidate/assignee/owner) | bpms:identity-link-add:add-identity-link |
| Identity link deleted | bpms:identity-link-delete:delete-identity-link |
Upgrading from 1.3.1-ee
In 1.3.1-ee, Variable business events used inconsistent past-tense type names — variable-instance:created, variable-instance:updated, variable-instance:deleted — instead of the imperative-style names every other entity uses. As of 1.3.2-ee, these are corrected to create/update/delete as shown above (migrate was already correctly named). If you built downstream consumers against the 1.3.1-ee strings, update them — the old strings are no longer published.
Jobs, Batches & External Tasks
| Fired when | Event type |
|---|---|
| Job created | bpms:job:create |
| Job execution failed | bpms:job:fail |
| Job executed successfully | bpms:job:success |
| Job deleted | bpms:job:delete |
| Batch started | bpms:batch:start |
| Batch execution progress updated | bpms:batch:update |
| Batch ended | bpms:batch:end |
| External task created | bpms:external-task:create |
| External task execution failed | bpms:external-task:fail |
| External task executed successfully | bpms:external-task:success |
| External task deleted | bpms:external-task:delete |
Incidents & Decisions
| Fired when | Event type |
|---|---|
| Incident created | bpms:incident:create |
| Incident migrated | bpms:incident:migrate |
| Incident resolved | bpms:incident:resolve |
| Incident updated | bpms:incident:update |
| Incident deleted | bpms:incident:delete |
| DMN decision evaluated | bpms:decision:evaluate |
Forms & Audit Trail
| Fired when | Event type |
|---|---|
| Form property submitted/updated via a task or start form | bpms:form-property:form-property-update |
| User operation log entry created (one event per changed property) | bpms:user-operation-log:create |
Script violation detected — Script Guard records a violation (AUDIT or ENFORCE mode) | bpms:script-violation:create |
Configuration
Business Events are configured under the eximeebpms.bpm.business-events prefix and are disabled by default:
eximeebpms:
bpm:
business-events:
enabled: true
publisher: kafka
prefix: bpms
enabled-event-types: "*"
disabled-event-types: []
dispatch-interval-ms: 5000
dispatcher-batch-size: 100
outbox-retention-ms: 604800000 # 7 days
outbox-cleanup-interval-ms: 3600000 # 1 hour
publisher-properties:
kafka.bootstrap-servers: "kafka-1:9092,kafka-2:9092"
kafka.topic: "eximeebpms.business-events"
| Property | Default | Description |
|---|---|---|
enabled | false | Master switch for the whole feature. When disabled, no outbox rows are written and the dispatcher does not run. |
publisher | noop | Symbolic name of the publisher to dispatch events to. |
prefix | bpms | Prefix prepended to every business event's fully-qualified type, i.e. the <prefix> in <prefix>:<entity>:<event>. Added in 1.3.1-ee. Does not affect the envelope's metadata.origin field, which is always "bpms" — see Event Envelope. |
enabled-event-types | * | Allowlist of published event types — see Limiting Published Event Types. Added in 1.4.1-ee, Enterprise Edition only. |
disabled-event-types | empty | Denylist of published event types, applied after enabled-event-types and taking precedence over it. Added in 1.4.1-ee, Enterprise Edition only. |
dispatch-interval-ms | 5000 | How often the dispatcher polls the outbox for undelivered events. |
dispatcher-batch-size | 100 | Maximum number of outbox rows read and handed to the publisher per dispatch cycle. |
outbox-retention-ms | 604800000 (7 days) | How long delivered outbox rows are kept before cleanup removes them. |
outbox-cleanup-interval-ms | 3600000 (1 hour) | How often the cleanup job runs. |
publisher-properties | empty | Publisher-specific properties (see below), passed through to BusinessEventPublisher.init(Map). |
Upgrading from a release before 1.3.1-ee
Releases before 1.3.1-ee published events with the hardcoded prefix camunda7 (e.g. camunda7:task-instance:complete). Starting with 1.3.1-ee, the default prefix is bpms. If downstream consumers (SIEM rules, stream processors, dashboards) match on the literal type string, either update them to the bpms: prefix or set prefix: camunda7 explicitly to preserve the previous behavior during migration.
The four tuning properties below were renamed in the same release, dropping their business-event- segment: business-event-dispatch-interval-ms → dispatch-interval-ms, business-event-dispatcher-batch-size → dispatcher-batch-size, business-event-outbox-retention-ms → outbox-retention-ms and business-event-outbox-cleanup-interval-ms → outbox-cleanup-interval-ms. The old names bind to nothing and are ignored without any startup warning, so an upgraded engine silently falls back to the defaults. Check your configuration if you tuned any of them.
Limiting Published Event Types
Enterprise Edition only
Unlike the rest of this page, the enabled-event-types/disabled-event-types filter described in
this section is an Enterprise Edition feature, available from 1.4.1-ee onward. Community Edition
1.4.0 has no event-type filter: with Business Events enabled, every event type listed above is
published, and neither property exists — the engine plugin’s enabledEventTypes, which never had
any effect, was removed in 1.4.0.
enabled is a master switch: it turns the whole mechanism on or off. To publish only some of the
event types, narrow the selection with enabled-event-types and
disabled-event-types.
Both take a list of tokens naming event types by their <entity>:<event> pair — the same pair that
forms the fully-qualified <prefix>:<entity>:<event> type:
| Token | Matches |
|---|---|
* | every event type |
<entity>:*, or just <entity> | every event on that entity, e.g. variable-instance:* |
<entity>:<event> | a single type, e.g. task-instance:complete |
<prefix>:<entity>:<event> | the same, written the way the tables above and metadata.type spell it; the prefix must be the configured one |
An event is published when enabled-event-types matches it and disabled-event-types does not —
the denylist always wins. Tokens are matched case-insensitively.
Publish everything except the two highest-volume entities:
eximeebpms:
bpm:
business-events:
enabled: true
disabled-event-types:
- variable-instance:*
- activity-instance:*
Publish only the task lifecycle, minus assignment/update changes:
eximeebpms:
bpm:
business-events:
enabled: true
enabled-event-types:
- task-instance:*
disabled-event-types:
- task-instance:update
Outside Spring Boot, the same two properties are set on the engine plugin, comma-separated —
in standalone bpm-platform.xml and, with the identical syntax, in the WildFly subsystem:
<process-engine name="default">
...
<plugins>
<plugin>
<class>org.eximeebpms.bpm.engine.impl.businessevent.BusinessEventConfigurationPlugin</class>
<properties>
<property name="enabled">true</property>
<property name="publisher">kafka</property>
<property name="disabledEventTypes">variable-instance:*,activity-instance:*</property>
</properties>
</plugin>
</plugins>
</process-engine>
A token that names nothing fails startup
A token naming an unknown entity, or an unknown event on a known entity, aborts engine bootstrap with
InvalidBusinessEventTypeException rather than silently matching nothing. This is checked even while
enabled is still false, so a typo cannot lie in wait until the feature is switched on. When a
filter is in effect, the engine logs the resulting set of published types at startup.
Two things worth knowing before narrowing the list:
- Consumers see gaps, not reordering. Disabling
process-instance:startwhile leavingprocess-instance:endenabled means downstream systems receive anendevent for an instance they never saw start. Delivery order among the types that are published is unaffected. process-instance:*does not cover the update event. Its entity isprocess-instance-update, notprocess-instance(see the type table above), so it needs its own token:process-instance-update:update.
The filter is resolved once, when the engine starts; changing it requires a restart. It has no effect on outbox rows already written — those are dispatched as normal.
Built-in Publishers
noop (default)
Writes to the outbox but never dispatches anywhere. Useful for exercising the outbox and cleanup mechanics without wiring an external system.
kafka
Publishes events to an Apache Kafka topic. Enable with publisher: kafka and configure under publisher-properties:
| Property | Required | Description |
|---|---|---|
kafka.bootstrap-servers | yes | Comma-separated list of Kafka bootstrap servers. |
kafka.topic | yes | Kafka topic events are published to. |
kafka.client-id | no | Kafka client id. |
kafka.send-timeout-ms | no | Timeout waiting for broker acknowledgement. Default 30000. |
kafka.client.* | no | Passed through verbatim to the underlying Kafka producer, with the kafka.client. prefix stripped (e.g. kafka.client.acks=all becomes producer property acks=all). |
Writing a Custom Publisher
To deliver events somewhere other than Kafka (a webhook, a message broker, a SIEM ingestion endpoint), implement the BusinessEventPublisher SPI:
package org.eximeebpms.bpm.commons.eventbus;
public interface BusinessEventPublisher extends AutoCloseable {
String getName();
default void init(Map<String, String> properties) {
}
BusinessEventPublishResult publish(Event event);
@Override
default void close() {
}
}
Register the implementation so it’s discoverable under its getName() value, then set publisher: <name> in configuration. init(Map<String, String>) receives whatever is configured under publisher-properties for that publisher name.
Script Guard / SIEM Integration
As of 1.2.19-ee, Script Guard violations are published as business events (bpms:script-violation:create by default) through this same mechanism. This means routing Script Guard violations to a SIEM is a matter of enabling Business Events and pointing the configured publisher at your SIEM ingestion endpoint (via the Kafka publisher, or a custom BusinessEventPublisher implementation) — no separate integration is required. The same applies to the user operation log events introduced in 1.3.1-ee, which give a SIEM a real-time feed of administrative actions (task assignment/suspension, batch/job/deployment operations) in addition to the periodic ACT_HI_OP_LOG table.
Querying the Outbox
The engine exposes a query API over the outbox via BusinessEventService:
processEngine.getBusinessEventService()
.createBusinessEventOutboxQuery()
.processInstanceId(processInstanceId)
.eventType("bpms:script-violation:create")
.list();
This is primarily useful for diagnostics and for verifying delivery independently of the configured publisher. Results are ordered by id, which follows write order. unprocessed() restricts the query to events not yet delivered. So the delivery backlog and its oldest event are:
BusinessEventQuery pending = processEngine.getBusinessEventService()
.createBusinessEventOutboxQuery()
.unprocessed();
long backlog = pending.count();
List<BusinessEventOutbox> oldest = pending.listPage(0, 1); // getCreatedDate() gives its age
unprocessed() and BusinessEventOutbox.getCreatedDate() are Enterprise Edition additions. Community Edition 1.4.0 has neither.