Build CoreOrdered learning track

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.

12 min read2292 words
PrevNext
Lesson 1135 lesson track0719 Build Core
#java#camunda#camunda-8#zeebe+6 more

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:

  1. Membaca FEEL expression di BPMN/DMN tanpa menebak-nebak runtime behavior.
  2. Mendesain variable contract yang aman untuk expression evaluation.
  3. Menghindari incident karena ekspresi menghasilkan null, tipe salah, atau boolean yang tidak valid.
  4. Memindahkan business rule kecil dari Java worker ke model tanpa membuat model menjadi scripting dump.
  5. 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:

AreaContoh penggunaanRisiko utama
Exclusive gatewayriskScore >= 80riskScore null atau string
Sequence flow conditioncase.status = "APPROVED"equality semantics salah dipahami
Input/output mapping{ assignee: reviewer.id, priority: "HIGH" }variable leakage
Business rule taskDMN input expressionrule tidak match
Timer expressionduration("PT24H") atau variable durationformat temporal salah
Formsconditional visibility, validation-ish behaviorform dianggap domain validator
Connectorsrequest body, headers, endpoint interpolationside effect config tidak eksplisit
Multi-instancecollection expressioncollection 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 thinkingFEEL/Camunda thinking
object instancecontext/object variable
BigDecimal, Integer, Longnumber
List<T>list
Map<String, Object>context
exception on missing propertyoften null
method callfunction/expression only
mutationno 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:

  1. Timezone tidak eksplisit.
  2. Business calendar dianggap sama dengan calendar day.
  3. Weekend/holiday rule dimasukkan ke FEEL terlalu banyak.
  4. 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:

  1. Worker/DMN menghasilkan structured assessment.
  2. 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:

  1. Buka process instance di Operate.
  2. Lihat element yang incident.
  3. Lihat expression error message.
  4. Inspect variable saat token berada di element tersebut.
  5. Tanyakan: field missing, type mismatch, atau expression salah?
  6. Perbaiki variable jika incident operasional.
  7. Perbaiki model/worker contract jika incident sistemik.
  8. 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 levelApa yang dites
Expression unit testExpression menghasilkan value benar untuk input tertentu
BPMN path testGateway memilih path benar
DMN testRule table match benar
Contract testWorker output memenuhi variable shape
Incident regression testData buruk tidak membuat incident tidak terkendali

16.1 Test Case Matrix

Untuk setiap expression penting, buat minimal:

  1. Happy path.
  2. Boundary value.
  3. Missing field.
  4. Wrong type.
  5. Empty list.
  6. Null value intentional.
  7. Unknown enum.

Contoh gateway:

riskAssessment.band = "HIGH"

Test:

InputExpected
{ 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:

  1. Buat DMN Determine Enforcement Routing.
  2. Input: amount, subject type, validated evidence count, previous violation count.
  3. Output: route, priority, reason codes.
  4. 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:

  1. FEEL harus side-effect free.
  2. Gateway expression harus sederhana.
  3. Null harus disengaja, bukan akibat kontrak lemah.
  4. Worker Java harus menghasilkan variable shape yang stabil.
  5. Rule kompleks sebaiknya pindah ke DMN.
  6. Expression penting harus dites dengan missing/wrong-type cases.
  7. 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

Lesson Recap

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.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.