Skip to content
  • There are no suggestions because the search field is empty.

Common Expression Language

MyDataHelps supports a powerful Common Expression Language (CEL), which is used in survey Calculated Fields to do things such as:

  • Calculate a score, which can include survey answers and custom fields
  • Classify a score or answer into a text string
  • Randomize participants 

Introduction

The MyDataHelps CEL is a user-friendly expression language which works similarly to spreadsheet formulas and supports both numeric and text operations. It is an implementation of the open source Common Expression Language, extended with custom variables providing access to participant and survey data.

Quick Start Guide

CEL makes it simple to write calculations for things like BMI, date calculation, and scoring as in these examples.

// Calculate BMI from weight (kg) and height (m)
weight / (height * height)

// Calculate age from birthdate
dateDiff(today(), birthdate, "years")

// Average of three pain scores
mean([pain1, pain2, pain3])

// Check if pain level is severe
painLevel > 7

// Categorize BMI
bmi < 18.5 ? "Underweight" : 
  bmi < 25 ? "Normal weight" : 
  bmi < 30 ? "Overweight" : "Obese"

Expression Basics

CEL offers a predictable range of operators supporting numeric and text operations similar to spreadsheet formulas.

Numbers and Math
5 + 3          // Addition → 8
10 - 4         // Subtraction → 6
6 * 7          // Multiplication → 42
15 / 3         // Division → 5
17 % 5         // Remainder → 2
Comparing Values
age >= 18      // Greater than or equal
score == 100   // Equal to
status != "inactive"  // Not equal to
weight < previousWeight  // Less than
Membership Operations
"Headache" in symptoms  // Value in multiple-choice answer
arm in ["TreatmentA", "TreatmentC"] // Answer in collection of values
Text Operations
"Hello" + " " + "World"  // Combine text → "Hello World"
demographics.firstName + " " + demographics.lastName  // Full name
Making Decisions (IF-THEN-ELSE)
// IF age >= 18 THEN "Adult" ELSE "Minor"
age >= 18 ? "Adult" : "Minor"

// Nested decisions
temperature > 100 ? "Fever" : 
  temperature > 98.6 ? "Elevated" : "Normal"

Accessing Your Data

The MyDataHelps implementation of CEL offers access to information about the current participant, including current and past survey answers.

Survey Answers
// Simple answer access
weight      // Current weight answer
painLevel   // Current pain level

// Explicit access
surveyAnswers.getCurrent("painLevel")   // Current pain level
sa.c("weight", "Week 1")      // weight for "Week 1" event

// All answers, current + historical
surveyAnswers.getAll("weight")     // All weight measurements
sa.a("weight")   // All weight measurements, shorthand notation
Participant Information
customFields.TreatmentGroup
participantIdentifier
demographics.firstName
enrollmentDate
institution.code
project.name
organization.description

Helpful Functions

CEL supports a wide range of functions, allowing complex calculations and operations.

Aggregation
sum(sa.a("score"))   // Add values together
mean(sa.a("score"))  // Calculate average
min(sa.a("score"))   // Find lowest value
max(sa.a("score"))   // Find highest value
Date Functions
today()                                // Today's date
dateDiff(today(), birthdate, "years")  // Age calculation
addDays(today(), 7)               // One week from now
formatDate(visitDate, "MMM d, yyyy")   // Format: Jan 22, 2026
Text Functions
response.answers[0].contains("yes")    // Check if text contains word
email.answers[0].matches(".*@.*")      // Validate format
name.answers[0].size()                 // Count characters
List Functions
// Filter pain scores > 5
sa.a("pain").filter(x, x > 5)

// Check if any match
sa.a("symptoms").exists(x, "Fever" in x)

Basic Syntax

The basic building blocks of CEL are types, type conversion, and operators. 

Supported Types

Type DESCRIPTION Examples
Integer Whole numbers

42

-17

Decimal Floating-point numbers

3.14

-0.5

String Text, enclosed in double quotes

"Hello, World!"

"Control"

Boolean A true or false value

true

false

Date Text representation of a date, in ISO format (YYYY-MM-DD) "2026-01-22"
Timestamp Text representation of a date, in ISO-8601 format including timezone offset. "2026-01-22T10:30:00-05:00"

TimeOfDay

Text representation of a time, in 12-hour time format

"10:30 AM"

"02:45 PM"

List List (array) of variables of another type, enclosed in square brackets.

[1, 2, 3, 4, 5]

["apple", "banana", "cherry"]

[true, false, false]

Null Absence of a value null

Type Conversion

CEL provides built-in functions to convert between these types.

String Conversion

Convert any value to a string:

string(123)           // → "123"
string(3.14)          // → "3.14"
string(true)          // → "true"
Integer Conversion

Convert to an integer (hole number):

int("42")             // → 42
int(3.14)             // → 3 (truncates decimal)
int("3.14")           // → 3
Decimal Conversion

Convert to a decimal (floating-point number):

double("3.14")        // → 3.14
double(42)            // → 42.0
double("42")          // → 42.0

Operators

Arithmetic Operators
Operator Description Example Result
+

Addition

`5 + 3` 8
- Subtraction `10 - 4` 6
Multiplication `6 * 7` 42
/ Division `15 / 3` 5
Modulo (remainder) `17 % 3` 2
- (unary) Negation `-5` -5

Examples:

(8 + 2) * 3              // → 30
age * 2 + 10             // If age is 25: → 60
Comparison Operators
Operator Description Example Result
== Equal to 5 == 5 true
!= Not equal to 5 != 3 true
< Less than 3 < 5 true
<= Less than or equal to 5 <= 5 true
> Greater than 7 > 5 true
>= Greater than or equal to 5 >= 3 true

Examples:

age >= 18                // Check if adult
gender == "F"            // Check gender
Logical Operators
Operator Description Example Result
&& Logical AND true && false false
|| Logical OR true || false true
! Logical NOT !true false

Examples:

age >= 18 && age < 65                     // Working age adult
status == "active" || status == "pending" // Either status
!hasCompletedSurvey                       // Survey not completed
String Concatenation

The + operator concatenates strings:

"Hello" + " " + "World"   // → "Hello World"
"Welcome " + firstName    // → "Welcome John"
Membership Operator
Operator Description Example Result
in Membership test "a" in ["a", "b"] true

Examples:

"apple" in ["apple", "banana"]           // → true
"Headache" in symptoms                   // Check if symptom selected
status in ["active", "pending"]          // Status is one of these
Ternary Operator (Conditional)

Returns one of two values based on a condition:

condition ? valueIfTrue : valueIfFalse

Examples:

age >= 18 ? "adult" : "minor"
score >= 90 ? "A" : score >= 80 ? "B" : "C"
enrolled && active ? "Participating" : "Not Participating"

Operator Precedence

Operators are evaluated in the following order (highest to lowest precedence):

  1. Unary operators: !, - (negation)
  2. Multiplicative: *, /, %
  3. Additive: +, -
  4. Comparison: <, <=, >, >=
  5. Equality: ==, !=
  6. Logical AND: &&
  7. Logical OR: ||
  8. Ternary: ? :

Use parentheses to override precedence:

(age + 5) * 2            // Add first, then multiply
age >= 18 && (status == "active" || status == "pending")  // If age = 18 AND status is "active" or "pending"

Functions

The MyDataHelps CEL implementation supports built-in CEL functions as well as custom MyDataHelps functions. All supported functions are defined below.

String Functions

String functions work with string literals and string variables.

function DESCRIPTION Example
contains(substring) Check if string contains substring "Hello World".contains("World")true
startsWith(prefix) Check if string starts with prefix "Hello".startsWith("He")true
endsWith(suffix) Check if string ends with suffix "file.txt".endsWith(".txt")true
matches(pattern) Check if string matches regex customFields.MyAnswer.matches(".*@.*\\..*")true
size() Get string length "Hello".size()5

Examples with survey answers:

comments.contains("yes") // Check if comments contains "yes"
email.matches("[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z]+")  // Email validation
name.size() > 3  // Name length check

List Functions

List functions operate on lists of objects.

function DESCRIPTION Example
size() Get number of elements [1, 2, 3].size()3
map(var, expr) Transform each element [1, 2, 3].map(x, x * 2)[2, 4, 6]
filter(var, cond) Keep matching elements [1, 2, 3, 4].filter(x, x > 2)[3, 4]
exists(var, cond) Check if any match [1, 3, 5].exists(x, x > 4)true
all(var, cond) Check if all match [1, 2, 3].all(x, x > 0)true
has(field) Check if key exists has(customFields.Age)true/false

Examples with survey answers:

// Size
sa.a("weight").size()  // Count of weight measurements

// Filter
sa.a("pain").filter(x, x > 5)  // Pain scores > 5

// Exists
sa.a("symptoms").exists(x, "Headache" in x)  // Any headache across any symptom answer

// All
sa.a("compliance").all(x, x == "Yes")  // 100% compliant?

// Has
has(my_answer) ? my_answer : 0  // Default value if my_answer has no or invalid current value

Math Functions

Aggregate Functions
function DESCRIPTION Example
sum(list) Sum all numbers sum([1, 2, 3])6
mean(list) Calculate average mean([10, 20, 30])20
min(list) Find minimum min([5, 2, 8])2
max(list) Find maximum max([5, 2, 8])8

Examples with survey answers:

mean(sa.a("weight"))  // Average of all weight answers
sum(sa.a("score"))  // Add up all score values
min([score1, score2, score3])  // Explicitly min of 3 scores by result identifier
Rounding Functions
function DESCRIPTION Example
round(number) Round to nearest integer round(3.6)4
round(number, places) Round to decimal places round(3.14159, 2)3.14
abs(number) Absolute value abs(-5)5
ceil(number) Round up to integer ceil(3.1)4
ceil(number, places) Round up to decimal places ceil(3.141, 2)3.15
floor(number) Round down to integer floor(3.9)3
floor(number, places) Round down to decimal places floor(3.149, 2)3.14

Examples:

// Basic rounding
round(bmi)  // Round BMI to integer
abs(weight - targetWeight)  // Weight difference (absolute)

// Decimal places
round(3.14159, 2)  // → 3.14 (round to 2 decimals)
round(bmi, 1)  // → 24.7 (round BMI to 1 decimal)
ceil(3.141, 2)  // → 3.15 (round up to 2 decimals)
floor(3.149, 2)  // → 3.14 (round down to 2 decimals)

// Negative decimal places (round to tens, hundreds, etc)
round(1234.5, -1)  // → 1230 (round to nearest 10)
round(1567, -2)  // → 1600 (round to nearest 100)
ceil(1234, -2)  // → 1300 (round up to nearest 100)
floor(1267, -2)  // → 1200 (round down to nearest 100)

// Practical uses
round(dosage / 10, 1)  // Dosage calculation with 1 decimal
ceil(age / 10) * 10  // Age decade (rounded up)
floor(price, 2)  // Price rounded down to cents

 

Date & Time Functions

Date functions work with multiple formats and preserve input formatting where possible.

Supported formats:

  • Date only: "2026-01-22" (YYYY-MM-DD)
  • ISO timestamp: "2026-01-22T10:30:00Z" (UTC)
  • Timestamp with timezone: "2026-01-22T10:30:00-05:00"
  • Time only: "10:00 AM" or "2:30 PM" (12-hour format with AM/PM)
 Current Date/Time:
function returns Example
today() Current date (YYYY-MM-DD) today()"2026-01-22"
now() Current timestamp with timezone now()"2026-01-22T14:30:00-05:00"
Date Arithmetic:
function description Example
addDays(date, days) Add/subtract days addDays("2026-01-22", 7)"2026-01-29"
addMonths(date, months) Add/subtract months addMonths(today(), 3) → 3 months from now
addHours(datetime, hours) Add/subtract hours addHours("10:00 AM", 2)"12:00 PM"
addMinutes(datetime, minutes)

Add/subtract minutes

addMinutes("2026-01-22T10:00:00-05:00", 45)"2026-01-22T10:45:00-05:00"
Date Components:
function description Example
year(date) Extract year year("2026-01-22")2026
month(date) Extract month (1-12) month("2026-01-22")1
day(date) Extract day (1-31) day("2026-01-22")22
Date Formatting & Validation:
function description Example
formatDate(date, pattern) Format date with pattern formatDate("2026-01-22", "MMM d, yyyy")"Jan 22, 2026"
isDate(value) Check if valid date isDate("2026-01-22")true
dateDiff(date1, date2, unit) Difference between dates dateDiff(today(), birthdate, "years") → age

Common format patterns: MM/dd/yyyy (default), MMMM d, yyyy, yyyy-MM-dd
dateDiff units: "days" (default), "months", "years", "hours", "minutes"

Format Preservation Examples:

// Date only (YYYY-MM-DD) → always returns date only
addDays("2026-01-22", 7)  // → "2026-01-29"
addMonths("2026-01-22", 3)  // → "2026-04-22"

// ISO timestamp (UTC) → returns ISO timestamp
addDays("2026-01-22T10:30:00Z", 7)  // → "2026-01-29T10:30:00Z"
addHours("2026-01-22T10:30:00Z", 2)  // → "2026-01-22T12:30:00Z"

// Timestamp with timezone → preserves timezone
addDays("2026-01-22T10:30:00-05:00", 7)  // → "2026-01-29T10:30:00-05:00"
addHours("2026-01-22T10:30:00-05:00", 2)  // → "2026-01-22T12:30:00-05:00"

// Time only (12-hour AM/PM) → returns time only
addHours("10:00 AM", 2)  // → "12:00 PM"
addMinutes("10:00 AM", 45)  // → "10:45 AM"
addMinutes("11:30 PM", 45)  // → "12:15 AM" (wraps to next day)

// Time calculations (time-only format)
dateDiff("2:00 PM", "10:00 AM", "hours")  // → 4
dateDiff("2:30 PM", "2:00 PM", "minutes")  // → 30

// Age calculation
dateDiff(today(), birthdate, "years")  // Age in years

// Date components work with all formats
year("2026-01-22")  // → 2026
year("2026-01-22T10:30:00Z")  // → 2026
month("2026-01-22")  // → 1 (January)
day("2026-01-22")  // → 22

// Formatting (always outputs as specified pattern)
formatDate("2026-01-22", "MMMM d, yyyy")  // → "January 22, 2026"
formatDate(appointmentDate, "EEEE, MMM d")  // → "Wednesday, Jan 22"

Random Functions

Seeded Random (Deterministic)

Seeded randomization functions produce the same result for the same seed, and should be used wherever reproducibility is required such as participant assignment.

function description Example
random.int(min, max, seed) Random integer in range random.int(1, 100, participantID) → same value per participant
random.pick(list, seed) Pick random element random.pick(["A", "B", "C"], participantID) → same choice per participant
random.float(seed) Random 0-1 decimal random.float(participantID) → same float per participant

Examples:

// Random assignment (reproducible)
random.int(0, 1, participantID) == 0 ? "Group A" : "Group B"
random.pick(["Control", "Treatment A", "Treatment B"], participantID)

// Weighted randomization
random.float(participantID) < 0.7 ? "Common" : "Rare"  // 70/30 split
Non-Seeded Random (Ephemeral)

Non-seeded randomization functions can produce a different value with each valuation.

Using non-seeded randomization produces a different value each time and is not reproducible, which may make it unsuitable for participant assignment.

function description Example
random.nowInt(min, max) Random integer (changes each time) random.nowInt(1, 6) → dice roll
random.nowPick(list) Pick random element (changes) random.nowPick(["A", "B"]) → different each time
random.nowFloat() Random 0-1 decimal (changes) random.nowFloat() → different each time

Examples:

// Random messages (OK for ephemeral use)
random.nowPick(["Great job!", "Keep it up!", "Well done!"])

// NOT for assignment
random.nowInt(0, 1)  // ❌ DON'T use for group assignment, unless you don't care about reproducibility

Survey Answers

A key feature of the MyDataHelps Common Expression Language is the ability to easily access survey answers. Any survey answer can easily be accessed by its resultidentifier, even if it's not in the current survey.

For instance:

// Access survey answer with result identifier "weight"
weight

// Use in expression
weight > 150

Accessing Survey Answers

Survey answers can be accessed in multiple ways depending on what is needed for this expression.

Current Survey Answers
Method 1: Direct Variable Access (Recommended)

If the result identifier is a valid variable name, you can access it directly:

// Simple access
painLevel

// In calculations
bmi >= 25

// In comparisons
bloodPressure == "120/80"
Method 2: surveyAnswers.getCurrent() and sa.c() shorthand

Use surveyAnswers.getCurrent() (or the sa.c() shorthand) when you need to get the answer for a specific event or when the result identifier isn't a valid variable name.

// Access current answer for a result identifier
surveyAnswers.getCurrent("resultIdentifier")

// Shorthand - same as above
sa.c("resultIdentifier")

// Access current answer for a specific event
sa.c("resultIdentifier", "eventName")

Examples:

// When identifier has special characters
sa.c("step-1-pain")               // Result identifier with hyphen

// When identifier starts with number
sa.c("123field")                  // Starts with number

// Specify event explicitly
sa.c("weight", "Week 1")         // Weight from Week 1 event

// Access step identifier
sa.c("weight", "Week 1")         // Step identifier for weight from Week 1 event
Current Answer Resolution Order

When accessing "current" answers, MyDataHelps follows this resolution order:

  1. Check Current Survey Context

    • If result identifier is in the current survey
    • AND (no event specified OR event matches current event)
    • THEN return answer from current survey context
  2. Check Current Event

    • If the survey is being run under an event AND no event was specified
    • THEN take the current answer (by instance) for the current event
  3. Return First Match

    • Take the current answer (by instance) for the latest event

Example Scenario:

A participant is completing a "Week 2" event survey with the answer `weight` = 155. Historical survey data exists for "Week 1" event with `weight` = 150 and "Week 3" with `weight` = 160

// Editing Week 2 survey:
weight                          // Returns: 155 (current survey)
sa.c("weight")                  // Returns: 155 (current survey)
sa.c("weight", "Week 1")        // Returns: 150 (historical)
sa.c("weight", "Week 3")        // Returns: 160 (future historical)

// After Week 2 survey submitted, no survey context:
weight                          // Returns: 160 (most recent = Week 3)
sa.c("weight")                  // Returns: 160
sa.c("weight", "Week 2")        // Returns: 155 (week 2)
All Answers Access

You may also access all past answers for a given question, not just the current one. Use surveyAnswers.getAll() (or the sa.a() shorthand) to access a list of survey answers.

// Get all answers for a result identifier
surveyAnswers.getAll("resultIdentifier")

// Get all answers for a result identifier
sa.a("resultIdentifier")

// Get all answers for a specific event
sa.a("resultIdentifier", "eventName")

This method returns an array of answer values. In most cases this is just a single value (e.g. an integer, decimal, string) depending on the type of question. In the case of multiple-choice questions or calculated fields that return lists, it may be an array of arrays.

Examples:

// Get all weight measurements
sa.a("weight")                    // Returns: all weight answer values

// Get all weight measurements from Week 1
sa.a("weight", "Week 1")         // Returns: Week 1 weight answer values

// Use with list functions
sa.a("weight").size()            // Count of weight measurements

// Returned values are primitives
sa.a("pain")    // Returns an array of pain answer values

// Multiple choice values return an array of arrays
sa.a("symptoms").filter(x, "Headache" in x)    // Filters answers to symptoms question to only those that contian "Headache"

Survey Answer Types

The answer property on a SurveyAnswer object, and any answers using direct variable access are automatically typed based on their question's answer format.

Those answers that are unable to be converted to their type will be null if referenced in direct variable access. However, surveyAnswers.getCurrent() and surveyAnswers.getAll() will contain a SurveyAnswer object (with a null answer). This may be used for detecting blank or null answers. Note that answers that are skipped by branching logic are not stored at all.

The type conversion happens when data is retrieved, and is based on the currently published version of the survey in your project.

Answerformat resulting type notes
Text string  
Numeric double must parse to an integer or double
Text Choice double, string, list If multiple choice, will be list. If all choices are numeric, value(s) will be double, otherwise they will all strings
Yes/No Choice boolean must parse to case-insensitive string "true" or "false", or be the number 1 or 0
Scale (Discrete) double  
Scale (Continuous) double  
Date string if "Date Only", is in ISO Date format, e.g. "2026-01-22". If "Date and Time", is full ISO timestamp with timezone offset, e.g. "2026-01-22T10:30:00-05:00"
Time of Day string in 12-hour format with AM/PM, e.g. "10:20 AM"
Value Picker double or string If all choices are numeric, value will be double, otherwise they will all strings
File Upload string  
Height double  
Weight double  
Phone Number string  
Postal Code string  
Date of Birth in ISO Date format, e.g. "2026-01-22"  
Email Address string  

 

Web View Steps always return strings.

Calculated fields will be converted the type of the field (e.g. if declared as a list of decimals).

Variables

CEL supports the following variables.

Variable Example Description
participantID `participantID` The ID of the participant (internal to MyDataHelps, automatically assigned)
participantIdentifier `participantIdentifier` The participant identifier (can be viewed from the Participants tab)
enrollmentDate  `enrollmentDate` The enrollment date of the participant
customFields `customFields.MyCustomField` Any custom field in your project — just change “MyCustomField” to your custom field name. For instance: customFields.SurgeryDate, customFields.Arm
demographics `demographics.firstName`

The demographic fields supported are:

  • email
  • mobilePhone
  • firstName
  • middleName
  • lastName
  • dateOfBirth
  • gender
  • preferredLanguage
  • street1
  • city
  • state
  • postalCode
  • utcOffset
  • timeZone
  • unsubscribedFromEmails
  • unsubscribedFromSms

institution

`institution.code` or `institution.name`

The Code or Name of the participant's assigned Institution.

project `project.description`

Information about the project. Supported fields are:

  • id
  • name
  • description
  • code
  • supportEmail
  • supportPhone
  • learnMoreLink
  • learnMoreTitle
organization `organization.name`

Information about the workspace. Supported fields are:

  • id
  • name
  • description
  • color
  • code, found in your workspace URL:28990841482643

 

surveyAnswers

sa.c("pain")

weight

Access to survey responses. Supports the following methods:

  • sa.c(id)
  • sa.c(id, event)
  • sa.a(id)
  • sa.a(id, event)

Common Examples

// Participant information
demographics.firstName + " " + demographics.lastName
dateDiff(today(), enrollmentDate, "days")  // Days enrolled

// Custom fields
customFields.age >= 18
customFields.studyGroup == "Treatment"

// Institution and project
institution.code == "01"
project.name + " - " + organization.name

// Survey answers
weight > 150
sa.c("pain", "Week 1") > 5
mean(sa.a("scores").map(x, int(x)))