> ## Documentation Index
> Fetch the complete documentation index at: https://docs.morf.health/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Common Expression Language (CEL)

> Customize your workflow configuration with CEL

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)](https://github.com/google/cel-spec).

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:

```go theme={null}
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:

```go theme={null}
answers.phone_number
```

Arrays of values can be indexed by position as follows:

```go theme={null}
ordered_answers[1]
```

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

```go theme={null}
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.

<Note>
  Optionals subscribe to the "functor" typeclass interface, so you can **map**
  over these values.
</Note>

<AccordionGroup>
  <Accordion title="Constructing an Optional value">
    `optional.of()` Wraps a given value in an optional.

    Examples:
  </Accordion>

  <Accordion title="Constructing a missing Optional value (None / `null`)">
    `optional.none` Returns the none value (represents "null").

    ```go theme={null}
    optional.none -> <optional.none>
    ```
  </Accordion>

  <Accordion title="Checking a value is specified at runtime">
    `optional.hasValue()` Returns a boolean at runtime, `true` if specified, `false` if `none` / `null`

    ```go theme={null}
    <optional(any)>.hasValue() -> <bool>
    ```

    Examples:

    ```go theme={null}
    optional.of("foo").hasValue() // returns true
    optional.none.hasValue() // returns false
    ```
  </Accordion>

  <Accordion title="Getting the value if specified with a backfup default value">
    `optional.orValue("default")` Returns the value if specified or the default value if not.

    <Note>
      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.
    </Note>

    ```go theme={null}
    <optional(any)>.orValue(<any>) -> <any>
    ```

    Examples:

    ```go theme={null}
    optional.of("foo").orValue("bar") // returns "foo"
    optional.none.orValue("bar") // returns "bar"
    ```
  </Accordion>

  <Accordion title="Mapping over an optional value">
    `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.

    ```go theme={null}
    <optional.optMap(variable binding : <any>, function : <any> -> <any>)> -> optional<any>
    ```

    Examples:

    ```go theme={null}
    optional.of("foo").optMap(value, value + "bar") // returns optional.of("foobar")
    optional.none.optMap(value, value + "bar") // returns optional.none
    ```
  </Accordion>

  <Accordion title="Chaining / sequencing operations on optional values">
    `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.

    ```go theme={null}
    <optional.optFlatMap(variable binding : <any>, function : <any> -> optional<any>)> -> optional<any>
    ```

    Examples:

    ```go theme={null}
    optional.of("foo").optMap(value, optional.of(value + "bar")) // returns optional.of("foobar")
    optional.none.optMap(value, value + "bar") // returns optional.none
    ```
  </Accordion>
</AccordionGroup>

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:

```go theme={null}
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.

<Warning>
  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.
</Warning>

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.

```go theme={null}
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).

```go theme={null}
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.

```go theme={null}
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.

```go theme={null}
morf_profile_ids -> <object>
```

Fields of the object are enumerated below:

Examples:

* `morf_profile_ids.customer`

<Note>
  This is an automatically generated V4 UUID identifier that Morf associates
  with a freshly created Profile.
</Note>

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:

```go theme={null}
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
```

<Note>
  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.
</Note>

### 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

```go theme={null}
get_current_property_value(<string>) -> <optional(any)>
```

Examples:

```go theme={null}
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.

```go theme={null}
morf_event_time -> <timing.v1.Timestamp>
```

`morf.now()` Returns the current time. <Warning>Prefer using a timestamp field of the event instead of this function to get a stable timestamp on retries.</Warning>

```go theme={null}
morf.now() -> <timing.v1.Timestamp>
```

Examples:

```go theme={null}
morf.now() // returns timing.v1.Timestamp{Seconds: 1611840000, Nanoseconds: 0}
```

`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.

```go theme={null}
getLookaheadOffset() -> <optional(duration)>
```

Examples:

```go theme={null}
// Returns the lookahead offset, or zero if none is configured
getLookaheadOffset().orValue(parseDuration("0s")) // returns duration(168h) when offset is 7 days
```

## Strings

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

<Note>As a general note, all indices are zero-based.</Note>

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

```go theme={null}
<string>.charAt(<int>) -> <string>
```

Examples:

```go theme={null}
'hello'.charAt(4)  // return 'o'
'hello'.charAt(5)  // return ''
'hello'.charAt(-1) // error
```

### emailAddSubaddress

Adds a subaddress to an email address.

```go theme={null}
<string>.emailAddSubaddress(<string>) -> <string>
```

Examples:

```go theme={null}
"foo@gmail.com".emailAddSubaddress("bar") // returns "foo+bar@gmail.com"
```

### titleCase

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

```go theme={null}
<string>.titleCase() -> <string>
```

Examples:

```go theme={null}
"hello_world".titleCase() // returns "Hello World"
"user_name".titleCase() // returns "User Name"
```

### stripPrefix

Removes the specified prefix from a string.

```go theme={null}
<string>.stripPrefix(<string>) -> <string>
```

Examples:

```go theme={null}
"hello_world".stripPrefix("hello_") // returns "world"
"prefix_text".stripPrefix("prefix_") // returns "text"
```

### urlEncode

URL encodes a string for safe use in URLs.

```go theme={null}
<string>.urlEncode() -> <string>
```

Examples:

```go theme={null}
"hello world".urlEncode() // returns "hello%20world"
"user@example.com".urlEncode() // returns "user%40example.com"
```

### regexReplaceAll

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

```go theme={null}
<string>.regexReplaceAll(<string>, <string>) -> <string>
```

Examples:

```go theme={null}
"hello 123 world 456".regexReplaceAll(`\d+`, "XXX") // returns "hello XXX world XXX"
"test-case".regexReplaceAll("-", "_") // returns "test_case"
```

### regexFindAll

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

```go theme={null}
<string>.regexFindAll(<string>) -> <list<string>>
```

Examples:

```go theme={null}
"hello 123 world 456".regexFindAll(`\d+`) // returns ["123", "456"]
"user@domain.com".regexFindAll(`\w+`) // returns ["user", "domain", "com"]
```

### zipCodeExists

Checks if a US zip code exists and is valid.

```go theme={null}
<string>.zipCodeExists() -> <bool>
```

Examples:

```go theme={null}
"90210".zipCodeExists() // returns true
"00000".zipCodeExists() // returns false
```

### toJsonString

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

```go theme={null}
toJsonString(<bool>, <any>) -> <string>
```

Examples:

```go theme={null}
toJsonString(true, {"name": "John", "age": 30}) // returns formatted JSON
toJsonString(false, {"name": "John"}) // returns compact JSON
```

### parseInt

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

```go theme={null}
<string>.parseInt() -> <int>
```

Examples:

```go theme={null}
"123".parseInt() // returns 123
"45.0".parseInt() // returns 45
```

### 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.

```go theme={null}
<string>.parseUrlQuery() -> <map<string, list<string>>>
```

Examples:

```go theme={null}
"https://example.com?foo=bar&baz=qux".parseUrlQuery() // returns {"foo": ["bar"], "baz": ["qux"]}
"https://example.com?tags=red&tags=green".parseUrlQuery() // returns {"tags": ["red", "green"]}
"?message=hello%20world&email=test%40example.com".parseUrlQuery() // returns {"message": ["hello world"], "email": ["test@example.com"]}
"https://example.com?flag&debug=true".parseUrlQuery() // returns {"flag": [""], "debug": ["true"]}
```

### 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.

```go theme={null}
<string>.format(<list>) -> <string>
```

Examples:

```go theme={null}
"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.

<Warning>
  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](https://github.com/google/cel-go/issues/1051).
</Warning>

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).

```go theme={null}
<string>.indexOf(<string>) -> <int>
<string>.indexOf(<string>, <int>) -> <int>
```

Examples:

```go theme={null}
'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.

```go theme={null}
<list<string>>.join() -> <string>
<list<string>>.join(<string>) -> <string>
```

Examples:

```go theme={null}
['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).

```go theme={null}
<string>.lastIndexOf(<string>) -> <int>
<string>.lastIndexOf(<string>, <int>) -> <int>
```

Examples:

```go theme={null}
'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.

```go theme={null}
<string>.lowerAscii() -> <string>
```

Examples:

```go theme={null}
'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.

```go theme={null}
<string>.regexReplaceAll(<regex>, <string>) -> <string>
regexReplaceAll(<string>, <regex>, <string>) -> <string>
```

Examples:

```go theme={null}
'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.

```go theme={null}
<string>.replace(<string>, <string>) -> <string>
<string>.replace(<string>, <string>, <int>) -> <string>
```

Examples:

```go theme={null}
'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

```go theme={null}
<string>.reverse() -> <string>
```

Examples:

```go theme={null}
'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.

```go theme={null}
<string>.split(<string>) -> <list<string>>
<string>.split(<string>, <int>) -> <list<string>>
```

Examples:

```go theme={null}
'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.

```go theme={null}
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.

```go theme={null}
<string>.stripPrefix(<string>) -> <string>
```

Examples:

```go theme={null}
"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.

```go theme={null}
<string>.substring(<int>) -> <string>
<string>.substring(<int>, <int>) -> <string>
```

Examples:

```go theme={null}
'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.

```go theme={null}
<string>.titleCase() -> <string>
```

Examples:

```go theme={null}
"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](https://en.wikipedia.org/wiki/Whitespace_character#Unicode)

```go theme={null}
<string>.trim() -> <string>
```

Examples:

```go theme={null}
'  \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.

```go theme={null}
<string>.upperAscii() -> <string>
```

Examples:

```go theme={null}
'TacoCat'.upperAscii()      // returns 'TACOCAT'
'TacoCÆt Xii'.upperAscii()  // returns 'TACOCÆT XII'
```

### urlEncode

Encodes a string for use in a URL.

```go theme={null}
<string>.urlEncode() -> <string>
```

Examples:

```go theme={null}
"Hello World!".urlEncode() // returns "Hello+World%21"
```

### randomNumbers

Generates `n` random digits as a string.

<Note>
  This function is not safe for cryptographic purposes.
</Note>

```go theme={null}
randomNumbers(<int>) -> <string>
```

Examples:

```go theme={null}
randomNumbers(6) // returns "492031"
randomNumbers(2) // returns "03"
```

### shorten\_link

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](https://nginx.org/en/):

```nginx theme={null}
server {
    listen 443 ssl;
    server_name links.mycompany.com;

    location /l {
        return 301 https://api.morf.healthcare$request_uri;
    }
}
```

```go theme={null}
// uses api.morf.healthcare as a default
<string>.shorten_link() -> <string>
// or with a custom hostname
<string>.shorten_link(<string>) -> <string>
```

Examples:

```go theme={null}
// Default hostname (api.morf.healthcare)
"https://my.very.long.url/to/be/shortened".shorten_link() // returns "https://api.morf.healthcare/l/aB1cD2"

// Custom hostname
"https://my.very.long.url/to/be/shortened".shorten_link("links.mycompany.com") // returns "https://links.mycompany.com/l/aB1cD2"
```

### uuidV4

Generates a random UUID v4 string.

```go theme={null}
uuidV4() -> <string>
```

Examples:

```go theme={null}
uuidV4() // returns a UUID like "1cbc1a49-09ad-4450-8a98-41c31a139873"
```

## 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.

```go theme={null}
<list<T> or map>.all(expr -> <bool>) -> <bool>
```

Examples:

```go theme={null}
[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.

```go theme={null}
<list<T> or map>.exists(expr -> <bool>) -> <bool>
```

Examples:

```go theme={null}
[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.

```go theme={null}
<list<T> or map>.exists_one(expr -> <bool>) -> <bool>
```

Examples:

```go theme={null}
[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.

```go theme={null}
<list<T> or map>.map(<T>, predicate -> bool, expr -> <U>) -> <list<U>>
```

Examples:

```go theme={null}
[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.

```go theme={null}
<list<T> or map>.filter(<T>, predicate -> bool) -> <list<T>>
```

Examples:

```go theme={null}
[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.

```go theme={null}
<T> in <list<T> or map> -> <bool>
```

Examples:

```go theme={null}
1 in [1, 2, 3] // returns true
"foo" in {"foo": 1, "bar": 2} // returns true
```

#### size

Returns the size of a list or map.

```go theme={null}
<list<T> or map>.size() -> <int>
```

Examples:

```go theme={null}
[1, 2, 3].size() // returns 3
{"foo": 1, "bar": 2}.size() // returns 2
```

### Lists

#### take

Takes the first n elements from a list.

```go theme={null}
<list<T>>.take(<int>) -> <list<T>>
```

Examples:

```go theme={null}
[1, 2, 3, 4, 5].take(3) // returns [1, 2, 3]
["a", "b", "c"].take(1) // returns ["a"]
```

#### drop

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

```go theme={null}
<list<T>>.drop(<int>) -> <list<T>>
```

Examples:

```go theme={null}
[1, 2, 3, 4, 5].drop(2) // returns [3, 4, 5]
["a", "b", "c"].drop(1) // returns ["b", "c"]
```

### Maps (Objects / Dictionaries)

#### flattenMaps

Flattens a list of maps into a single map. Later maps override earlier ones for duplicate keys.

```go theme={null}
<list<map>>.flattenMaps() -> <map>
```

Examples:

```go theme={null}
[{"a": 1}, {"b": 2}, {"a": 3}].flattenMaps() // returns {"a": 3, "b": 2}
form_answers.map(a, {a: form_answers[a].answer + "_test"}).flattenMaps()
```

#### merge

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

```go theme={null}
<map>.merge(<map>) -> <map>
```

Examples:

```go theme={null}
{"a": 1, "b": 2}.merge({"b": 3, "c": 4}) // returns {"a": 1, "b": 3, "c": 4}
map1.merge({"foo": 123, "baz": "rhsbaz"})
```

#### deleteKey

Deletes a key from a map.

```go theme={null}
<map>.deleteKey(<string>) -> <map>
```

Examples:

```go theme={null}
{"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.

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](https://pkg.go.dev/time#Layout) of the string to parse, and the second argument is the string to parse.

<Note>
  We recommended reading [https://pkg.go.dev/time#Layout](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`.{' '}
</Note>

```go theme={null}
parseTimestamp(<string>) -> <timing.v1.Timestamp>
parseTimestamp(<string>, <string>) -> <timing.v1.Timestamp>
```

Examples:

```go theme={null}
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"
```

#### 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](https://pkg.go.dev/time#Layout) of the string to parse, and the second argument is the string to parse.

<Note>
  We recommended reading [https://pkg.go.dev/time#Layout](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`.{' '}
</Note>

```go theme={null}
parseDate(<string>) -> <values.v1.Date>
parseDate(<string>, <string>) -> <values.v1.Date>
```

Examples:

```go theme={null}
parseDate("2023-02-03")
parseDate("02/01/06", "27/11/24") // returns values.v1.Date{Year: 2024, Month: 11, Day: 24}
```

#### parseDuration

Parses a duration string into the duration type.

```go theme={null}
<string>.parseDuration() -> <duration>
```

Examples:

```go theme={null}
parseDuration("1h")
```

#### formatDateUSStyle

Formats a `v1.values.Date` OR `v1.timing.Timestamp` OR `string` to a US style date string *MM/DD/YY*.

<Note>
  When used on a string, the only accepted input formats are `2006-01-02` or *RFC3339* format `"2006-01-02T15:04:05Z07:00"`.
</Note>

```go theme={null}
formatDateUSStyle(<v1.values.Date>) -> <string>
// or
formatDateUSStyle(<v1.timing.Timestamp>) -> <string>
// or
formatDateUSStyle(<string>) -> <string>
```

Examples:

```go theme={null}
formatDateUSStyle(created_at) // returns "09/20/25"
```

#### formatSimpleLocalDatetimeWithTimezone

Formats a `v1.timing.Timestamp` OR `string` value as a simple local datetime with timezone using the given [*ISO timezone*](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) location string. *e.g.* `Monday, January 27th at 10:30am PST`.

<Note>
  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"`.
</Note>

```go theme={null}
<v1.timing.Timestamp>.formatSimpleLocalDatetimeWithTimezone(<string>) -> <string>
// or
<string>.formatSimpleLocalDatetimeWithTimezone(<string>) -> <string>
```

Examples:

```go theme={null}
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](https://pkg.go.dev/time#Layout) and [*ISO timezone*](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).

```go theme={null}
<v1.timing.Timestamp>.customFormatInTimezone(<string>, <string>) -> <string>
```

Examples:

```go theme={null}
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"
```

#### customFormatWithTimezoneOffsetSeconds

Formats a `v1.timing.Timestamp` value as a string with the given [format](https://pkg.go.dev/time#Layout) 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.

```go theme={null}
<v1.timing.Timestamp>.customFormatWithTimezoneOffsetSeconds(<string>, <int>) -> <string>
```

Examples:

```go theme={null}
parseTimestamp("2024-11-27T00:00:00Z").customFormatWithTimezoneOffsetSeconds("2006-01-02 15:04:05", -25200) // returns "2024-11-26 17:00:00" (Pacific Standard Time, -7 hours = -25200 seconds)
```

#### 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.

<Note>
  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"`.
</Note>

```go theme={null}
<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:

```go theme={null}
"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"`.

```go theme={null}
<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:

```go theme={null}
"1983-11-13".nextWeekdayTime("Monday", "13:00:00", "America/Los_Angeles") // "1983-11-14T13:00:00-0800"
```

#### getDate

Returns the date of a timestamp.

```go theme={null}
<timing.v1.Timestamp>.getDate() -> <values.v1.Date>
```

Examples:

```go theme={null}
parseTimestamp("2023-02-03T23:31:20+00:00").getDate() // returns values.v1.Date{Year: 2023, Month: 2, Day: 3}
created_at.getDate()
```

#### getWeekDay

Returns the day of the week as a string.

```go theme={null}
<timing.v1.Timestamp>.getWeekDay() -> <string>
```

Examples:

```go theme={null}
created_at.getWeekDay() // returns "Monday"
```

#### getYearDay

Returns the day of the year.

```go theme={null}
<timing.v1.Timestamp>.getYearDay() -> <int>
```

Examples:

```go theme={null}
created_at.getYearDay() // returns 143
```

#### getMilliseconds

Returns the UNIX epoch milliseconds of a timestamp.

```go theme={null}
<timing.v1.Timestamp>.getMilliseconds() -> <int>
```

Examples:

```go theme={null}
created_at.getMilliseconds() // returns 1611840000000
```

#### getDateString

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

```go theme={null}
<timing.v1.Timestamp>.getDateString() -> <string>
```

Examples:

```go theme={null}
created_at.getDateString() // returns "2022-03-08"
```

#### formatTimeStringInTimezone

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

```go theme={null}
<timing.v1.Timestamp>.formatTimeStringInTimezone(<string>) -> <string>
```

Examples:

```go theme={null}
created_at.formatTimeStringInTimezone("America/Denver") // returns "9:00PM"
```

#### formatDateStringInTimezone

Formats a timestamp as a date string in the given [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). *e.g.* `February 2, 2024`.

```go theme={null}
<timing.v1.Timestamp>.formatDateStringInTimezone(<string>) -> <string>
```

Examples:

```go theme={null}
created_at.formatDateStringInTimezone("America/Denver") // returns "February 2, 2024"
```

#### abs\_diff

Calculates the absolute duration difference between two timestamps.

```go theme={null}
<timing.v1.Timestamp>.abs_diff(<timing.v1.Timestamp>) -> <duration>
```

Examples:

```go theme={null}
created_at.abs_diff(updated_at) // returns duration("1h")
```

#### isAfter

Checks if a timestamp is after another timestamp. Or if a date is after another date.

```go theme={null}
<timing.v1.Timestamp>.isAfter(<timing.v1.Timestamp>) -> <bool>
// or
<values.v1.Date>.isAfter(<values.v1.Date>) -> <bool>
```

Examples:

```go theme={null}
created_at.isAfter(updated_at) // returns false
```

#### isBefore

Checks if a timestamp is before another timestamp. Or if a date is before another date.

```go theme={null}
<timing.v1.Timestamp>.isBefore(<timing.v1.Timestamp>) -> <bool>
// or
<values.v1.Date>.isBefore(<values.v1.Date>) -> <bool>
```

Examples:

```go theme={null}
created_at.isBefore(updated_at) // returns true
```

#### modDuration

Calculates the modulus of two durations.

```go theme={null}
<duration>.mod_duration(<duration>) -> <duration>
```

Examples:

```go theme={null}
parseDuration("30m").mod_duration(parseDuration("8m")) // returns duration("6m")
```

#### getYear

Returns the year of a timestamp/date.

```go theme={null}
<values.v1.Date>.getYear() -> <int>
// or
<timing.v1.Timestamp>.getYear() -> <int>
```

Examples:

```go theme={null}
date.getYear() // returns 2021
```

#### getMonth

Returns the month of a timestamp/date.

```go theme={null}
<values.v1.Date>.getMonth() -> <int>
// or
<timing.v1.Timestamp>.getMonth() -> <int>
```

Examples:

```go theme={null}
date.getMonth() // returns 1
```

#### getDayOfMonth

Returns the day of a timestamp/date.

```go theme={null}
<values.v1.Date>.getDayOfMonth() -> <int>
// or
<timing.v1.Timestamp>.getDayOfMonth() -> <int>
```

Examples:

```go theme={null}
date.getDayOfMonth() // returns 27
```

#### add

Adds a duration to a date.

```go theme={null}
<values.v1.Date>.add(<duration>) -> <values.v1.Date>
// or
<timing.v1.Timestamp>.add(<duration>) -> <timing.v1.Timestamp>
```

Examples:

```go theme={null}
date.add(parseDuration("1h"))
```

#### sub

Substracts a duration from a date.

```go theme={null}
<values.v1.Date>.sub(<duration>) -> <values.v1.Date>
// or
<timing.v1.Timestamp>.sub(<duration>) -> <timing.v1.Timestamp>
```

Examples:

```go theme={null}
date.sub(parseDuration("1h"))
```

#### toString

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

```go theme={null}
<values.v1.Date>.toString() -> <string>
```

Examples:

```go theme={null}
date.toString()
```

### 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.

```go theme={null}
<string>.zipCodeToTimezone() -> <string>
```

Examples:

```go theme={null}
"90210".zipCodeToTimezone() // "America/Los_Angeles"

// or

zipCodeToTimezone("10010") // "America/New_York"
```

#### 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](https://simple.wikipedia.org/wiki/List_of_ZIP_Code_prefixes).

```go theme={null}
<string>.zipCodeToState() -> <string>
```

Examples:

```go theme={null}
"90210".zipCodeToState() // "CA"

// or

zipCodeToState("10010") // "NY"
```

## 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: `""`.

```go theme={null}
<string>.formatPhoneNumber(<string>) -> <string>
formatPhoneNumber(<string>, <string>) -> <string>
<phonenumbers.PhoneNumber>.formatPhoneNumber(<string>) -> <string>
formatPhoneNumber(<phonenumbers.PhoneNumber>, <string>) -> <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:**

```go theme={null}
// Basic E164 formatting
"(555) 123-4567".formatPhoneNumber("E164") // returns "+15551234567"

// International formatting
"555-123-4567".formatPhoneNumber("INTERNATIONAL") // returns "+1 555-123-4567"

// National formatting
"5551234567".formatPhoneNumber("NATIONAL") // returns "(555) 123-4567"

// RFC3966 formatting
"555.123.4567".formatPhoneNumber("RFC3966") // returns "tel:+1-555-123-4567"

// Function call style
formatPhoneNumber("(555) 123-4567", "E164") // returns "+15551234567"

// Creating a full international number by prepending +1
"+1 " + "555-123-4567".formatPhoneNumber("NATIONAL") // returns "+1 (555) 123-4567"
```

**International phone numbers:**

```go theme={null}
// UK number
"+44 20 7946 0958".formatPhoneNumber("E164") // returns "+442079460958"

// French number
"+33 1 42 86 83 26".formatPhoneNumber("INTERNATIONAL") // returns "+33 1 42 86 83 26"

// Japanese number
"+81 3-3242-4000".formatPhoneNumber("E164") // returns "+81332424000"
```

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

```go theme={null}
// Standardize phone numbers for storage
patient_phone.formatPhoneNumber("E164")

// Format for display in US format
patient_phone.formatPhoneNumber("NATIONAL")

// Create international display format with country code
"+1 " + patient_phone.formatPhoneNumber("NATIONAL")

// Format for tel: links
patient_phone.formatPhoneNumber("RFC3966")

// Chain with other operations
patient_phone.formatPhoneNumber("E164").size() > 10
```

## Real Examples

| Example                                                                                                                                                                                                                                                             | Description                                                                                                                                                                                                                                                            | Used in   |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| `staffer_users[?0].?properties.?healthie_dietician_id.orValue("451492")`                                                                                                                                                                                            | Gets the first dietician ID from the staffer users list, or returns the fallback ID "451492" if none is present.                                                                                                                                                       | Value     |
| `answers.?current_step_id.orValue("") == "appointment-booking-inprogress" && answers.?scheduled_appt.?id.hasValue()`                                                                                                                                                | Passes only when the form's current step matches the expected booking step AND a scheduled appointment ID was returned — confirms the booking completed successfully.                                                                                                  | Filter    |
| `!get_current_property_value("lifecyclestage").hasValue()`                                                                                                                                                                                                          | Passes only when the patient's lifecycle stage profile property has never been set. Useful for one-time onboarding workflows that should not re-run.                                                                                                                   | Filter    |
| `entries.metric_entries.size() == 0`                                                                                                                                                                                                                                | Passes only when a patient has no metric entries yet — use to trigger an initial data-collection step for new patients.                                                                                                                                                | Filter    |
| `morf_event_type.contains("SIGNED")`                                                                                                                                                                                                                                | Passes only when the Healthie event is a document-signing event (e.g., a provider signed a note or a patient signed a consent form).                                                                                                                                   | Filter    |
| `form_answers[?"21234567"].hasValue() ? morf.now().isBefore(parseTimestamp("2006-01-02", form_answers["21234567"].answer).add(parseDuration("96h"))) : true`                                                                                                        | Passes if the Healthie form answer for question "21234567" exists and is less than 96 hours old. Returns true (passes) if the answer doesn't exist yet, so the workflow proceeds on first run.                                                                         | Filter    |
| `!["Program Hold","Discharge","Ineligible","Lost","Test"].exists_one(grp, morf.get_current_property_value("user_group_name").orValue("").contains(grp))`                                                                                                            | Passes only when the patient's user group name does NOT match any of the excluded values. Use to prevent a workflow from running on discharged, ineligible, or test patients.                                                                                          | Filter    |
| `morf_profile_ids.intercom.?id.hasValue()`                                                                                                                                                                                                                          | Passes only when the patient already has an Intercom contact ID, ensuring downstream Intercom actions have a valid target.                                                                                                                                             | Filter    |
| `(current_step_id.orValue("") == "Insurance Details" \|\| finalized == true) && answers.?insurance_member_id.hasValue()`                                                                                                                                            | Passes when a Formsort responder has reached or completed the Insurance Details step AND provided an insurance member ID — use to trigger insurance verification mid-form.                                                                                             | Filter    |
| `get_current_property_value("phone_number").hasValue() \|\| get_current_property_value("email_address").hasValue()`                                                                                                                                                 | Passes when the patient has at least one contact method on file. Use before sending an outreach message to avoid no-op actions.                                                                                                                                        | Filter    |
| `(morf_event_type.contains("SCHEDULED") \|\| morf_event_type.contains("UPDATED")) && appointment_type_title.orValue("").contains("Prescriber Evaluation")`                                                                                                          | Passes when a Healthie appointment was booked or rescheduled AND the appointment type name includes "Prescriber Evaluation". Use to start a prescriber-specific workflow.                                                                                              | Filter    |
| `answers.?appointment_confirmed.orValue(false) && answers.?scheduled_appt.?id.hasValue()`                                                                                                                                                                           | Passes when a Formsort form response confirms the appointment and contains a valid appointment ID — distinguishes a completed booking from an abandoned or in-progress one.                                                                                            | Filter    |
| `morf_event_type.contains("SCHEDULED") && appointment_type_title.orValue("").contains("Initial")`                                                                                                                                                                   | Passes when a Healthie appointment was booked AND the appointment type contains "Initial" — use to trigger a new-patient onboarding workflow only on first appointments.                                                                                               | Filter    |
| `form_completed == true && step_id == "legal"`                                                                                                                                                                                                                      | Passes when a Formsort form was fully completed at the "legal" step — use to trigger actions only after legal consent is submitted, not on mid-form autosaves.                                                                                                         | Filter    |
| `appointment_notes.orValue("").indexOf("Zocdoc") >= 0 ? true : false`                                                                                                                                                                                               | Passes when the appointment notes contain the string "Zocdoc". Use to detect referral source from free-text appointment notes.                                                                                                                                         | Filter    |
| `profile_ids.?healthie.hasValue()`                                                                                                                                                                                                                                  | Passes only when the profile has a Healthie patient ID. Use to ensure the patient exists in Healthie before running Healthie actions.                                                                                                                                  | Filter    |
| `referring_physicians.size() > 0`                                                                                                                                                                                                                                   | Passes only when the Healthie patient has at least one referring physician on file. Use to gate actions that require a referring provider.                                                                                                                             | Filter    |
| `tags.exists_one(x, x == "Important")`                                                                                                                                                                                                                              | Passes only when the Calendly lead has a tag matching "Important".                                                                                                                                                                                                     | Filter    |
| `finalized`                                                                                                                                                                                                                                                         | Returns `true` when a Formsort form has been fully submitted. Use as a Filter condition to only proceed on complete responses, not partial autosaves.                                                                                                                  | Filter    |
| `appointment_datetime.sub(parseDuration("72h"))`                                                                                                                                                                                                                    | Returns a timestamp 72 hours before the appointment — use as the "wait until" time to fire a pre-appointment reminder.                                                                                                                                                 | Wait node |
| `appointment_datetime.add(parseDuration("90m"))`                                                                                                                                                                                                                    | Returns a timestamp 90 minutes after the appointment starts — use as the "wait until" time to fire a post-visit follow-up.                                                                                                                                             | Wait node |
| `morf_event_type.contains("NO_SHOW") ? morf.now() : morf.now().add(parseDuration("24h"))`                                                                                                                                                                           | Returns now if the appointment was a no-show (no delay), or 24 hours from now otherwise — use to fire a follow-up immediately on no-shows and the next day otherwise.                                                                                                  | Wait node |
| `appointments.filter(a, a.appointment_status == "Confirmed" && a.appointment_datetime.isAfter(morf.now()))[0].appointment_datetime`                                                                                                                                 | Returns the datetime of the first upcoming confirmed appointment from a list — use as the "wait until" time to delay until the patient's next appointment.                                                                                                             | Wait node |
| `staffer_users[?0].?properties.?healthie_dietician_id.orValue("451492")`                                                                                                                                                                                            | Gets the first dietician ID from the staffer users list, falling back to a default ID.                                                                                                                                                                                 | Value     |
| `appointment_first_name.orValue("") + " " + appointment_last_name.orValue("")`                                                                                                                                                                                      | Builds a full name string from the appointment's own first and last name fields. Note: the appointment may carry a different name than the profile (e.g., a parent booking for a child).                                                                               | Value     |
| `first_name.orValue("") + " " + last_name.orValue("")`                                                                                                                                                                                                              | Builds a full name string from the event's top-level first and last name fields (e.g., from a Healthie user event).                                                                                                                                                    | Value     |
| `get_current_property_value("first_name").orValue("") + " " + get_current_property_value("last_name").orValue("")`                                                                                                                                                  | Builds a full name from stored profile properties rather than event fields — use when the event may not carry name data directly.                                                                                                                                      | Value     |
| `appointment_id + "\|" + morf.get_current_property_value("session_id").orValue("")`                                                                                                                                                                                 | Builds a composite deduplication key from the appointment ID and session ID, separated by a pipe. Useful for idempotent event tracking in analytics systems.                                                                                                           | Value     |
| `double(form_answers["31111111"].answer)`                                                                                                                                                                                                                           | Converts the Healthie form answer for question ID "31111111" to a double (number). Required when passing a numeric form answer to a calculation or to an action field that expects a number.                                                                           | Value     |
| `scheduled_event.?cancellation.?reason.orValue("No cancellation reason")`                                                                                                                                                                                           | Returns the cancellation reason from a Calendly scheduled event, or the string "No cancellation reason" if none was provided.                                                                                                                                          | Value     |
| `email.lowerAscii()`                                                                                                                                                                                                                                                | Lowercases the email address from the event. Use when setting an email property to avoid duplicate profiles from case differences (e.g., in HubSpot or Intercom).                                                                                                      | Value     |
| `answers.?phone_number.or(answers.?phone_number_sp)`                                                                                                                                                                                                                | Returns the primary phone number answer if present, otherwise falls back to the Spanish-language equivalent field. Use when a form has both English and Spanish field variants.                                                                                        | Value     |
| `get_current_property_value("date_of_birth").optMap(d, formatDateUSStyle(d)).orValue("")`                                                                                                                                                                           | Formats the date-of-birth profile property in US date format (MM/DD/YYYY), defaulting to an empty string if absent.                                                                                                                                                    | Value     |
| `parseTimestamp(morf_event_time.add(parseDuration("720h")).getDateString() + "T00:00:00Z").getMilliseconds()`                                                                                                                                                       | Adds 30 days to the event time, snaps to midnight UTC, and returns the result in milliseconds. Use for HubSpot close date fields, which expect a millisecond timestamp.                                                                                                | Value     |
| `answers.?is_day_time_patient.optMap(val, val == 'true' ? 'No' : 'Yes')`                                                                                                                                                                                            | Inverts a boolean string form answer ("true"/"false") to a display label ("No"/"Yes"). Use when the field's logical meaning is the opposite of its stored value.                                                                                                       | Value     |
| `answers.?time_slot_appt_type_id.orValue("") == "382952" ? "Psychiatry" : answers.?time_slot_appt_type_id.orValue("") == "382864" ? "Psychotherapy" : "Other"`                                                                                                      | Maps a form answer appointment type ID to a human-readable label using chained ternary expressions. Use when downstream systems need a label rather than an internal ID.                                                                                               | Value     |
| `healthie_user.diagnoses.map(d, d.icd_code.?code.orValue("")).join(", ")`                                                                                                                                                                                           | Extracts all ICD codes from a Healthie patient's diagnoses array and joins them as a comma-separated string.                                                                                                                                                           | Value     |
| `healthie_user.?date_of_birth.optMap(dob, dob.getAge(morf.now()))`                                                                                                                                                                                                  | Computes the patient's current age in years from their Healthie date of birth.                                                                                                                                                                                         | Value     |
| `autoscored_sections.filter(s, s.section_title == "Total Score")[0].value`                                                                                                                                                                                          | Finds the autoscored section titled "Total Score" in a Healthie form response and returns its numeric value.                                                                                                                                                           | Value     |
| `morf.now().getDateString()`                                                                                                                                                                                                                                        | Returns today's date as an ISO string (e.g., "2024-01-15"). Use when setting a "last contacted" or "enrolled on" date property.                                                                                                                                        | Value     |
| `morf.now().add(parseDuration("24h")).getDateString()`                                                                                                                                                                                                              | Returns tomorrow's date as an ISO string. Use when setting a follow-up date property one day out.                                                                                                                                                                      | Value     |
| `answers.?drugs.orValue([]).filter(x, x != "Something else").join(", ") + (answers.?drugs_something_else.hasValue() ? ", " + answers.?drugs_something_else.orValue("") : "")`                                                                                       | Filters the "Something else" placeholder from a multi-select array, joins the real values, and appends the free-text override field if the patient filled it in.                                                                                                       | Value     |
| `morf_profile_ids.stripe.?id.hasValue() ? 1 : 0`                                                                                                                                                                                                                    | Returns 1 if the patient has a Stripe customer ID, 0 otherwise. Use for CRM systems that expect a number instead of a boolean.                                                                                                                                         | Value     |
| `form_answers[?"38844177"].answer.orValue("").regexReplaceAll("<[^>]+>", "").trim()`                                                                                                                                                                                | Strips HTML tags from a Healthie form answer and trims surrounding whitespace. Use when a rich-text answer needs to be stored or sent as plain text.                                                                                                                   | Value     |
| `morf_event_type.lowerAscii().stripPrefix("healthie_")`                                                                                                                                                                                                             | Converts a Healthie event type to lowercase and removes the "healthie\_" prefix, producing a clean event name for analytics (e.g., `healthie_APPOINTMENT_SCHEDULED` → `appointment_scheduled`).                                                                        | Value     |
| `answers.?sex.optMap(s, s.titleCase())`                                                                                                                                                                                                                             | Title-cases the sex field value from a form answer (e.g., "male" → "Male"). Use when the downstream system expects title-cased values.                                                                                                                                 | Value     |
| `morf.now().customFormatInTimezone("2006-01-02T15:04:05Z07:00", "UTC")`                                                                                                                                                                                             | Formats the current timestamp as an ISO 8601 string in UTC using Go-style layout. Use when an action field expects an exact datetime string format.                                                                                                                    | Value     |
| `active_tags.size() > 0 ? dyn(active_tags.join(";")) : dyn(null)`                                                                                                                                                                                                   | Joins an array of Healthie active tags into a semicolon-delimited string, or returns null if the array is empty. `dyn()` is needed because the field type varies between string and null.                                                                              | Value     |
| `step_id.replace("- ", "").replace(" ", "_") + "_submitted"`                                                                                                                                                                                                        | Normalizes a Formsort step ID to a snake\_case analytics event name by stripping dashes and replacing spaces (e.g., "Step 1 - Intake" → "Step\_1\_Intake\_submitted").                                                                                                 | Value     |
| `form_answers["15956392"].answer.split(" - ")[0]`                                                                                                                                                                                                                   | Extracts the first segment of a Healthie form answer that encodes multiple values separated by " - " (e.g., a score paired with a label).                                                                                                                              | Value     |
| `appointment_datetime.formatSimpleLocalDatetimeWithTimezone(healthie_user.timezone)`                                                                                                                                                                                | Formats an appointment timestamp in the patient's own stored timezone as a human-readable string (e.g., "Monday, March 3rd at 10:30am EST"). Use in reminder messages.                                                                                                 | Value     |
| `answers.merge({"event_timestamp_milliseconds": morf_event_time.getMilliseconds(), "is_final_form_response": finalized})`                                                                                                                                           | Adds computed fields to the Formsort answers map before passing it to downstream actions. Use to enrich the payload without modifying the original form data.                                                                                                          | Value     |
| `morf_event_payload.deleteKey("dietitian_email_address").deleteKey("has_created_password").deleteKey("money_owed")`                                                                                                                                                 | Strips sensitive Healthie fields from the event payload before forwarding it to an external system like an analytics pipeline or CRM.                                                                                                                                  | Value     |
| `get_current_property_value("insurance_type").orValue("[]").regexReplaceAll(r'\[\|\]\|"', '')`                                                                                                                                                                      | Converts a JSON-array-formatted profile property (e.g., `["PPO","HMO"]`) to a plain comma-separated string by stripping the bracket and quote characters.                                                                                                              | Value     |
| `toJsonString(false, providers)`                                                                                                                                                                                                                                    | Serializes a list or map to a compact JSON string. Use when an action field expects a JSON-encoded value rather than a native object.                                                                                                                                  | Value     |
| `form_answers.filter(a, form_answers[a].answer != "").map(a, {a: form_answers[a].answer}).flattenMaps()`                                                                                                                                                            | Collects all non-empty Healthie form answers into a single flat map of question ID to answer string. Use to forward a clean answer set to a downstream action.                                                                                                         | Value     |
| `appointment_add_to_cal_link.optMap(atcl, atcl.parseUrlQuery()[?"location"][?0]).orValue("")`                                                                                                                                                                       | Extracts the `location` query parameter from a Healthie appointment's calendar invite link URL.                                                                                                                                                                        | Value     |
| `primary_phone_number.optMap(x, x.formatPhoneNumber("E164"))`                                                                                                                                                                                                       | Formats an optional phone number in E.164 format (e.g., "+16175551234"), propagating absent if the field is missing. Use when sending SMS or storing a phone number for Twilio/Intercom.                                                                               | Value     |
| `has(hubspot_v1_get_contact_result_0.properties.referral_appointment_attended) && hubspot_v1_get_contact_result_0.properties.referral_appointment_attended != "" ? string(int(hubspot_v1_get_contact_result_0.properties.referral_appointment_attended) + 1) : "1"` | Increments a HubSpot string-typed counter field by reading it as an integer, adding 1, and converting back to string. Returns "1" if the property is not yet set. Use `has()` (not `?`) here because HubSpot properties are concrete struct fields, not CEL optionals. | Value     |
