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).

Elderly member present#

Person over 65:

members.exists(m, age_years(m.birthdate) >= 65)

Disabled member present#

members.exists(m, m.has_disability == true)

Household size#

members.count(m, true) >= 3

Sum member income#

members.sum(m.income, true)

With threshold:

members.sum(m.income, true) < 10000

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.

Has recent visit#

Visit in last 90 days:

has_event("visit", within_days=90)

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

Geographic targeting#

In target region:

r.area_id.name == "Region IV-A"

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#

  1. Check field names: Use autocomplete to find exact field paths

  2. Verify context: Individual vs group expressions need different symbols

  3. Test incrementally: Start simple, add complexity

  4. Use variables: Extract repeated logic into variables

  5. Add tests: Create test cases in Studio before publishing

Are you stuck?#

See CEL troubleshooting for common issues and debugging tips.