CEL cookbook
Contents
CEL cookbook#
This guide is for implementers who need ready-to-use CEL patterns.
Copy, paste, and adapt these patterns for your use case.
Note
Which symbols are available depends on the screen and profile. Use the editor's symbol browser to confirm what exists in your context.
Basic patterns#
Boolean logic#
r.is_registrant == true and r.active == true
Equivalent with operators:
r.is_registrant && r.active
Handle missing values#
Always check existence before accessing optional fields:
has(r.birthdate) and age_years(r.birthdate) >= 18
Ternary (if/else)#
r.is_group ? 1 : 0
More complex:
r.income < 5000 ? "low" : (r.income < 15000 ? "medium" : "high")
String comparisons#
r.status == "active"
r.region != "excluded_region"
Household composition#
Any child under 5#
members.exists(m, age_years(m.birthdate) < 5)
Or newer syntax:
members.exists(age_years(m.birthdate) < 5)
Count children under 18#
members.count(m, age_years(m.birthdate) < 18)
Count children in age range#
Children aged 6-12:
members.count(m, age_years(m.birthdate) >= 6 and age_years(m.birthdate) <= 12)
Female head of household#
members.exists(m, head(m) and m.gender == "female")
Note
The exact gender field/value depends on your data model. Use autocomplete to find the right field (e.g., gender, gender_id.name, gender_id.code).
Disabled member present#
members.exists(m, m.has_disability == true)
Household size#
members.count(m, true) >= 3
Program enrollments#
Any active enrollment#
enrollments.exists(e, e.state == "enrolled")
Enrolled in specific program#
enrollments.exists(e, e.state == "enrolled" and e.program_id.name == "4Ps")
Not currently enrolled#
not enrollments.exists(e, e.state == "enrolled")
Count active enrollments#
enrollments.count(e, e.state == "enrolled")
Entitlements#
Has approved entitlement#
entitlements.exists(ent, ent.state == "approved")
Total entitlement amount#
entitlements.sum(ent.amount, ent.state == "approved")
Entitlements this year#
entitlements.exists(ent, ent.state == "approved" and ent.date_approved >= "2024-01-01")
Event data#
Requires event/CEL integration modules.
Count visits this year#
events_count("visit", period=this_year())
Total payments received#
events_sum("payment", "amount", period=this_year())
Average survey score#
events_avg("survey", "score", within_months=12)
Highest payment amount#
events_max("payment", "amount", period=this_year())
Latest survey income#
event("survey", "income", select="latest", within_months=12, default=0)
Income below threshold#
event("survey", "income", select="latest", within_months=12, default=0) < 500
Completed health check#
has_event("health_check", within_months=6)
Eligibility rules#
Income-based#
Household income under threshold:
household_income < poverty_line
Or with literal:
household_income < 10000
Income within a range:
between(household_income, 5000, 15000)
Demographic targeting#
Children under 5 in low-income household:
children_under_5_count >= 1 and household_income < poverty_line
Multi-criteria#
household_income < poverty_line
and children_under_5_count >= 1
and not enrollments.exists(e, e.state == "enrolled" and e.program_id.name == "Other Program")
Vulnerability score#
vulnerability_score >= 50
Entitlement amount formulas#
These are evaluated at runtime with a small context (typically r and base_amount).
Fixed amount#
500
Scaled by base#
base_amount * 1.1
Per child bonus#
base_amount + (children_under_5_count * 200)
Capped amount#
min(base_amount + (children_count * 100), 2000)
Tiered by household size#
members.count(m, true) <= 3 ? 500 : (members.count(m, true) <= 5 ? 750 : 1000)
Scoring formulas#
Simple indicator#
r.has_disability ? 10 : 0
Age-based score#
age_years(r.birthdate) >= 65 ? 5 : 0
Household composition score#
children_under_5_count * 3 + elderly_count * 2
Normalized score#
min(vulnerability_score / 10, 10)
Validation rules#
Required field#
has(r.birthdate)
Age validation#
has(r.birthdate) and age_years(r.birthdate) >= 0 and age_years(r.birthdate) <= 120
Using between() for range check:
has(r.birthdate) and between(age_years(r.birthdate), 0, 120)
Format check#
Phone number prefix:
startswith(r.phone, "+63")
ID format with regex:
matches(r.national_id, "^[A-Z]{2}-[0-9]{8}$")
Cross-field validation#
Head of household must be adult:
not head(r) or age_years(r.birthdate) >= 18
Workflow routing#
These are runtime evaluated with ticket/case context.
Priority based on age#
r.registrant_id.age >= 65 ? "priority" : "standard"
Escalate high value#
r.amount > 10000 ? "manager_review" : "standard"
Route by region#
r.area_id.name == "NCR" ? "ncr_team" : "regional_team"
Tips for adapting patterns#
Check field names: Use autocomplete to find exact field paths
Verify context: Individual vs group expressions need different symbols
Test incrementally: Start simple, add complexity
Use variables: Extract repeated logic into variables
Add tests: Create test cases in Studio before publishing
Are you stuck?#
See CEL troubleshooting for common issues and debugging tips.
openspp.org