Type matching
Twilio Cohorts filter criteria accept the following primitive types:
| Type | Description | Example |
|---|---|---|
string | Text value | "hello", "US" |
integer | Whole number (64-bit signed) | 0, 25, -1 |
double | Floating-point number (64-bit) | 3.14, -0.5 |
boolean | Boolean value | true, false |
timestamp | Date-time value | timestamp("2026-01-01T00:00:00Z") |
duration | Time duration | duration("24h"), duration("30m") |
When you combine two values in filter criteria, both sides generally need to be the same type.
| Operator | Allowed operand types |
|---|---|
==, != | Any type, provided that both operands are the same type — except for integer and double, which are numerically coerced when compared. |
<, <=, >, >= | integer, double, string, timestamp, or duration. Both operands must be the same type — except for integer and double, which are numerically coerced when compared. |
profile.trait.Account.age > 18
age and 18 are both numbers, so this comparison is valid. Comparing a string-typed trait to an integer literal, or a timestamp to a string, is invalid and causes an evaluation failure.
| Operator | Allowed operand types |
|---|---|
+ | integer or double + integer or double, string + string (concatenation), or timestamp + duration. |
- | integer or double - integer or double, timestamp - duration, or timestamp - timestamp. |
*, / | integer or double, combined with integer or double. |
% | integer % integer only. |
integer and double values can be mixed in +, -, *, and /, but not in %.
Mixed types, such as a string plus an integer, are invalid.
The left-hand value's type must match the type of the elements in the list on the right.
profile.trait.Contact.country in ["US", "FR", "DE"]
Comparing a string-typed trait to a list of integers is invalid. Unlike the == operator, this comparison does not coerce integer or double values. Ensure that each element in the list shares the trait's data type, or explicitly cast the elements. For example, a trait of type double is compatible with [1.0, 2.0] or [double(1), double(2)].
Both branches of a ?: expression must evaluate to the same type.
profile.trait.Metrics.score > 0 ? profile.trait.Metrics.score : 0.0
In this example, score is a double, so the value in the right-hand branch must also be a double. Like the in operator, the conditional operator does not coerce integer and double values. Make sure that the expressions in both branches share the same data type, or cast one branch explicitly.
A type mismatch behaves differently from a missing trait. A missing trait evaluates to false and quietly excludes the profile. A type mismatch fails validation when you create or update a cohort or create a snapshot from inline criteria. In those cases, the request is rejected with an HTTP 400 Bad Request error.
If you need to combine or compare values of different types, convert one of them explicitly by using a type-conversion function such as string(), int(), or double().
string(profile.trait.Account.age) == "18"
If a trait is stored as a string but represents a date (for example, 2026-01-01T00:00:00Z), convert it to a timestamp before you compare it with now() or with another timestamp. The string must use RFC 3339 format.
timestamp(profile.trait.Account.lastPurchaseDate) > timestamp("2026-01-01T00:00:00Z")
If the trait is already of type timestamp, compare it directly without wrapping it in timestamp().
Some numeric and timestamp operations behave differently from the standard CEL specification.
When you convert a double to a string with string(), whole numbers keep a decimal point, so string(1.0) returns "1.0" rather than "1". Very large and very small numbers use scientific notation, such as "1.0E7" for 10000000.0.
Because the text format of a number can vary, don't compare the result of string() on a number to a string literal. Compare the number itself instead.
profile.trait.Game.score == 1
The same applies to math.least() and math.greatest() when you give them a mix of integers and doubles. The result is a double, so string(math.least(1, 2.5)) returns "1.0".
string() on integers, booleans, and strings works as the CEL specification describes.
When you compare a double trait to an integer literal, the literal is converted to a double before the comparison. A double can't represent every integer above 253 (9,007,199,254,740,992), so neighboring integers beyond that point can compare as equal. For example, this expression is true:
9007199254740992.0 == 9007199254740993
Avoid large integer literals
Don't compare double traits to integer literals larger than 253
(about 9 quadrillion). The results might include or skip profiles you don't
expect.
The timestamp() function keeps millisecond precision. Digits beyond the third fractional digit are dropped, so two timestamps that differ only below the millisecond are treated as equal.
When you convert a timestamp to a string with string(), the result is in UTC with a trailing Z. Trailing zeros are removed from the fractional seconds, and the fraction is omitted when it's zero. For example, string(timestamp("2024-01-01T10:00:00+02:00")) returns "2024-01-01T08:00:00Z".
- Review the available functions, including type conversion.
- Learn about null handling for missing traits.