Skip to main content
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.
Optionals subscribe to the “functor” typeclass interface, so you can map over these values.
optional.of() Wraps a given value in an optional.Examples:
optional.none Returns the none value (represents “null”).
optional.hasValue() Returns a boolean at runtime, true if specified, false if none / null
Examples:
optional.orValue("default") Returns the value if specified or the default value if not.
If the default value’s type is different to the optional’s value, the type information will be thrown away and become “dynamic” meaning typechecking may yield different results.
Examples:
optional.optMap(value, value + " example extra suffix text") Maps over a value, operating on the bound variable (e.g. value here), to yield a value within the optional result.
Examples:
optional.optFlatMap(value, optional.of("foo").optMap(x, + x + " example extra suffix text")) Maps over a value, operating on the bound variable (e.g. value here) with a function that also returns an optional value.
Examples:
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:

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
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.junction
  • morf_profile_ids.segment
  • morf_profile_ids.intakeq
  • morf_profile_ids.customer_io
  • morf_profile_ids.freshdesk
  • morf_profile_ids.hubspot
  • morf_profile_ids.stripe
  • morf_profile_ids.feathery
  • morf_profile_ids.open_phone
  • morf_profile_ids.elation
  • morf_profile_ids.athena
  • morf_profile_ids.posthog
  • morf_profile_ids.nextech
  • morf_profile_ids.medplum
  • morf_profile_ids.spruce
  • morf_profile_ids.zoho
  • morf_profile_ids.boulevard
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:
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 ID of that third party type 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

Fetches the current value of a patient profile property at the time this node executes.
  • Dynamic: The value is fetched fresh each time the node runs, not snapshotted at workflow start
  • Retry-aware: On node retries, this may return a different value if the profile was updated externally between attempts
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.
Prefer using a timestamp field of the event instead of this function to get a stable timestamp on retries.
Examples:
getLookaheadOffset() Returns the lookahead offset configured on the scheduled event producer that triggered this workflow, if any. Returns an optional duration — use .orValue(parseDuration("0s")) to fall back to a default when no offset is configured.
Examples:

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:
Examples:

emailAddSubaddress

Adds a subaddress to an email address.
Examples:

titleCase

Converts a string to title case, replacing underscores with spaces and capitalizing each word.
Examples:

stripPrefix

Removes the specified prefix from a string.
Examples:

urlEncode

URL encodes a string for safe use in URLs.
Examples:

regexReplaceAll

Replaces all matches of a regular expression with a replacement string.
Examples:

regexFindAll

Finds all matches of a regular expression in a string and returns them as a list.
Examples:

zipCodeExists

Checks if a US zip code exists and is valid.
Examples:

toJsonString

Converts a value to a JSON string representation. Supports pretty printing.
Examples:

parseInt

Parses a string to an integer. Falls back to parsing as float if direct integer parsing fails.
Examples:

parseUrlQuery

Parses a string as a URL and returns a map of query parameters where each key maps to a list of values. The values are automatically URL-decoded.
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 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.
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.
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).
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:

randomNumbers

Generates n random digits as a string.
This function is not safe for cryptographic purposes.
Examples:
Creates a shorter link to a URL. This is particularly useful when sending SMS messages to patients. The shortened URL will be of the form: https://api.morf.healthcare/l/aB1cD2. To use your own domain you need the following:
  1. A new subdomain created on your DNS configuration for short links. e.g. links.mycompany.com.
  2. A load balancer to redirect requests from your custom subdomain to api.morf.healthcare.
For example with "https://my.very.long.url/to/be/shortened".shorten_link("links.mycompany.com"), you would need a load balancer configured to redirect links.mycompany.com to api.morf.healthcare. Here is an example configuration for the popular tool nginx:
Examples:

uuidV4

Generates a random UUID v4 string.
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:

Lists

take

Takes the first n elements from a list.
Examples:

drop

Drops the first n elements from a list, returning the remaining elements.
Examples:

Maps (Objects / Dictionaries)

flattenMaps

Flattens a list of maps into a single map. Later maps override earlier ones for duplicate keys.
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. If only one argument is provided, the string is expected to be in the RFC3339 format "2006-01-02T15:04:05Z07:00" or the following date format "2006-01-02". If two arguments are provided, the first argument is the Golang format of the string to parse, and the second argument is the string to parse.
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.
Examples:

parseDate

Parses a date string into the date type. If only one argument is provided, the string is expected to be in the "2006-01-02" format. If two arguments are provided, the first argument is the Golang format of the string to parse, and the second argument is the string to parse.
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.
Examples:

parseDuration

Parses a duration string into the duration type.
Examples:

formatDateUSStyle

Formats a v1.values.Date OR v1.timing.Timestamp OR string to a US style date string MM/DD/YY.
When used on a string, the only accepted input formats are 2006-01-02 or RFC3339 format "2006-01-02T15:04:05Z07:00".
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.
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".
Examples:

customFormatInTimezone

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

customFormatWithTimezoneOffsetSeconds

Formats a v1.timing.Timestamp value as a string with the given format and timezone offset in seconds east of UTC. This function is useful when you have a numeric timezone offset in seconds rather than a named timezone, for example on a Healthie Appointment event payload.
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.
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".
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:

getDate

Returns the date of a timestamp.
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. Or if a date is after another date.
Examples:

isBefore

Checks if a timestamp is before another timestamp. Or if a date is before another date.
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:

Zip Codes (and Timezones)

zipCodeToTimezone

Parses a 5-digit US zipcode as a IANA database timezone, e.g. America/New_York. Note: most EHRs that accept timezones accept this standard form of timezone.
Examples:

zipCodeToState

Parses the first three characters of a US zipcode, e.g. 902 from 90210 and returns the upper-cased 2-letter US state abbreviation, for example "CA". See example zip code mappings listed here.
Examples:

Phone Numbers

Functions that operate on phone number strings to format and validate them using international phone number standards.

formatPhoneNumber

Formats a phone number value or a phone number string according to the specified format. Supports both US and international phone numbers. This function will error if passed a phone number string which is invalid - for example an empty string: "".
Supported formats:
  • "E164" - International format without separators (e.g., +15551234567)
  • "INTERNATIONAL" - International format with separators (e.g., +1 555-123-4567)
  • "NATIONAL" - National format for the country (e.g., (555) 123-4567)
  • "RFC3966" - RFC3966 standard format (e.g., tel:+1-555-123-4567)
Examples:
International phone numbers:
Error handling: The function will return an error if:
  • The input string is not a valid phone number
  • The phone number cannot be parsed
Common use cases:

Real Examples