Conditions and variables
How workflow params and conditions get their values. Covers /#steps references, Handlebars templates and escaping, every condition operator and its casting rules, the changes diff, workflow variables for secrets, and error cases.
Every value in a workflow, whether it's a step param, a condition operand or a REST header, is one of four things:
The rule: a string that starts with /# is a reference or function call and keeps its type, but only when the reference is the whole string. Any other string is a template, so "Order /#steps.1.id" is sent as that literal text. To mix text and values, use {{ }}.
For a guided introduction to this language, see Data and variables. This page is the reference.
After resolution, each param is cast to the type in the action's input schema. A template that renders "42" for a number input becomes 42.
References
/# followed by a dotted path into the run's context:
A path that doesn't exist resolves to undefined. It doesn't cause an error, so a typo in a step ID silently produces nothing. Check the run's logs if a value seems to be missing.
When a param's reference resolves to null or undefined, the param is left out of the request entirely. An empty object counts as "not set" too. The one exception is queue, where null is kept and means "unassign".
Path syntax
Only an object's own keys are read, so inherited properties such as constructor resolve to nothing.
{
"id": "followup",
"action": "kustomer.conversation.update",
"params": {
"id": "/#steps.1.id",
"assignedUsers": ["/#steps.owner.id"],
"custom": {
"orderTotalNum": "/#fn:parseFloat,steps.order.data.total",
"lastOrderAt": "/#steps.order.createdAt"
}
},
"transitions": []
}Templates
Strings without /# are Handlebars templates rendered against the same context, so {{steps.1.name}}, {{{variables.<variableId>}}} and the standard block helpers ({{#if}}, {{#each}}, {{#unless}}, {{#with}}) all work. Kustomer adds helpers for dates, HMAC signing, JSON and formatting.
{
"body": "{{{steps.lookup.firstName}}} asked about order {{{steps.1.custom.orderNumberStr}}}.{{#if steps.1.custom.vipBool}} VIP customer.{{/if}}",
"uri": "https://api.example.com/orders/{{{steps.1.custom.orderNumberStr}}}?expand=items"
}A template always produces a string. To pass an array or object, use a reference instead: "tags": "/#steps.1.tags", not "tags": "{{steps.1.tags}}".
A reference keeps its type. This param gets the array itself, so use a reference, not a template, to pass arrays and objects.
1 is the trigger (a conversation update) and lookup is a later step's output. Only path lookups, {{#if}} and eq are evaluated here. Values marked ⇄ can be flipped.Conditions
Conditions guard transitions. Each one is { "op": …, "values": […] }. Each value can be a literal, a reference or a template.
{ "op": "eq", "values": ["/#steps.1.changes.attributes.status.after", "done"] }Operators
There are no in or not-in operators. To test "status is one of A or B", use an or group of eq conditions. An op that isn't in this table fails the run with operation <op> not supported.
The builder offers only the operators that suit the field you pick. Text, list, date-time and number fields all get Exists, Does not exist, Equals and Not equal. Text and list fields add Contains and Does not contain, date-time fields add Before and After, and number fields add the four numeric comparisons. Boolean fields get only Equals and Not equal. A path you type yourself in code view gets every operator. The builder never offers true; when a step's only transition is always-true, the canvas draws a plain line instead of a condition.
contains understands skills: on a conversation's skills array it matches the skill ID, and only for active skills.
Case-insensitive text checks
contains and eq are case-sensitive. Condition values can be templates as well as references, so render the left side in lowercase and compare it with a lowercase literal:
{ "op": "contains", "values": ["{{{toLower steps.1.subject}}}", "refund"] }A template always renders a string, so use this only for text. For numbers and booleans, keep the plain reference so eq can cast.
Conditions on what changed
Update triggers fire for every change to the record, including changes your workflow makes. Base conditions on the changes diff, which describes only this update:
{
"changes": {
"attributes": {
"custom": {
"op": "replace",
"before": { "tierStr": "silver", "loyaltyPointsNum": 1200 },
"after": { "tierStr": "gold", "loyaltyPointsNum": 2400 }
},
"modifiedAt": { "op": "replace", "before": "2026-10-21T15:00:00.000Z", "after": "2026-10-21T15:20:42.554Z" }
}
}
}Custom attributes change as one custom entry whose before and after hold the changed custom fields, so the field name comes after before or after in the path. Tags, users and teams are stored as IDs, so compare with IDs, not names. The full diff format is in The event envelope.
Variables and secrets
Workflow variables hold values you don't want written into the workflow itself: API keys, signing secrets, or a URL that differs between your sandbox and production orgs. Each variable belongs to one workflow.
curl -X POST "https://acme.api.kustomerapp.com/v1/workflows/$WORKFLOW_ID/variables" \
-H "Authorization: Bearer $KUSTOMER_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "analyticsToken", "value": "sk_live_…", "type": "string" }'{ "data": { "id": "6513b2f4a4b1e1001a6b3f20", "attributes": { "name": "analyticsToken" } } }namestringrequired- A label for people: letters, numbers,
-,_and., up to 128 characters, with no spaces. Workflows don't use it to look the variable up. valuestringrequired- 1 to 1,024 characters. The API returns it masked, as
maskedValue, so it can't be read back. The API and the builder show only its last 6 characters. typestringstring.
Refer to a variable by its ID, the 24-character data.id returned when you create it, not by its name. Use /#variables.<variableId> as a whole param, or {{{variables.<variableId>}}} inside a template:
{ "headers": { "Authorization": "Bearer {{{variables.6513b2f4a4b1e1001a6b3f20}}}" } }In the builder, the value picker's Variables section inserts /#variables.<variableId> for you, and JSON fields have a Variables button that copies {{{variables.<variableId>}}}. To find an ID later, list the workflow's variables with GET /v1/workflows/{id}/variables.
Variables are cached, so like workflow edits, a changed value can take about a minute to reach new runs. Manage them with the variables endpoints.
Handle errors
When a step fails, for example because a Kustomer action rejects its input with a 4xx, the step's errorCases decide what happens. Without a matching case, the run stops and the failure is recorded on the workflow. See Debugging.
The classic workflow builder has no editor for error cases. Add errorCases in Edit workflow JSON or with the API.
{
"id": "create-order",
"action": "kustomer.kobject.create-with-customer",
"params": { "klassName": "order", "customer": "/#steps.1.customer", "title": "/#steps.1.custom.orderNumberStr", "data": {} },
"errorCases": [
{
"condition": { "op": "and", "values": [{ "op": "eq", "values": ["/#error.status", 400] }] },
"errorAction": { "type": "continue" }
},
{
"condition": { "op": "and", "values": [] },
"errorAction": { "type": "stop-processing" }
}
],
"transitions": [{ "target": "tag", "condition": { "op": "exists", "values": ["/#steps.create-order.id"] } }]
}- Cases are checked in order and the first match applies. A case whose
condition.valuesis empty matches every error. - An error case's
condition.opmust beandoror, with up to five nested conditions, and a step can have up to 10 cases. - While cases are checked, the error is at
/#error, never at/#steps.<id>.error. For Kustomer actions it's the first item of the API'serrorsarray, with the fieldsstatus,code,title,detailandsource.
Use continue and stop-processing. The schema accepts retry-action and retry-workflow, but neither retries anything; branch explicitly instead.
App settings
Workflows that ship in an app read the installing org's settings with the setting helper, which resolves before the template renders:
{ "uri": "{{{ setting myapp.default.apiBaseUrl }}}/v1/orders/{{{steps.1.custom.orderNumberStr}}}" }The path is <app>.<category>.<setting name>, matching the app definition's settings. You can add a default value and a type after the path: {{{ setting <app>.<category>.<name> <default> <type> }}}. Kustomer checks setting references when you save, and rejects invalid ones with Invalid app settings configuration passed.