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.

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

LanguageLooks likeUsed in
Functions"/#fn:parseInt,steps.1.custom.qtyStr"Workflow params and conditions. Returns typed values
Handlebars helpers"{{{dateFormat steps.1.createdAt format='LL'}}}"Any workflow string that isn't a reference. Returns strings
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.

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.

FunctionExampleReturns
parseInt/#fn:parseInt,steps.1.custom.qtyStrAn integer. 0 if the input is empty or not a number
parseFloat/#fn:parseFloat,steps.1.attributes.data.totalA number. 0 if the input is empty or not a number
parseBool/#fn:parseBool,steps.1.attributes.data.viptrue for true or "true" (any case), otherwise false
parseJSON/#fn:parseJSON,steps.call.bodyThe parsed object. {} if the string isn't valid JSON
dateFormat/#fn:dateFormat,steps.1.createdAt,YYYY-MM-DDThe date in a Moment.js format. The default is ISO 8601. undefined if the input isn't a date
slice/#fn:slice,steps.1.tags,0,3Part of an array or string, like JavaScript's slice(start, end)
map/#fn:map,steps.lookup.emails,emailPulls one property out of every item in an array: ["jane@example.com", …]
transform/#fn:transform,steps.call.body.items,name=title,sku=idEach array item reshaped: the new key name takes the old key title, and so on
cond/#fn:cond,steps.1.status,open=Waiting,done=ResolvedThe 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.nameThe second expression if the path has a value, otherwise undefined, so the param is left out
URIEncode/#fn:URIEncode,steps.1.attributes.data.emailencodeURIComponent of the value
URIDecode/#fn:URIDecode,steps.1.attributes.data.redirectdecodeURIComponent of the value
isValidEmailAddress/#fn:isValidEmailAddress,steps.1.attributes.data.emailtrue or false
isValidPhoneNumber/#fn:isValidPhoneNumber,steps.1.attributes.data.phonetrue if the number can be formatted as a valid phone number
findIncludedResourceAttribute/#fn:findIncludedResourceAttribute,steps.call.body.included,type=order,id=4417,attributes.totalFrom a JSON:API included array, the property of the first resource matching every key=value
inBusinessHours/#fn:inBusinessHours,steps.1.createdAt,64f1c0ffee5ca1ab1e0000aaWhether the date falls within a business schedule. See below

inBusinessHours

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

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

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

Signing and encoding

HelperExampleOutput
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 IDs. Workflows refer to variables by ID, never by name. See Call your own code for signing requests to your own services with authHMAC.

JSON and text

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

Logic

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

App settings

In workflows that ship with an app, {{{ 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.

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.

WhereEndpoint
Queue rulesPOST /v1/routing/queue-rules
Fields queue rules acceptGET /v1/routing/queue-rules-criteria
Satisfaction surveysPOST /v1/satisfaction

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.

Operators

OperatorvaluePasses when
is_setnoneThe field has a value. false and 0 count as set; empty strings and empty arrays don't
not_setnoneThe field is missing, null, an empty string or an empty array
equalsanyEqual after casting the value to the field's type. For an array field, the array includes the value
not_equalsanyequals would fail
gt, gte, lt, ltenumber or dateNumeric comparison, or a date comparison if the value is a date. See relative dates below
inarrayThe field's value is in the list. For an array field, at least one item is
not_inarrayin would fail
containsstring or arrayThe field contains the text, ignoring case. For an array field, an item does. With an array value, every item must be contained
not_containsstring or arraycontains would fail
contains_any_ofarrayThe field contains at least one of the strings, ignoring case
not_contains_any_ofarrayIt contains none of them
matchesregex stringThe field matches the regular expression (case-sensitive)
in_business_hoursschedule IDThe date field falls within the business schedule
not_in_business_hoursschedule IDIt doesn't
changesbefore, afterThe 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.

Last seen more than a week ago
{ "customer_lastSeenAt": { "operator": "lt", "value": "now-7d" } }
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.