# Expressions reference

> Complete reference for Kustomer's expression languages. Covers every /#fn function, every Handlebars helper with its arguments and defaults, and the criteria format and operators used by queue rules and satisfaction surveys.

Source: https://developer.kustomer.com/docs/workflows/expressions-reference

Kustomer has three small expression languages. This page lists everything in each.

| Language                                  | Looks like                                                                          | Used in                                                     |
| ----------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| [Functions](#functions)                   | `"/#fn:parseInt,steps.1.custom.qtyStr"`                                             | Workflow params and conditions. Returns typed values        |
| [Handlebars helpers](#handlebars-helpers) | `"{{{dateFormat steps.1.createdAt format='LL'}}}"`                                  | Any workflow string that isn't a reference. Returns strings |
| [Criteria](#criteria)                     | `{ "and": [{ "conversation_status": { "operator": "equals", "value": "open" } }] }` | Queue rules and satisfaction surveys                        |

For how references, templates and conditions fit together, start with [Conditions and variables](https://developer.kustomer.com/docs/workflows/conditions-and-expressions).

## Functions

A function call is a reference that starts with `/#fn:`. The parts are separated by commas, with no spaces: the function name, a path to the input value, then any arguments.

```text
/#fn:<name>,<path>,<arg1>,<arg2>
```

Because commas separate the parts, arguments can't contain commas (`dateFormat`'s format is the one exception). The path is resolved like any reference, so `steps.1.custom.qtyStr` means `/#steps.1.custom.qtyStr`. Write it as a bare path: `/#fn:parseInt,steps.1.custom.qtyStr`, not `/#fn:parseInt,/#steps.1.custom.qtyStr`.

Like any reference, a function call only works as the whole value of a param or condition operand. A function name that doesn't exist doesn't fail: it silently returns the input value unchanged, so check spelling carefully.

| Function                        | Example                                                                                           | Returns                                                                                                                                          |
| ------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `parseInt`                      | `/#fn:parseInt,steps.1.custom.qtyStr`                                                             | An integer. `0` if the input is empty or not a number                                                                                            |
| `parseFloat`                    | `/#fn:parseFloat,steps.1.attributes.data.total`                                                   | A number. `0` if the input is empty or not a number                                                                                              |
| `parseBool`                     | `/#fn:parseBool,steps.1.attributes.data.vip`                                                      | `true` for `true` or `"true"` (any case), otherwise `false`                                                                                      |
| `parseJSON`                     | `/#fn:parseJSON,steps.call.body`                                                                  | The parsed object. `{}` if the string isn't valid JSON                                                                                           |
| `dateFormat`                    | `/#fn:dateFormat,steps.1.createdAt,YYYY-MM-DD`                                                    | The date in a [Moment.js format](https://momentjs.com/docs/#/displaying/format/). The default is ISO 8601. `undefined` if the input isn't a date |
| `slice`                         | `/#fn:slice,steps.1.tags,0,3`                                                                     | Part of an array or string, like JavaScript's `slice(start, end)`                                                                                |
| `map`                           | `/#fn:map,steps.lookup.emails,email`                                                              | Pulls one property out of every item in an array: `["jane@example.com", …]`                                                                      |
| `transform`                     | `/#fn:transform,steps.call.body.items,name=title,sku=id`                                          | Each array item reshaped: the new key `name` takes the old key `title`, and so on                                                                |
| `cond`                          | `/#fn:cond,steps.1.status,open=Waiting,done=Resolved`                                             | The value for the matching key, or `undefined`. Matches strings exactly, so it doesn't work on numbers                                           |
| `ifExists`                      | `/#fn:ifExists,steps.lookup.id,/#steps.lookup.name`                                               | The second expression if the path has a value, otherwise `undefined`, so the param is left out                                                   |
| `URIEncode`                     | `/#fn:URIEncode,steps.1.attributes.data.email`                                                    | `encodeURIComponent` of the value                                                                                                                |
| `URIDecode`                     | `/#fn:URIDecode,steps.1.attributes.data.redirect`                                                 | `decodeURIComponent` of the value                                                                                                                |
| `isValidEmailAddress`           | `/#fn:isValidEmailAddress,steps.1.attributes.data.email`                                          | `true` or `false`                                                                                                                                |
| `isValidPhoneNumber`            | `/#fn:isValidPhoneNumber,steps.1.attributes.data.phone`                                           | `true` if the number can be formatted as a valid phone number                                                                                    |
| `findIncludedResourceAttribute` | `/#fn:findIncludedResourceAttribute,steps.call.body.included,type=order,id=4417,attributes.total` | From a JSON:API `included` array, the property of the first resource matching every `key=value`                                                  |
| `inBusinessHours`               | `/#fn:inBusinessHours,steps.1.createdAt,64f1c0ffee5ca1ab1e0000aa`                                 | Whether the date falls within a [business schedule](https://developer.kustomer.com/api/core-resources/create-a-business-schedule). See below     |

### inBusinessHours

| Form                                             | Checks                                                                       |
| ------------------------------------------------ | ---------------------------------------------------------------------------- |
| `/#fn:inBusinessHours,<scheduleId>`              | Now, against that schedule                                                   |
| `/#fn:inBusinessHours,<datePath>,<scheduleId>`   | The date at the path, against that schedule                                  |
| `/#fn:inBusinessHours,<datePath>,<schedulePath>` | The date, against the schedule ID found at a second path, such as a variable |
| `/#fn:inBusinessHours,<datePath>`                | The date, against the org's default schedule                                 |

Holidays on the schedule count as closed. If there's no schedule to check against, or the date isn't valid, the result is `true`.

```json title="Route differently out of hours"
{
  "target": "after-hours-reply",
  "condition": { "op": "neq", "values": ["/#fn:inBusinessHours,steps.1.createdAt,64f1c0ffee5ca1ab1e0000aa", true] }
}
```

## Handlebars helpers

Use helpers inside templates, with positional arguments first and then `key=value` options. Wrap them in triple braces, `{{{ }}}`, so the output isn't HTML-escaped. Arguments can be paths (`steps.1.createdAt`), quoted strings or numbers.

### Dates

| Helper          | Example                                                                                       | Output                                                                                                                                          |
| --------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `date`          | `{{{date 'hours' count=24}}}`                                                                 | ISO timestamp 24 hours **ago**. Units: `minutes`, `hours`, `days`, `years`, singular or plural                                                  |
| `date` (future) | `{{{date 'days' count=-3}}}`                                                                  | ISO timestamp 3 days **from now**. Use a negative count                                                                                         |
| `dateFormat`    | `{{{dateFormat steps.1.createdAt format='MMMM Do YYYY, h:mma' timezone='America/New_York'}}}` | `October 21st 2026, 11:20am`                                                                                                                    |
| `dateDiff`      | `{{{dateDiff 'hours' from=steps.1.createdAt to=steps.1.lastDone.createdAt}}}`                 | Whole units from `from` to `to`. Units: `years`, `quarters`, `months`, `weeks`, `days` (default), `hours`, `minutes`, `seconds`, `milliseconds` |

`dateFormat` options: `format` (Moment.js, default `LL`, such as "October 21, 2026"), `locale` (default `en-us`), `inputTimezone` and `timezone` (output; IANA names or `UTC`). It accepts ISO strings and Unix timestamps in milliseconds, and outputs an empty string for invalid dates.

> **Schedule for later with a negative count:** `date` counts backwards from now. `{{{date 'hours' count=24}}}` is 24 hours **ago**, which is a common mistake when scheduling. To compute an absolute `runAt` for the [Scheduler](https://developer.kustomer.com/docs/workflows/triggers-and-actions#schedule-a-workflow), use a negative count: `"value": "{{{date 'hours' count=-2}}}"` is two hours from now. For a fixed delay, a relative `runAt` in seconds is simpler.

### Signing and encoding

| Helper         | Example                                                                                                               | Output                                                                                                                                                    |
| -------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `authHMAC`     | `{{{authHMAC steps.1.id secretKey=variables.6513b2f4a4b1e1001a6b3f24 algorithm='sha256' encoding='hex'}}}`            | HMAC of the first argument. `algorithm`: `sha1`, `sha224`, `sha256` (default), `sha384`, `sha512`. `encoding`: `base64` (default), `hex`, `utf8`, `ascii` |
| `base64Encode` | `{{#base64Encode}}{{{variables.6513b2f4a4b1e1001a6b3f22}}}:{{{variables.6513b2f4a4b1e1001a6b3f23}}}{{/base64Encode}}` | Base64 of the block's content. Add `uriEncode="true"` to URL-encode the result                                                                            |
| `base64Decode` | `{{#base64Decode}}{{{steps.1.attributes.data.payload}}}{{/base64Decode}}`                                             | Decoded block content (ASCII)                                                                                                                             |
| `uriEncode`    | `{{{uriEncode steps.1.attributes.data.email}}}`                                                                       | `encodeURIComponent`                                                                                                                                      |
| `uriDecode`    | `{{{uriDecode steps.1.attributes.data.next}}}`                                                                        | `decodeURIComponent`                                                                                                                                      |

The long hex strings in these examples are [workflow variable](https://developer.kustomer.com/docs/workflows/conditions-and-expressions#variables-and-secrets) IDs. Workflows refer to variables by ID, never by name. See [Call your own code](https://developer.kustomer.com/docs/workflows/code-steps#sign-your-requests) for signing requests to your own services with `authHMAC`.

### JSON and text

| Helper           | Example                                                                                        | Output                                                |
| ---------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `toJSON`         | `{{{toJSON steps.lookup.custom}}}`                                                             | The value as JSON text                                |
| `parseJSON`      | `{{#parseJSON steps.call.body}}{{{order.number}}}{{/parseJSON}}`                               | Renders the block with the parsed JSON as its context |
| `split`          | `{{{split steps.1.attributes.data.name separator=' ' index=0}}}`                               | One part of a split string                            |
| `substring`      | `{{{substring steps.1.preview start=0 end=140}}}`                                              | Part of a string                                      |
| `replace`        | `{{{replace steps.1.preview search='[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}' newvalue='[card]'}}}` | Regex replace. `flags` defaults to `gm`               |
| `markdownToHTML` | `{{markdownToHTML steps.note.body}}`                                                           | Inline HTML from Markdown                             |
| `removeMarkdown` | `{{{removeMarkdown steps.1.preview}}}`                                                         | Plain text without Markdown syntax                    |
| `formatCurrency` | `{{{formatCurrency steps.order.data.total currency='EUR' locale='de-DE'}}}`                    | `1.234,56 €`. Defaults: `USD`, `en-US`                |
| `math`           | `{{{math steps.order.data.total '*' 100}}}`                                                    | `+`, `-`, `*`, `/` or `%` on two numbers              |
| `randomId`       | `{{{randomId size=12}}}`                                                                       | A random ID string. Pass the length as `size`         |

The string helpers from [Lodash](https://lodash.com/docs#camelCase) are available too, each taking one string: `camelCase`, `capitalize`, `escape`, `escapeRegExp`, `kebabCase`, `lowerCase`, `lowerFirst`, `snakeCase`, `startCase`, `toLower`, `toUpper`, `trim`, `trimEnd`, `trimStart`, `truncate`, `upperCase` and `upperFirst`.

Conditions accept templates, so these helpers also make text checks case-insensitive: `{ "op": "contains", "values": ["{{{toLower steps.1.subject}}}", "refund"] }`. See [Case-insensitive text checks](https://developer.kustomer.com/docs/workflows/conditions-and-expressions#case-insensitive-text-checks).

### Logic

| Helper    | Example                                                              | Output                                                                                |
| --------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `or`      | `{{{or steps.lookup.firstName 'there'}}}`                            | The first value if it's truthy, otherwise the second                                  |
| `cond`    | `{{{cond steps.1.status open='Waiting on us' done='Resolved'}}}`     | The option named by the value, or an empty string                                     |
| `ifCond`  | `{{#ifCond steps.1.priority '>=' 4}}Urgent{{else}}Normal{{/ifCond}}` | Block helper. Operators: `==`, `===`, `!=`, `!==`, `<`, `<=`, `>`, `>=`, `&&`, `\|\|` |
| `isEmpty` | `{{#isEmpty steps.1.tags}}Untagged{{else}}Tagged{{/isEmpty}}`        | Block helper: empty strings, arrays, objects, `null` and `undefined` are empty        |

Handlebars' built-ins (`if`, `unless`, `each`, `with`, `lookup`) work as documented at [handlebarsjs.com](https://handlebarsjs.com/guide/builtin-helpers.html).

### App settings

In workflows that ship with an [app](https://developer.kustomer.com/docs/apps/settings), `{{{ setting <app>.<category>.<name> }}}` inserts the installing org's value for that setting. Write it with exactly one space inside each set of braces, as shown. To supply a fallback and a type, add them after the path: `{{{ setting <app>.<category>.<name> <default> <type> }}}`. Settings are resolved before the rest of the template renders, and invalid `setting` references are rejected when you save the workflow.

## Criteria

Queue rules and satisfaction surveys don't use workflow conditions. They use **criteria**: an object with `and` and `or` lists of field checks.

```json title="Route VIP chats to a queue"
{
  "and": [
    { "conversation_channel": { "operator": "in", "value": ["chat"] } },
    { "customer_custom_tierStr": { "operator": "equals", "value": "gold" } }
  ],
  "or": [
    { "conversation_priority": { "operator": "gte", "value": 4 } },
    { "conversation_preview": { "operator": "contains_any_of", "value": ["refund", "chargeback"] } }
  ]
}
```

The criteria match when **every** `and` check passes **and** at least one `or` check passes. An empty list doesn't restrict anything, so `{ "and": [], "or": [] }` matches everything.

| Where                     | Endpoint                                                                                                               |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Queue rules               | [`POST /v1/routing/queue-rules`](https://developer.kustomer.com/api/queues-and-routing/create-queue-rule)              |
| Fields queue rules accept | [`GET /v1/routing/queue-rules-criteria`](https://developer.kustomer.com/api/queues-and-routing/get-queue-criteria)     |
| Satisfaction surveys      | [`POST /v1/satisfaction`](https://developer.kustomer.com/api/settings-and-configurations/create-a-satisfaction-survey) |

### Field names

A field is `<object>_<attribute>`, where the object is `conversation`, `customer`, `message`, `company` or `kobject`, and the attribute keeps its camelCase name: `customer_lastSeenAt`, `conversation_assignedTeams`. Custom attributes are `<object>_custom_<name>`, such as `conversation_custom_orderNumberStr`. A few names are shortcuts: `conversation_channel` checks `channels`, and `customer_email` and `customer_phone` check the primary email and phone.

The queue rules criteria endpoint lists every field you can use, with its type and an `operator` group (`string`, `number`, `datetime`, `boolean`, `list`) that tells you which operators make sense.

> **Not the same as search:** Criteria field names look like [search](https://developer.kustomer.com/docs/concepts/search) attribute names but aren't the same. Search uses names such as `customer_last_seen_at`, while criteria use the attribute's camelCase name. Don't copy one into the other.

### Operators

| Operator                 | `value`           | Passes when                                                                                                                     |
| ------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `is_set`                 | none              | The field has a value. `false` and `0` count as set; empty strings and empty arrays don't                                       |
| `not_set`                | none              | The field is missing, `null`, an empty string or an empty array                                                                 |
| `equals`                 | any               | Equal after casting the value to the field's type. For an array field, the array includes the value                             |
| `not_equals`             | any               | `equals` would fail                                                                                                             |
| `gt`, `gte`, `lt`, `lte` | number or date    | Numeric comparison, or a date comparison if the value is a date. See relative dates below                                       |
| `in`                     | array             | The field's value is in the list. For an array field, at least one item is                                                      |
| `not_in`                 | array             | `in` would fail                                                                                                                 |
| `contains`               | string or array   | The field contains the text, ignoring case. For an array field, an item does. With an array value, every item must be contained |
| `not_contains`           | string or array   | `contains` would fail                                                                                                           |
| `contains_any_of`        | array             | The field contains at least one of the strings, ignoring case                                                                   |
| `not_contains_any_of`    | array             | It contains none of them                                                                                                        |
| `matches`                | regex string      | The field matches the regular expression (case-sensitive)                                                                       |
| `in_business_hours`      | schedule ID       | The date field falls within the business schedule                                                                               |
| `not_in_business_hours`  | schedule ID       | It doesn't                                                                                                                      |
| `changes`                | `before`, `after` | The field changed in this update, and its old and new values match the nested checks                                            |

Date comparisons accept ISO 8601 timestamps or relative values: `now-2d` means two days ago and `now+1h` an hour from now. The units are `s` (seconds), `m` (minutes), `h` (hours), `d` (days), `w` (weeks) and `M` (months), with up to 99 of each.

```json title="Last seen more than a week ago"
{ "customer_lastSeenAt": { "operator": "lt", "value": "now-7d" } }
```

```json title="Status changed from open to done"
{
  "conversation_status": {
    "operator": "changes",
    "before": { "operator": "equals", "value": "open" },
    "after": { "operator": "equals", "value": "done" }
  }
}
```

A check on a custom attribute that has since been deleted is ignored, so it passes. Remove such checks when you delete attributes.
