Configuration (Common) Expression Language

Morf uses a small configuration language designed to enable powerful and type-safe configurations of Workflows, it’s called Common Expression Language (CEL for short).

In Morf we use it for many of the core features of the workflow builder, such as:

  • Controlling branching (filters) through boolean expressions.
  • Defining duration of pauses through expressions that return durations.
  • Computing calculated values like first_name + " " + last_name with simple operators and custom functions.

The syntax should feel familiar to developers. We use it because it enables us to easily define an ever-growing library of utility functions for use by Workflow users and allows us to type-check expressions for safety and debugging before usage.

Workflow Configuration Expression Syntax

Source events for workflows have defined object definitions. From these definitions we are able to build a Workflow Context which contains the core data in a Workflow Execution. Some examples of contexts would be things like, a Formsort Form Response (JSON payload) or a Healthie Appointment (fetched by Morf in response to receiving a Healthie Appointment webhook).

Fields from these objects are accessible in a CEL expression by simply referencing the field, for example:

appointment_datetime

This expression would yield the timestamp value associated with this event’s appointment_datetime field.

Nested objects are indexed using familiar .-syntax, for example in a Formsort Form Response:

answers.phone_number

Arrays of values can be indexed by position as follows:

ordered_answers[1]

or you can find the size of an array as follows:

ordered_answers.size()

Where safe indexing is provided with the special ? modifier described in the next section.

Optionals

Morf workflow configurations make explicit the difference between a value being not present (missing / undefined / null) and the empty or “default” value, for example "" for an empty string, 0 for an integer value.

Optionals subscribe to the “functor” typeclass interface, so you can map over these values.

Safe indexing into optionals is possible using the special ? modifier before the field being indexed into. For example for a Formsort Form Response event payload, where variables aren’t necessarily always defined for all steps of a Formsort flow, you could access the value safely as follows:

answers.?phone_number.orValue("phone number not defined")

Directly attempting to access an optional field which isn’t defined can yield a runtime error and potentially fail the execution of a Workflow action. If the field is defined, you can elide the ? check to yield the underlying value inside the optional.

Numericals

CEL supports only 64-bit integers and 64-bit IEEE double-precision floating-point.

Note that the integer 7 as an int is a different value than 7 as a uint, which would be written 7u. Double-precision floating-point is also supported, and the integer 7 would be written 7.0, 7e0, .700e1, or any equivalent representation using a decimal point or exponent.

Note that currently there are no automatic arithmetic conversions for the numeric types (int, uint, and double). The arithmetic operators typically contain overloads for arguments of the same numeric type, but not for mixed-type arguments. Therefore an expression like 1 + 1u is going to fail to dispatch. To perform mixed-type arithmetic, use explicit conversion functions such as uint(1) + 1u. Such explicit conversions will maintain their meaning even if arithmetic conversions are added in the future.

The following operators are supported:

OperatorDescription
- (unary)Negation
*Multiplication
/Division
%Remainder
+Addition
- (binary)Subtraction
== != < > <= >=Relations

Workflow Data Variables

Variables that are always available in the Workflow configuration environment at runtime regardless of the event payload data type or source.

morf_event_type

A string representation of the Source event type that triggered the workflow.

morf_event_type -> <string>

Examples:

  • "HEALTHIE_FORM_ANSWER_GROUP_CREATED"
  • "VITAL_LAB_ORDER_RESULTS_UPDATE"

morf_formatted_event_type

A string representation of the formatted Source event type that triggered the workflow, designed for display to the end user (for example in analytics rollups).

morf_formatted_event_type -> <string>

Examples:

  • "Healthie Form Answer Group Created"
  • "Vital Lab Order Results Update"

morf_object_type

The ID of the object type that triggered the workflow. This is the identifier for the Source third party object that triggered the workflow.

morf_object_type -> <string>

Examples:

  • "6123512"

morf_profile_ids

The matched Profiles associated third party IDs. When a Workflow discovers or creates a new Profile, its associated active and merged IDs are available with this object.

morf_profile_ids -> <object>

Fields of the object are enumerated below:

Examples:

  • morf_profile_ids.customer
This is an automatically generated V4 UUID identifier that Morf associates with a freshly created Profile.

Other third party IDs are given below:

  • morf_profile_ids.formsort
  • morf_profile_ids.healthie
  • morf_profile_ids.axle_health
  • morf_profile_ids.butterfly_labs
  • morf_profile_ids.recurly
  • morf_profile_ids.intercom
  • morf_profile_ids.sana_benefits
  • morf_profile_ids.active_campaign
  • morf_profile_ids.vital
  • morf_profile_ids.segment
  • morf_profile_ids.intakeq
  • morf_profile_ids.customer_io
  • morf_profile_ids.freshdesk
  • morf_profile_ids.hubspot

Each profile ID type is optional, because you may not store a value for each of the possible integrations. In order to check if the third party ID of your desired type is stored, you would access it as follows:

morf_profile_ids.healthie.id // returns the active Healthie ID on the profile, each third party type has this `id` field available
morf_profile_ids.healthie.?id.hasValue() // returns true or false depending on presence of the value
morf_profile_ids.healthie.merged_ids.size() // returns the count of merged Healthie IDs on the profile, e.g. 0
You may discover a Profile during a workflow’s profile lookup process using a merged ID, but the active id value will always represent the most recently updated Healthie User ID that Morf has processed.

morf_event_time

morf_event_time: timing.v1.Timestamp representation of the triggering event’s timestamp.

Profile Properties

Profile properties are accessible within CEL expressions using the following macro.

get_current_property_value

Gets the current value of a profile property for the given name.

get_current_property_value(<string>) -> <optional(any)>

Examples:

get_current_property_value("email_address") // returns optional("foo@gmail.com")

Event Time and Time Now

morf_event_time Returns the time of the event snapshot, i.e. the time the event that triggered the source to send Morf data occurred at.

morf_event_time -> <timing.v1.Timestamp>

morf.now() Returns the current time.

Prefer using a timestamp field of the event instead of this function to get a stable timestamp on retries.

morf.now() -> <timing.v1.Timestamp>

Examples:

morf.now() // returns timing.v1.Timestamp{Seconds: 1611840000, Nanoseconds: 0}

Strings

Functions that operate on values of the type string a UTF-8 string representation.

As a general note, all indices are zero-based.

charAt

Returns the character at the given position. If the position is negative, or greater than the length of the string, the function will produce an error:

<string>.charAt(<int>) -> <string>

Examples:

'hello'.charAt(4)  // return 'o'
'hello'.charAt(5)  // return ''
'hello'.charAt(-1) // error

emailAddSubaddress

Adds a subaddress to an email address.

<string>.emailAddSubaddress(<string>) -> <string>

Examples:

"foo@gmail.com".emailAddSubaddress("bar") // returns "foo+bar@gmail.com

format

Returns a new string with substitutions being performed, printf-style. The valid formatting clauses are:

  • %s - substitutes a string. This can also be used on bools, lists, maps, bytes, Duration and Timestamp, in addition to all numerical types (int, uint, and double). Note that the dot/period decimal separator will always be used when printing a list or map that contains a double, and that null can be passed (which results in the string “null”) in addition to types.

  • %d - substitutes an integer.

  • %f - substitutes a double with fixed-point precision. The default precision is 6, but this can be adjusted. The strings Infinity, -Infinity, and NaN are also valid input for this clause.

  • %e - substitutes a double in scientific notation. The default precision is 6, but this can be adjusted.

  • %b - substitutes an integer with its equivalent binary string. Can also be used on bools.

  • %x - substitutes an integer with its equivalent in hexadecimal, or if given a string or bytes, will output each character’s equivalent in hexadecimal.

  • %X - same as above, but with A-F capitalized.

  • %o - substitutes an integer with its equivalent in octal.

<string>.format(<list>) -> <string>

Examples:

"this is a string: %s\nand an integer: %d".format(["str", 42]) // returns "this is a string: str\nand an integer: 42"
"a double substituted with %%s: %s".format([64.2]) // returns "a double substituted with %s: 64.2"
"string type: %s".format([type(string)]) // returns "string type: string"
"timestamp: %s".format([timestamp("2023-02-03T23:31:20+00:00")]) // returns "timestamp: 2023-02-03T23:31:20Z"
"duration: %s".format([duration("1h45m47s")]) // returns "duration: 6347s"
"%f".format([3.14]) // returns "3.140000"
"scientific notation: %e".format([2.71828]) // returns "scientific notation: 2.718280\u202f\u00d7\u202f10\u2070\u2070"
"5 in binary: %b".format([5]), // returns "5 in binary; 101"
"26 in hex: %x".format([26]), // returns "26 in hex: 1a"
"26 in hex (uppercase): %X".format([26]) // returns "26 in hex (uppercase): 1A"
"30 in octal: %o".format([30]) // returns "30 in octal: 36"
"a map inside a list: %s".format([[1, 2, 3, {"a": "x", "b": "y", "c": "z"}]]) // returns "a map inside a list: [1, 2, 3, {"a":"x", "b":"y", "c":"d"}]"
"true bool: %s - false bool: %s\nbinary bool: %b".format([true, false, true]) // returns "true bool: true - false bool: false\nbinary bool: 1"

Passing an incorrect type (a string to %b) is considered an error, as well as attempting to use more formatting clauses than there are arguments (%d %d %d while passing two ints, for instance). If compile-time checking is enabled, and the formatting string is a constant, and the argument list is a literal, then letting any arguments go unused/unformatted is also considered an error.

indexOf

Returns the integer index of the first occurrence of the search string. If the search string is not found the function returns -1.

Currently, the function will throw an error if it is called on an empty string e.g. "".indexOf("foo"). This is a bug in the library, tracked here.

The function also accepts an optional position from which to begin the substring search. If the substring is the empty string, the index where the search starts is returned (zero or custom).

<string>.indexOf(<string>) -> <int>
<string>.indexOf(<string>, <int>) -> <int>

Examples:

'hello mellow'.indexOf('')         // returns 0
'hello mellow'.indexOf('ello')     // returns 1
'hello mellow'.indexOf('jello')    // returns -1
'hello mellow'.indexOf('', 2)      // returns 2
'hello mellow'.indexOf('ello', 2)  // returns 7
'hello mellow'.indexOf('ello', 20) // error

join

Returns a new string where the elements of string list are concatenated.

The function also accepts an optional separator which is placed between elements in the resulting string.

<list<string>>.join() -> <string>
<list<string>>.join(<string>) -> <string>

Examples:

['hello', 'mellow'].join() // returns 'hellomellow'
['hello', 'mellow'].join(' ') // returns 'hello mellow'
[].join() // returns ''
[].join('/') // returns ''

lastIndexOf

Returns the integer index at the start of the last occurrence of the search string. If the search string is not found the function returns -1.

The function also accepts an optional position which represents the last index to be considered as the beginning of the substring match. If the substring is the empty string, the index where the search starts is returned (string length or custom).

<string>.lastIndexOf(<string>) -> <int>
<string>.lastIndexOf(<string>, <int>) -> <int>

Examples:

'hello mellow'.lastIndexOf('')         // returns 12
'hello mellow'.lastIndexOf('ello')     // returns 7
'hello mellow'.lastIndexOf('jello')    // returns -1
'hello mellow'.lastIndexOf('ello', 6)  // returns 1
'hello mellow'.lastIndexOf('ello', -1) // error

lowerAscii

Returns a new string where all ASCII characters are lower-cased.

This function does not perform Unicode case-mapping for characters outside the ASCII range.

<string>.lowerAscii() -> <string>

Examples:

'TacoCat'.lowerAscii()      // returns 'tacocat'
'TacoCÆt Xii'.lowerAscii()  // returns 'tacocÆt xii'

regexReplaceAll

Returns the first string arguments’ segments matching the second string argument (a valid POSIX regular expression) with the third argument string.

<string>.regexReplaceAll(<regex>, <string>) -> <string>
regexReplaceAll(<string>, <regex>, <string>) -> <string>

Examples:

'foobar'.regexReplaceAll('[aeiou]', '*') // returns 'f**b*r'
regexReplaceAll('foobarbaz', 'ba', 'fo') // returns 'fooforfoz'

replace

Returns a new string based on the target, which replaces the occurrences of a search string with a replacement string if present. The function accepts an optional limit on the number of substring replacements to be made.

When the replacement limit is 0, the result is the original string. When the limit is a negative number, the function behaves the same as replace all.

<string>.replace(<string>, <string>) -> <string>
<string>.replace(<string>, <string>, <int>) -> <string>

Examples:

'hello hello'.replace('he', 'we')     // returns 'wello wello'
'hello hello'.replace('he', 'we', -1) // returns 'wello wello'
'hello hello'.replace('he', 'we', 1)  // returns 'wello hello'
'hello hello'.replace('he', 'we', 0)  // returns 'hello hello'
'hello hello'.replace('', '_')  // returns '_h_e_l_l_o_ _h_e_l_l_o_'
'hello hello'.replace('h', '')  // returns 'ello ello'

reverse

Returns a new string whose characters are the same as the target string, only formatted in reverse order. This function relies on converting strings to rune arrays in order to reverse

<string>.reverse() -> <string>

Examples:

'gums'.reverse() // returns 'smug'
'John Smith'.reverse() // returns 'htimS nhoJ'

split

Returns a list of strings split from the input by the given separator. The function accepts an optional argument specifying a limit on the number of substrings produced by the split.

When the split limit is 0, the result is an empty list. When the limit is 1, the result is the target string to split. When the limit is a negative number, the function behaves the same as split all.

<string>.split(<string>) -> <list<string>>
<string>.split(<string>, <int>) -> <list<string>>

Examples:

'hello hello hello'.split(' ')     // returns ['hello', 'hello', 'hello']
'hello hello hello'.split(' ', 0)  // returns []
'hello hello hello'.split(' ', 1)  // returns ['hello hello hello']
'hello hello hello'.split(' ', 2)  // returns ['hello', 'hello hello']
'hello hello hello'.split(' ', -1) // returns ['hello', 'hello', 'hello']

strings.quote

Introduced in version: 1

Takes the given string and makes it safe to print (without any formatting due to escape sequences). If any invalid UTF-8 characters are encountered, they are replaced with \uFFFD.

strings.quote(<string>)

Examples:

strings.quote(‘single-quote with “double quote”’) // returns ‘“single-quote with “double quote""’ strings.quote(“two escape sequences \a\n”) // returns ‘“two escape sequences \a\n“‘

stripPrefix

Removes a prefix from a string.

<string>.stripPrefix(<string>) -> <string>

Examples:

"foo_bar".stripPrefix("foo_") // returns "bar"

substring

Returns the substring given a numeric range corresponding to character positions. Optionally may omit the trailing range for a substring from a given character position until the end of a string.

Character offsets are 0-based with an inclusive start range and exclusive end range. It is an error to specify an end range that is lower than the start range, or for either the start or end index to be negative or exceed the string length.

<string>.substring(<int>) -> <string>
<string>.substring(<int>, <int>) -> <string>

Examples:

'tacocat'.substring(4)    // returns 'cat'
'tacocat'.substring(0, 4) // returns 'taco'
'tacocat'.substring(-1)   // error
'tacocat'.substring(2, 1) // error

titleCase

Converts a string to title case.

<string>.titleCase() -> <string>

Examples:

"hello_world".titleCase() // returns "Hello World"

trim

Returns a new string which removes the leading and trailing whitespace in the target string. The trim function uses the Unicode definition of whitespace which does not include the zero-width spaces. See: https://en.wikipedia.org/wiki/Whitespace_character#Unicode

<string>.trim() -> <string>

Examples:

'  \ttrim\n    '.trim() // returns 'trim'

upperASCII

Returns a new string where all ASCII characters are upper-cased.

This function does not perform Unicode case-mapping for characters outside the ASCII range.

<string>.upperAscii() -> <string>

Examples:

'TacoCat'.upperAscii()      // returns 'TACOCAT'
'TacoCÆt Xii'.upperAscii()  // returns 'TACOCÆT XII'

urlEncode

Encodes a string for use in a URL.

<string>.urlEncode() -> <string>

Examples:

"Hello World!".urlEncode() // returns "Hello+World%21"

Collections

Functions that operate on collections.

Lists or Maps (Objects / Dictionaries)

all

Tests whether a predicate holds for all elements of a list or keys of a map.

<list<T> or map>.all(expr -> <bool>) -> <bool>

Examples:

[1, 2, 3].all(x, x > 0) // returns true
{"foo": 1, "bar": 2}.all(k, k == "foo") // returns false

exists

Like all, but tests whether a predicate holds for any element.

<list<T> or map>.exists(expr -> <bool>) -> <bool>

Examples:

[1, 2, 3].exists(x, x > 2) // returns true
{"foo": 1, "bar": 2}.exists(k, k == "foo") // returns true

exists_one

Tests whether a predicate holds for exactly one element.

<list<T> or map>.exists_one(expr -> <bool>) -> <bool>

Examples:

[1, 2, 3].exists_one(x, x > 1) // returns false
{"foo": 1, "bar": 2}.exists_one(k, k == "foo") // returns true

map

Transforms a list by running an expression on each element. Or transforms a map by running an expression on each key and returning a list.

An optional second argument can be passed and runs as a filter.

<list<T> or map>.map(<T>, predicate -> bool, expr -> <U>) -> <list<U>>

Examples:

[1, 2, 3].map(x, x * 2) // returns [2, 4, 6]
{"foo": 1, "bar": 2}.map(k, k + "_test") // returns ["foo_test", "bar_test"]
[1, 2, 3].map(x, x > 1, x * 2) // returns [4, 6]
{"foo": 1, "bar": 2}.map(k, k == "foo", k + "_test") // returns ["foo_test"]

filter

Filters a list by running an expression on each element. Or filters a map by running an expression on each key and returning a list.

<list<T> or map>.filter(<T>, predicate -> bool) -> <list<T>>

Examples:

[1, 2, 3].filter(x, x > 1) // returns [2, 3]
{"foo": 1, "bar": 2}.filter(k, k == "foo") // returns ["foo"]

in

Checks if an element exists in a list. Or if a key exists in a map.

<T> in <list<T> or map> -> <bool>

Examples:

1 in [1, 2, 3] // returns true
"foo" in {"foo": 1, "bar": 2} // returns true

size

Returns the size of a list or map.

<list<T> or map>.size() -> <int>

Examples:

[1, 2, 3].size() // returns 3
{"foo": 1, "bar": 2}.size() // returns 2

Maps (Objects / Dictionaries)

flattenMaps

Flattens maps.

<list<map>>.flattenMaps() -> <map>

Examples:

form_answers.map(a, {a: form_answers[a].answer + "_test"}).flattenMaps()

merge

Merges 2 maps, the right-hand side map takes precedence.

<map>.merge(<map>) -> <map>

Examples:

map1.merge({"foo": 123, "baz": "rhsbaz"})

deleteKey

Deletes a key from a map.

<map>.deleteKey(<string>) -> <map>

Examples:

{"foo": 123, "baz": "rhsbaz"}.deleteKey("baz") // returns {"foo": 123}

Extra functions on other datatypes

Functions that we’ve added to CEL.

Timestamps and Civil Dates

parseTimestamp

Parses a timestamp string into the timestamp type.

Supports the RFC3339 format "2006-01-02T15:04:05Z07:00" or only the date "2006-01-02" by default, or the provided format is used.

We recommended reading https://pkg.go.dev/time#Layout if you have to define date/timestamp formats for displaying or parsing. It uses a reference date to construct a format rather than the more common approach YY/MM/dd HH:mm:ss.
<string>.parseTimestamp(<string>) -> <timing.v1.Timestamp>
<string>.parseTimestamp(<string>, <string>) -> <timing.v1.Timestamp>

Examples:

parseTimestamp("2023-02-03T23:31:20+00:00")
parseTimestamp("2023-02-03")
parseTimestamp("January 2, 2006", "February 12, 2025").customFormatInTimezone("2006-01-02", "UTC") // returns "2025-02-12"
parseTimestamp("01/02/2006 15:04:05 -0700", "03/15/2025 09:30:45 -0400").customFormatInTimezone("2006-01-02T15:04:05Z07:00", "America/New_York") // returns "2025-03-15T09:30:45-04:00"
parseTimestamp("2006-01-02T15:04:05Z07:00", "2025-04-20T18:22:31-04:00").customFormatInTimezone("2006-01-02T15:04:05Z07:00", "America/Los_Angeles") // returns "2025-04-20T15:22:31-07:00"

parseDuration

Parses a duration string into the duration type.

<string>.parseDuration() -> <duration>

Examples:

parseDuration("1h")

formatSimpleLocalDatetimeWithTimezone

Formats a v1.timing.Timestamp OR string value as a simple local datetime with timezone using the given ISO timezone location string. e.g. Monday, January 27th at 10:30am PST.

this method is also available on the string type, it expects the string to be in the RFC3339 format "2006-01-02T15:04:05Z07:00".
<v1.timing.Timestamp>.formatSimpleLocalDatetimeWithTimezone(<string>) -> <string>
// or
<string>.formatSimpleLocalDatetimeWithTimezone(<string>) -> <string>

Examples:

created_at.formatSimpleLocalDatetimeWithTimezone("PDT") // returns "Monday, January 27th at 10:30am PST"

customFormatInTimezone

Formats a v1.timing.Timestamp value as a string with the given format and ISO timezone.

<v1.timing.Timestamp>.customFormatInTimezone(<string>, <string>) -> <string>

Examples:

parseTimestamp("2023-02-03T23:31:20+00:00").customFormatInTimezone("Monday, 02-Jan-06 15:04:05 MST", "America/Los_Angeles") // returns "Friday, 03-Feb-23 15:31:20 PST"

getAge

Calculates the age, given a birth date (timing.v1.Timestamp OR values.v1.Date OR string value) on the left handside and an optional date on the right handside.

String arguments need to either be a date in the format "2006-01-02" or a timestamp string in RFC3339 format "2006-01-02T15:04:05Z07:00".
<values.v1.Date>.getAge(<values.v1.Date>) -> <int>
// or
<values.v1.Date>.getAge(<timing.v1.Timestamp>) -> <int>
// or
<values.v1.Date>.getAge(<string>) -> <int>
// or
<values.v1.Date>.getAge() -> <int>
// or
<timing.v1.Timestamp>.getAge(<values.v1.Date>) -> <int>
// or
<timing.v1.Timestamp>.getAge(<timing.v1.Timestamp>) -> <int>
// or
<timing.v1.Timestamp>.getAge(<string>) -> <int>
// or
<timing.v1.Timestamp>.getAge() -> <int>
// or
<string>.getAge(<values.v1.Date>) -> <int>
// or
<string>.getAge(<timing.v1.Timestamp>) -> <int>
// or
<string>.getAge(<string>) -> <int>
// or
<string>.getAge() -> <int>

Examples:

"1995-08-12".getAge(morf.now()) // returns 29

"1995-08-12".getAge(morf_event_time) // returns the age at the time of the event

nextWeekdayTime

Calculates the time at the next weekday after the first argument (timing.v1.Timestamp OR values.v1.Date OR string value) .

String arguments need to either be a date in the format "2006-01-02" or a timestamp in the RFC3339 format "2006-01-02T15:04:05Z07:00".

<values.v1.Date>.nextWeekdayTime(<string>,<string>,<string>) -> <timing.v1.Timestamp>
// or
<timing.v1.Timestamp>.nextWeekdayTime(<string>,<string>,<string>) -> <timing.v1.Timestamp>
// or
<string>.nextWeekdayTime(<string>,<string>,<string>) -> <timing.v1.Timestamp>

Examples:

"1983-11-13".nextWeekdayTime("Monday", "13:00:00", "America/Los_Angeles") // "1983-11-14T13:00:00-0800"

getWeekDay

Returns the day of the week as a string.

<timing.v1.Timestamp>.getWeekDay() -> <string>

Examples:

created_at.getWeekDay() // returns "Monday"

getYearDay

Returns the day of the year.

<timing.v1.Timestamp>.getYearDay() -> <int>

Examples:

created_at.getYearDay() // returns 143

getMilliseconds

Returns the UNIX epoch milliseconds of a timestamp.

<timing.v1.Timestamp>.getMilliseconds() -> <int>

Examples:

created_at.getMilliseconds() // returns 1611840000000

getDateString

Returns the date as a string. e.g. 2022-03-08

<timing.v1.Timestamp>.getDateString() -> <string>

Examples:

created_at.getDateString() // returns "2022-03-08"

formatTimeStringInTimezone

Formats a timestamp as a time string in the given timezone. e.g. 9:00PM.

<timing.v1.Timestamp>.formatTimeStringInTimezone(<string>) -> <string>

Examples:

created_at.formatTimeStringInTimezone("America/Denver") // returns "9:00PM"

formatDateStringInTimezone

Formats a timestamp as a date string in the given timezone. e.g. February 2, 2024.

<timing.v1.Timestamp>.formatDateStringInTimezone(<string>) -> <string>

Examples:

created_at.formatDateStringInTimezone("America/Denver") // returns "February 2, 2024"

abs_diff

Calculates the absolute duration difference between two timestamps.

<timing.v1.Timestamp>.abs_diff(<timing.v1.Timestamp>) -> <duration>

Examples:

created_at.abs_diff(updated_at) // returns duration("1h")

isAfter

Checks if a timestamp is after another timestamp.

<timing.v1.Timestamp>.isAfter(<timing.v1.Timestamp>) -> <bool>

Examples:

created_at.isAfter(updated_at) // returns false

isBefore

Checks if a timestamp is before another timestamp.

<timing.v1.Timestamp>.isBefore(<timing.v1.Timestamp>) -> <bool>

Examples:

created_at.isBefore(updated_at) // returns true

modDuration

Calculates the modulus of two durations.

<duration>.mod_duration(<duration>) -> <duration>

Examples:

parseDuration("30m").mod_duration(parseDuration("8m")) // returns duration("6m")

getYear

Returns the year of a timestamp/date.

<values.v1.Date>.getYear() -> <int>
// or
<timing.v1.Timestamp>.getYear() -> <int>

Examples:

date.getYear() // returns 2021

getMonth

Returns the month of a timestamp/date.

<values.v1.Date>.getMonth() -> <int>
// or
<timing.v1.Timestamp>.getMonth() -> <int>

Examples:

date.getMonth() // returns 1

getDayOfMonth

Returns the day of a timestamp/date.

<values.v1.Date>.getDayOfMonth() -> <int>
// or
<timing.v1.Timestamp>.getDayOfMonth() -> <int>

Examples:

date.getDayOfMonth() // returns 27

add

Adds a duration to a date.

<values.v1.Date>.add(<duration>) -> <values.v1.Date>
// or
<timing.v1.Timestamp>.add(<duration>) -> <timing.v1.Timestamp>

Examples:

date.add(parseDuration("1h"))

sub

Substracts a duration from a date.

<values.v1.Date>.sub(<duration>) -> <values.v1.Date>
// or
<timing.v1.Timestamp>.sub(<duration>) -> <timing.v1.Timestamp>

Examples:

date.sub(parseDuration("1h"))

toString

Formats a date as a string. e.g. 2022-03-08

<values.v1.Date>.toString() -> <string>

Examples:

date.toString()

Real Examples

ExampleDescription
staffer_users[?0].?properties.?healthie_dietician_id.orValue("451492")Get the first dietician ID from the staffer users, or return “451492” if it doesn’t exist.
answers.?current_step_id.orValue("") == "appointment-booking-inprogress" && answers.?scheduled_appt.?id.hasValue()Checks that the answers current_step_id value matches the expected, and that the scheduled_appt exists in the answers and has an id
!get_current_property_value("lifecyclestage").hasValue()Checks that the lifecyclestage property doesn’t have a value
entries.metric_entries.size() == 0Checks that the metric_entries array is empty
morf_event_type.contains("SIGNED")Checks that the morf_event_type contains the string “SIGNED”