Configuration
Workflow action configuration (common) expression language
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:
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:
Arrays of values can be indexed by position as follows:
or you can find the size of an array as follows:
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.
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:
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:
Operator | Description |
---|---|
- (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.
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).
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.
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.
Fields of the object are enumerated below:
Examples:
morf_profile_ids.customer
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:
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.
Examples:
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.now()
Returns the current time.
Examples:
Strings
Functions that operate on values of the type string
a UTF-8 string representation.
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:
Examples:
emailAddSubaddress
Adds a subaddress to an email address.
Examples:
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 stringsInfinity
,-Infinity
, andNaN
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.
Examples:
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.
"".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).
Examples:
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.
Examples:
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).
Examples:
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.
Examples:
regexReplaceAll
Returns the first string arguments’ segments matching the second string argument (a valid POSIX regular expression) with the third argument string.
Examples:
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.
Examples:
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
Examples:
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.
Examples:
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.
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.
Examples:
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.
Examples:
titleCase
Converts a string to title case.
Examples:
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
Examples:
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.
Examples:
urlEncode
Encodes a string for use in a URL.
Examples:
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.
Examples:
exists
Like all
, but tests whether a predicate holds for any element.
Examples:
exists_one
Tests whether a predicate holds for exactly one element.
Examples:
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.
Examples:
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.
Examples:
in
Checks if an element exists in a list. Or if a key exists in a map.
Examples:
size
Returns the size of a list or map.
Examples:
Maps (Objects / Dictionaries)
flattenMaps
Flattens maps.
Examples:
merge
Merges 2 maps, the right-hand side map takes precedence.
Examples:
deleteKey
Deletes a key from a map.
Examples:
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.
YY/MM/dd HH:mm:ss
. Examples:
parseDuration
Parses a duration string into the duration type.
Examples:
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
.
string
type, it expects the string to be in the RFC3339 format "2006-01-02T15:04:05Z07:00"
.Examples:
customFormatInTimezone
Formats a v1.timing.Timestamp
value as a string with the given format and ISO timezone.
Examples:
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.
"2006-01-02"
or a timestamp string in RFC3339 format "2006-01-02T15:04:05Z07:00"
.Examples:
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"
.
Examples:
getWeekDay
Returns the day of the week as a string.
Examples:
getYearDay
Returns the day of the year.
Examples:
getMilliseconds
Returns the UNIX epoch milliseconds of a timestamp.
Examples:
getDateString
Returns the date as a string. e.g. 2022-03-08
Examples:
formatTimeStringInTimezone
Formats a timestamp as a time string in the given timezone. e.g. 9:00PM
.
Examples:
formatDateStringInTimezone
Formats a timestamp as a date string in the given timezone. e.g. February 2, 2024
.
Examples:
abs_diff
Calculates the absolute duration difference between two timestamps.
Examples:
isAfter
Checks if a timestamp is after another timestamp.
Examples:
isBefore
Checks if a timestamp is before another timestamp.
Examples:
modDuration
Calculates the modulus of two durations.
Examples:
getYear
Returns the year of a timestamp/date.
Examples:
getMonth
Returns the month of a timestamp/date.
Examples:
getDayOfMonth
Returns the day of a timestamp/date.
Examples:
add
Adds a duration to a date.
Examples:
sub
Substracts a duration from a date.
Examples:
toString
Formats a date as a string. e.g. 2022-03-08
Examples:
Real Examples
Example | Description |
---|---|
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() == 0 | Checks that the metric_entries array is empty |
morf_event_type.contains("SIGNED") | Checks that the morf_event_type contains the string “SIGNED” |