FEEL Expression Mastery for Camunda 8
Learn Java BPMN with Camunda 8 Zeebe - Part 011
Master FEEL expression semantics in Camunda 8 for BPMN, DMN, forms, data mapping, gateway logic, null handling, and production-safe variable contracts.
Part 011 — FEEL Expression Mastery for Camunda 8
FEEL adalah bahasa ekspresi yang dipakai Camunda 8 di beberapa titik penting: BPMN expressions, DMN decision logic, Forms, variable mappings, gateway conditions, timers, connector inputs, dan beberapa konfigurasi model lain. Di Camunda 8, FEEL bukan sekadar sintaks kecil untuk if. FEEL adalah boundary language antara model bisnis dan runtime data.
Mental model yang benar:
Java worker menjalankan side effect. FEEL mendeskripsikan transformasi, seleksi, keputusan kecil, dan routing yang harus tetap deterministik, transparan, dan mudah diaudit.
Dokumentasi Camunda menjelaskan FEEL sebagai bagian dari DMN specification dan digunakan dalam BPMN, DMN, dan Forms. FEEL didesain side-effect free, memiliki data model sederhana mirip JSON, dan memakai three-valued logic: true, false, dan null.
1. Skill Target
Setelah bagian ini, kita ingin mampu:
- Membaca FEEL expression di BPMN/DMN tanpa menebak-nebak runtime behavior.
- Mendesain variable contract yang aman untuk expression evaluation.
- Menghindari incident karena ekspresi menghasilkan
null, tipe salah, atau boolean yang tidak valid. - Memindahkan business rule kecil dari Java worker ke model tanpa membuat model menjadi scripting dump.
- Menentukan kapan FEEL cukup, kapan DMN lebih tepat, dan kapan Java worker harus dipakai.
Kita tidak sedang belajar FEEL sebagai bahasa pemrograman umum. Kita belajar FEEL sebagai control surface untuk process orchestration.
2. Where FEEL Lives in Camunda 8
FEEL muncul di banyak tempat:
| Area | Contoh penggunaan | Risiko utama |
|---|---|---|
| Exclusive gateway | riskScore >= 80 | riskScore null atau string |
| Sequence flow condition | case.status = "APPROVED" | equality semantics salah dipahami |
| Input/output mapping | { assignee: reviewer.id, priority: "HIGH" } | variable leakage |
| Business rule task | DMN input expression | rule tidak match |
| Timer expression | duration("PT24H") atau variable duration | format temporal salah |
| Forms | conditional visibility, validation-ish behavior | form dianggap domain validator |
| Connectors | request body, headers, endpoint interpolation | side effect config tidak eksplisit |
| Multi-instance | collection expression | collection null atau bukan list |
Prinsip praktis:
FEEL harus membuat runtime lebih eksplisit, bukan menyembunyikan domain complexity.
Kalau expression sudah sulit dibaca oleh reviewer proses, kemungkinan ia bukan lagi expression. Ia sudah berubah menjadi program kecil yang tersembunyi di model.
3. FEEL Data Model: Think JSON, Not Java
FEEL di Camunda 8 terasa dekat dengan JSON:
null- number
- string
- boolean
- list
- context/object
- date/time/duration
Contoh:
{
caseId: "CASE-2026-0001",
severity: "HIGH",
amount: 125000,
flags: ["AML", "SANCTION"],
officer: {
id: "u-100",
group: "enforcement"
}
}
Jangan membawa asumsi Java secara langsung:
| Java thinking | FEEL/Camunda thinking |
|---|---|
| object instance | context/object variable |
BigDecimal, Integer, Long | number |
List<T> | list |
Map<String, Object> | context |
| exception on missing property | often null |
| method call | function/expression only |
| mutation | no mutation |
FEEL expression tidak boleh dianggap bisa memanggil service, query database, mengubah state eksternal, atau melakukan IO. Kalau butuh side effect, gunakan worker/connector.
4. Expression vs Unary Test
FEEL punya dua mode pemakaian yang sering tercampur:
4.1 Expression
Expression menghasilkan value.
riskScore >= 80
if amount > 100000 then "HIGH" else "NORMAL"
{ priority: "P1", dueIn: duration("PT24H") }
4.2 Unary Test
Unary test biasanya dipakai di DMN input entry. Ia mengevaluasi apakah input memenuhi kondisi tertentu.
> 100000
"HIGH"
["OPEN", "REOPENED"]
not(null)
Cara berpikir:
- Expression menjawab: hasilkan apa?
- Unary test menjawab: apakah input memenuhi kondisi?
Kesalahan umum adalah menulis expression penuh di tempat yang mengharapkan unary test, atau sebaliknya.
5. Null Semantics: Sumber Banyak Incident
Di Java, missing field sering menghasilkan NullPointerException. Di FEEL, banyak evaluation failure justru menghasilkan null.
Contoh:
case.officer.department
Jika case.officer tidak ada, hasilnya bisa null.
Contoh gateway condition:
case.officer.department = "ENFORCEMENT"
Jika case.officer.department null, hasil expression bisa tidak sesuai ekspektasi. Untuk gateway, Camunda membutuhkan condition yang menghasilkan boolean. Jika expression tidak menghasilkan boolean yang valid, instance bisa stuck/incident tergantung konteks.
5.1 Defensive Null Checks
Gunakan ekspresi eksplisit:
is defined(case.officer) and is defined(case.officer.department) and case.officer.department = "ENFORCEMENT"
Atau sederhanakan kontrak data: pastikan worker sebelum gateway selalu menghasilkan shape stabil.
{
"case": {
"officer": {
"department": null
}
}
}
Shape stabil lebih baik daripada field hilang acak.
5.2 Null Should Be a Domain Signal Only When Intentional
null boleh dipakai jika maknanya jelas:
{
"appealSubmittedAt": null
}
Artinya: appeal belum diajukan.
Tapi jangan pakai null untuk menutupi data quality issue:
{
"riskScore": null
}
Jika riskScore wajib ada sebelum risk gateway, null harus menjadi validation failure sebelum gateway, bukan dibiarkan menabrak expression.
6. Equality and Comparison
FEEL memakai = untuk equality.
status = "APPROVED"
Bukan:
status == "APPROVED"
Comparison membutuhkan tipe yang kompatibel.
amount > 100000
Aman jika amount number.
Berbahaya jika payload dari API memberi string:
{
"amount": "100000"
}
Expression berikut terlihat benar tetapi secara tipe bermasalah:
amount > 50000
Prinsip produksi:
Normalize data type di boundary worker, bukan di setiap gateway.
Worker adapter harus mengubah external payload menjadi process contract yang typed secara konsisten.
7. Boolean Expressions for Gateways
Gateway condition harus mudah dibaca, deterministik, dan testable.
Buruk:
amount > 100000 and contains(customer.name, "PT") or risk.score >= 90 and not(isVip)
Lebih baik:
requiresEnhancedReview = true
Lalu hitung requiresEnhancedReview di DMN atau worker sebelumnya.
7.1 Rule of Thumb
Gunakan FEEL langsung di gateway hanya untuk decision kecil:
approval.decision = "APPROVED"
caseStatus = "CLOSED"
count(violations) > 0
Gunakan DMN jika:
- condition punya banyak kolom;
- business owner ingin review rule;
- rule sering berubah;
- hasil perlu dijelaskan/auditable;
- ada hit policy.
Gunakan worker jika:
- butuh query external system;
- butuh machine learning scoring;
- butuh heavy computation;
- butuh side effect;
- butuh dependency runtime.
8. Context Expressions
Context adalah object literal.
{
decision: "ESCALATE",
reason: "HIGH_RISK",
priority: "P1"
}
Context berguna untuk output mapping:
{
caseId: case.id,
reviewerGroup: "senior-enforcement",
dueDate: now() + duration("P2D")
}
Tetapi hati-hati: context expression bisa membuat variable model liar jika setiap task menghasilkan shape berbeda.
8.1 Stable Context Contract
Lebih baik:
{
"reviewAssignment": {
"group": "senior-enforcement",
"priority": "P1",
"reason": "HIGH_RISK"
}
}
Daripada menyebar variable global:
{
"reviewerGroup": "senior-enforcement",
"priority": "P1",
"assignmentReason": "HIGH_RISK"
}
Context harus mencerminkan bounded concept.
9. List Expressions
List banyak muncul dalam regulatory/case workflow:
- daftar violation;
- daftar document;
- daftar entity terdampak;
- daftar required approval;
- daftar remediation item.
Contoh:
count(violations) > 0
list contains(flags, "SANCTION")
violations[severity = "HIGH"]
for v in violations return v.code
9.1 Multi-Instance Collection
Untuk multi-instance BPMN, collection expression harus menghasilkan list.
Baik:
affectedEntities
Buruk jika affectedEntities bisa null.
Lebih aman jika upstream contract menjamin:
{
"affectedEntities": []
}
Bukan:
{}
Empty list berarti tidak ada item. Missing field berarti contract rusak atau data tidak lengkap.
10. Temporal Expressions
Timers dan SLA sering memakai date/time/duration.
Contoh duration:
duration("PT24H")
duration("P7D")
Contoh deadline dari variable:
case.submittedAt + duration("P14D")
10.1 Temporal Pitfall
Masalah umum:
- Timezone tidak eksplisit.
- Business calendar dianggap sama dengan calendar day.
- Weekend/holiday rule dimasukkan ke FEEL terlalu banyak.
- SLA pause/resume dimodelkan sebagai timer sederhana.
Jika SLA punya business calendar, cut-off, holiday, pause/resume, jurisdiction-specific rule, gunakan decision service atau worker khusus untuk menghitung due date, lalu FEEL hanya membaca hasil:
sla.dueAt
FEEL bukan tempat terbaik untuk enterprise calendar engine.
11. Input/Output Mapping Patterns
Variable mapping adalah salah satu tempat paling penting untuk FEEL.
11.1 Input Mapping as Boundary
Service task input mapping seharusnya mengirim data minimal ke worker.
Buruk:
case
Worker menerima seluruh case object.
Lebih baik:
{
caseId: case.id,
subjectId: case.subject.id,
requestedChecks: risk.requestedChecks
}
Tujuan:
- mengurangi coupling;
- menghindari leak data sensitif;
- memperjelas contract worker;
- membuat worker lebih testable.
11.2 Output Mapping as Anti-Corruption Layer
Worker output mungkin teknis:
{
"httpStatus": 200,
"responseBody": {
"risk_score": 87,
"risk_band": "HIGH"
},
"headers": {
"x-request-id": "abc"
}
}
Jangan biarkan shape ini masuk ke process global variable secara mentah.
Map menjadi domain process state:
{
riskAssessment: {
score: responseBody.risk_score,
band: responseBody.risk_band,
assessedAt: now()
}
}
Prinsip:
Variable mapping harus membentuk process language, bukan menyalin vendor language.
12. FEEL in Regulatory Lifecycle Modeling
Regulatory workflow biasanya punya domain concepts berikut:
- case;
- allegation;
- evidence;
- subject/entity;
- finding;
- enforcement action;
- notice;
- appeal;
- remediation;
- deadline;
- reviewer;
- authorization;
- jurisdiction.
Jangan membuat expression seperti ini di gateway:
case.type = "AML" and amount > 100000000 and subject.category = "BANK" and count(evidence[status = "VALIDATED"]) >= 3 and previousViolations > 0
Lebih defensible:
- Worker/DMN menghasilkan structured assessment.
- Gateway membaca hasil assessment.
{
"enforcementRouting": {
"path": "SENIOR_REVIEW",
"reasonCodes": ["HIGH_AMOUNT", "REPEAT_SUBJECT", "VALID_EVIDENCE_THRESHOLD_MET"],
"confidence": "HIGH"
}
}
Gateway:
enforcementRouting.path = "SENIOR_REVIEW"
Ini membuat audit dan explainability jauh lebih baik.
13. FEEL and Incidents
Expression dapat menyebabkan incident jika runtime membutuhkan value tertentu tetapi expression gagal memenuhi kontrak.
Contoh situasi:
- gateway condition tidak menghasilkan boolean;
- timer expression invalid;
- input mapping mengakses data yang tidak ada lalu menghasilkan payload invalid;
- DMN input expression type mismatch;
- multi-instance collection bukan list.
13.1 Debugging Checklist
Saat expression incident:
- Buka process instance di Operate.
- Lihat element yang incident.
- Lihat expression error message.
- Inspect variable saat token berada di element tersebut.
- Tanyakan: field missing, type mismatch, atau expression salah?
- Perbaiki variable jika incident operasional.
- Perbaiki model/worker contract jika incident sistemik.
- Tambahkan test untuk path yang gagal.
13.2 Incident Is Not a Validation Strategy
Jangan sengaja membiarkan gateway incident sebagai cara validasi.
Buruk:
riskScore > 80
Dengan asumsi kalau riskScore tidak ada, Operate akan menunjukkan incident.
Lebih baik:
- validasi input di worker;
- modelkan business data incomplete path;
- gunakan DMN untuk rule outcome
INSUFFICIENT_DATA; - hanya biarkan incident untuk technical/model-contract failure yang tidak boleh terjadi.
14. FEEL Pattern Catalog
14.1 Routing Flag Pattern
Hitung decision detail di DMN/worker, route dengan flag sederhana.
routing.target = "ESCALATION"
Manfaat:
- gateway readable;
- decision auditable;
- process path stabil.
14.2 Stable Shape Pattern
Selalu buat variable object dengan shape lengkap.
{
"appeal": {
"submitted": false,
"submittedAt": null,
"channel": null
}
}
Bukan field opsional liar.
14.3 Reason Code Pattern
Setiap decision menghasilkan reason code.
{
"decision": "REJECT",
"reasonCodes": ["MISSING_EVIDENCE", "EXPIRED_SUBMISSION_WINDOW"]
}
Gateway boleh sederhana:
decision = "REJECT"
Audit tetap kaya karena reason codes disimpan.
14.4 Minimal Worker Input Pattern
Input mapping membatasi data worker.
{
caseId: case.id,
documentIds: documents[id != null].id
}
14.5 Explicit Empty List Pattern
Default collection sebagai empty list.
{
"violations": [],
"documents": [],
"affectedEntities": []
}
Menghindari null-check berulang.
15. FEEL Anti-Pattern Catalog
15.1 Script Dump in Gateway
Gateway berisi business rule panjang.
Dampak:
- susah review;
- susah test;
- susah explain;
- perubahan rule memaksa deploy BPMN.
Solusi: pindahkan ke DMN atau worker decision service.
15.2 Hidden Type Conversion
Mengandalkan FEEL untuk membereskan data type kacau.
Dampak:
- expression fragile;
- bug muncul hanya di path tertentu;
- incident sulit direproduksi.
Solusi: normalize di boundary worker.
15.3 Variable Archaeology
Expression membaca variable dari banyak tempat tanpa bounded context.
customerStatus = "ACTIVE" and kycScore > 70 and officerGroup = "A" and region != "X"
Solusi: bentuk object domain.
eligibility.result = "ELIGIBLE"
15.4 Null as Control Flow
Menggunakan missing variable untuk memilih path.
Solusi: pakai explicit flag.
{
"appeal": {
"submitted": false
}
}
15.5 Business Calendar in FEEL
Memasukkan semua holiday/working-day logic ke FEEL.
Solusi: worker/decision service menghasilkan due date final.
16. Testing FEEL
FEEL harus dites pada level yang sesuai.
| Test level | Apa yang dites |
|---|---|
| Expression unit test | Expression menghasilkan value benar untuk input tertentu |
| BPMN path test | Gateway memilih path benar |
| DMN test | Rule table match benar |
| Contract test | Worker output memenuhi variable shape |
| Incident regression test | Data buruk tidak membuat incident tidak terkendali |
16.1 Test Case Matrix
Untuk setiap expression penting, buat minimal:
- Happy path.
- Boundary value.
- Missing field.
- Wrong type.
- Empty list.
- Null value intentional.
- Unknown enum.
Contoh gateway:
riskAssessment.band = "HIGH"
Test:
| Input | Expected |
|---|---|
{ riskAssessment: { band: "HIGH" } } | true |
{ riskAssessment: { band: "LOW" } } | false |
{ riskAssessment: { band: null } } | false or handled path |
{} | contract failure or explicit guard |
Jangan hanya mengetes happy path.
17. Java Boundary: Preparing Variables for FEEL
Worker Java seharusnya menghasilkan variable yang friendly untuk FEEL.
Buruk:
record RiskResponse(
String risk_score,
String risk_band,
Map<String, Object> metadata
) {}
Lalu langsung publish semua sebagai variable.
Lebih baik:
public record RiskAssessmentVariable(
BigDecimal score,
String band,
List<String> reasonCodes,
Instant assessedAt
) {}
Dengan output:
{
"riskAssessment": {
"score": 87,
"band": "HIGH",
"reasonCodes": ["SANCTION_FLAG", "LARGE_AMOUNT"],
"assessedAt": "2026-06-28T10:15:30Z"
}
}
Expression menjadi sederhana:
riskAssessment.band = "HIGH"
17.1 Java Worker Contract Rule
Worker yang baik untuk FEEL:
- tidak mengeluarkan field random;
- tidak mencampur transport response dan domain response;
- memakai enum/string value stabil;
- memakai number sebagai number, bukan string;
- memakai empty list, bukan null list;
- memakai object wrapper untuk bounded concept;
- menyertakan reason code untuk audit.
18. FEEL Design Review Checklist
Gunakan checklist ini sebelum BPMN/DMN dipromosikan ke environment tinggi.
Expression Readability
- Apakah expression bisa dipahami dalam 10 detik?
- Apakah expression mengandung business rule kompleks?
- Apakah reviewer non-Java bisa memahami maksudnya?
Data Contract
- Apakah semua field yang dibaca dijamin ada?
- Apakah tipe data stabil?
- Apakah null punya makna domain yang eksplisit?
- Apakah list default ke
[]?
Runtime Safety
- Apakah gateway condition selalu boolean?
- Apakah timer expression menghasilkan temporal value valid?
- Apakah multi-instance expression selalu list?
- Apakah missing data punya modeled path?
Auditability
- Apakah decision menghasilkan reason code?
- Apakah expression penting tercakup test?
- Apakah variable output cukup untuk menjelaskan routing?
Maintainability
- Apakah rule sering berubah? Jika ya, seharusnya DMN.
- Apakah expression membutuhkan external lookup? Jika ya, worker.
- Apakah expression menduplikasi logic di tempat lain?
19. Mini Exercise: Refactor a Bad Gateway
Kita mulai dengan gateway buruk:
case.amount > 100000000 and case.subject.type = "BANK" and count(case.evidence[status = "VALIDATED"]) >= 3 and previousViolations > 0
Masalah:
- rule terlalu panjang;
- membaca terlalu banyak struktur;
- ada hidden assumption tentang
case.evidence; - tidak menghasilkan reason code;
- sulit explain kepada auditor;
- perubahan rule memaksa ubah BPMN.
Refactor:
- Buat DMN
Determine Enforcement Routing. - Input: amount, subject type, validated evidence count, previous violation count.
- Output: route, priority, reason codes.
- Gateway hanya baca route.
Gateway baru:
enforcementRouting.route = "SENIOR_REVIEW"
Variable:
{
"enforcementRouting": {
"route": "SENIOR_REVIEW",
"priority": "P1",
"reasonCodes": [
"HIGH_AMOUNT",
"REGULATED_ENTITY",
"EVIDENCE_THRESHOLD_MET",
"REPEAT_SUBJECT"
]
}
}
Hasil:
- model lebih bersih;
- decision lebih testable;
- audit lebih kuat;
- worker tidak menjadi tempat policy tersembunyi.
20. Summary
FEEL di Camunda 8 adalah bahasa ekspresi untuk membuat process model hidup dengan data. Tapi FEEL harus dipakai sebagai expression boundary, bukan sebagai scripting engine tersembunyi.
Prinsip utama:
- FEEL harus side-effect free.
- Gateway expression harus sederhana.
- Null harus disengaja, bukan akibat kontrak lemah.
- Worker Java harus menghasilkan variable shape yang stabil.
- Rule kompleks sebaiknya pindah ke DMN.
- Expression penting harus dites dengan missing/wrong-type cases.
- Auditability lebih penting daripada clever expression.
Di bagian berikutnya, kita akan memperluas ini ke DMN: bagaimana mengubah rule bisnis menjadi decision table yang eksplisit, versioned, testable, dan bisa dipakai sebagai decision boundary dalam BPMN.
References
- Camunda 8 Docs — What is FEEL?: https://docs.camunda.io/docs/components/modeler/feel/what-is-feel/
- Camunda 8 Docs — Expressions concept: https://docs.camunda.io/docs/components/concepts/expressions/
- Camunda 8 Docs — BPMN, DMN, and FEEL: https://docs.camunda.io/docs/components/concepts/bpmn-dmn-feel/
- Camunda 8 Docs — FEEL data types: https://docs.camunda.io/docs/components/modeler/feel/language-guide/feel-data-types/
- Camunda 8 Docs — FEEL control flow: https://docs.camunda.io/docs/components/modeler/feel/language-guide/feel-control-flow/
- Camunda 8 Docs — FEEL error handling: https://docs.camunda.io/docs/components/modeler/feel/language-guide/feel-error-handling/
- Camunda 8 Docs — FEEL built-in functions: https://docs.camunda.io/docs/components/modeler/feel/builtin-functions/feel-built-in-functions-introduction/
You just completed lesson 11 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.