Engine Expert
Reference for making changes to the Zeebe workflow engine with confidence. The engine processes thousands of commands per second on a single thread, state is rebuilt from the log on every restart, and partitions communicate over an unreliable network. Mistakes here cause leader/follower divergence, upgrade-path breakage, or production performance regressions. This skill exists to prevent the well-known classes of mistake.
Iron rules
These are summaries. The reference files (linked below) are authoritative — they carry the rationale, exceptions, and worked examples.
- No state mutation from processors. State changes only through events applied by event appliers.
- Released event appliers — and any method on a
Mutable*State interface or anything transitively called from an applier — must not change in logic. Add a new version/method instead. Cosmetic changes (formatting, imports, comments, behavior-equivalent renames) are fine. No golden file protects state class methods — extra care required.
- New event applier versions must reach every newer minor before its initial release. A version that ships only in an older minor's patch breaks upgrade replay — dead partitions, unrecoverable without an ad-hoc patch. See
event-appliers.md.
- Processors must end the command's processing by appending an event or a rejection. Exceptions are a last-resort rollback mechanism and are expensive — prefer pre-validation.
- Generated keys must be used as the record key of at least one appended record. Otherwise the key generator can't be rehydrated on replay and may hand out a duplicate.
- Inter-partition command receivers must be idempotent. Prefer reject-redundant-command over re-emitting events.
- Hot paths log at trace level only. INFO/DEBUG on a hot path is a performance regression at engine throughput.
- Values returned from
ColumnFamily.get(...) / state reads are backed by a shared, mutable buffer reused on the next read of the same column family. Never cache them or hold them across another read — copyFrom(...) if you need to keep the value, or use get(key, valueSupplier) to allocate a fresh instance.
- Non-transactional side effects (metrics, post-commit hooks) run only after all state and follow-up command writes that could throw. Exceptions roll back state, but already-executed side effects don't roll back — counters drift.
- No unbounded recursion unless documented why it's safe. Especially for process trees, call activities, and other user-provided input. A
StackOverflowError would ban the executing instance. Bound recursion or use iteration/trampolining.
Where to read next
| If you are… |
Read |
Designing or extending a record value type, intent, or ValueType |
records.md |
| Editing or creating a processor, behavior class, validation, or rejection |
processors.md |
Editing or creating an event applier, a Mutable*State interface method, or anything called from an applier |
event-appliers.md |
| Writing or modifying engine tests |
testing.md |
| Working with authorization resource types or permission types (which enum to use in which layer) |
authz-enums.md |
Local checks before commit
Run only the tests relevant to your change — running the full engine test suite locally takes a long time and is what CI is for.
# 1. Format (mandatory before commit when touching Java/markdown/pom.xml).
./mvnw license:format spotless:apply -T1C
# 2. Tests scoped to your change (single class or small pattern). Use
# -Dtest=YourTest, comma-separated classes, or a glob like '*UserTest*'.
./mvnw verify -pl zeebe/engine \
-Dtest='YourTest' -DskipTests=false -DskipITs -Dquickly
# 3. If you touched an event applier, a Mutable*State method, or anything
# transitively called from an applier — also run the golden file check.
./mvnw verify -pl zeebe/engine -Dtest=NoChangesTest \
-DskipTests=false -DskipITs -Dquickly
Re-run any new or modified test at least 3× to catch flakiness. Push and let CI run the comprehensive suite.
Recommended workflow for new processor logic
- Write the test first, using
EngineRule and RecordingExporter against appended records (see testing.md). Run it — confirm it fails.
- Implement the processor (and its event applier + state changes).
- Re-run the test 3+ times. Then run the local checks block above.
Canonical docs
1---2name: engine-expert3description: Use when implementing, fixing, or reviewing Zeebe engine code in zeebe/engine/ — BPMN process execution, DMN decision evaluation, job lifecycle, user/identity management, batch operations, variables, deployments, signals, messages, timers, multi-tenancy, or authorization. Also when modifying or reviewing processors, event appliers, state classes, record value types, intents, or engine tests.4---56# Engine Expert78Reference for making changes to the Zeebe workflow engine with confidence. The engine processes thousands of commands per second on a single thread, state is rebuilt from the log on every restart, and partitions communicate over an unreliable network. Mistakes here cause leader/follower divergence, upgrade-path breakage, or production performance regressions. This skill exists to prevent the well-known classes of mistake.910## Iron rules1112These are summaries. The reference files (linked below) are authoritative — they carry the rationale, exceptions, and worked examples.1314- No state mutation from processors. State changes only through events applied by event appliers.15- Released event appliers — and any method on a `Mutable*State` interface or anything transitively called from an applier — must not change in logic. Add a new version/method instead. Cosmetic changes (formatting, imports, comments, behavior-equivalent renames) are fine. No golden file protects state class methods — extra care required.16- New event applier versions must reach every newer minor before its initial release. A version that ships only in an older minor's patch breaks upgrade replay — dead partitions, unrecoverable without an ad-hoc patch. See `event-appliers.md`.17- Processors must end the command's processing by appending an event or a rejection. Exceptions are a last-resort rollback mechanism and are expensive — prefer pre-validation.18- Generated keys must be used as the record key of at least one appended record. Otherwise the key generator can't be rehydrated on replay and may hand out a duplicate.19- Inter-partition command receivers must be idempotent. Prefer reject-redundant-command over re-emitting events.20- Hot paths log at trace level only. INFO/DEBUG on a hot path is a performance regression at engine throughput.21- Values returned from `ColumnFamily.get(...)` / state reads are backed by a shared, mutable buffer reused on the next read of the same column family. Never cache them or hold them across another read — `copyFrom(...)` if you need to keep the value, or use `get(key, valueSupplier)` to allocate a fresh instance.22- Non-transactional side effects (metrics, post-commit hooks) run only after all state and follow-up command writes that could throw. Exceptions roll back state, but already-executed side effects don't roll back — counters drift.23- No unbounded recursion unless documented why it's safe. Especially for process trees, call activities, and other user-provided input. A `StackOverflowError` would ban the executing instance. Bound recursion or use iteration/trampolining.2425## Where to read next2627| If you are… | Read |28|--------------------------------------------------------------------------------------------------------------|---------------------|29| Designing or extending a record value type, intent, or `ValueType` | `records.md` |30| Editing or creating a processor, behavior class, validation, or rejection | `processors.md` |31| Editing or creating an event applier, a `Mutable*State` interface method, or anything called from an applier | `event-appliers.md` |32| Writing or modifying engine tests | `testing.md` |33| Working with authorization resource types or permission types (which enum to use in which layer) | `authz-enums.md` |3435## Local checks before commit3637Run only the tests relevant to your change — running the full engine test suite locally takes a long time and is what CI is for.3839```bash40# 1. Format (mandatory before commit when touching Java/markdown/pom.xml).41./mvnw license:format spotless:apply -T1C4243# 2. Tests scoped to your change (single class or small pattern). Use44# -Dtest=YourTest, comma-separated classes, or a glob like '*UserTest*'.45./mvnw verify -pl zeebe/engine \46 -Dtest='YourTest' -DskipTests=false -DskipITs -Dquickly4748# 3. If you touched an event applier, a Mutable*State method, or anything49# transitively called from an applier — also run the golden file check.50./mvnw verify -pl zeebe/engine -Dtest=NoChangesTest \51 -DskipTests=false -DskipITs -Dquickly52```5354Re-run any new or modified test at least 3× to catch flakiness. Push and let CI run the comprehensive suite.5556## Recommended workflow for new processor logic57581. Write the test first, using `EngineRule` and `RecordingExporter` against appended records (see `testing.md`). Run it — confirm it fails.592. Implement the processor (and its event applier + state changes).603. Re-run the test 3+ times. Then run the local checks block above.6162## Canonical docs6364- `zeebe/engine/README.md` — architecture, do's and don'ts.65- `docs/zeebe/event-applier-golden-files.md` — golden files, versioning, port rules.66- `docs/zeebe/developer_handbook.md` — step-by-step for new records, REST endpoints, authorization.67- `docs/zeebe/engine_questions.md` — FAQ on tokens, joining gateways, variable scoping.68- https://github.com/camunda/camunda/wiki/Logging — logging guidance.69- https://github.com/camunda/camunda/wiki/Error-Guidelines — error message format and rejection vs. exception.70