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

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

Every value in a workflow, whether it's a step param, a condition operand or a REST header, is one of four things:

| Form      | Example                                               | Result                                                             |
| --------- | ----------------------------------------------------- | ------------------------------------------------------------------ |
| Literal   | `"done"`, `3`, `true`, `["vip"]`                      | Used as is                                                         |
| Reference | `"/#steps.lookup.emails"`                             | The value at that path, **with its type** (array, number, object…) |
| Function  | `"/#fn:parseFloat,steps.1.attributes.data.total"`     | The function's result, typed                                       |
| Template  | `"Order {{{steps.1.custom.orderNumberStr}}} shipped"` | A **string**, rendered with Handlebars                             |

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](https://developer.kustomer.com/docs/workflows/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:

| Path                      | What it is                                               |
| ------------------------- | -------------------------------------------------------- |
| `steps.<id>.<field>`      | A step's output. The trigger is step `1`                 |
| `steps.<id>.list.0.field` | An array element, by index                               |
| `steps.<id>.list.length`  | An array's length                                        |
| `variables.<variableId>`  | A [workflow variable](#variables-and-secrets), by its ID |
| `error.<field>`           | The current error, inside [error cases](#handle-errors)  |

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

| Syntax          | Example                                                         | Notes                                                                                            |
| --------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Dots            | `steps.lookup.custom.tierStr`                                   | Each segment is an object key                                                                    |
| Index           | `steps.lookup.emails.0.email` or `steps.lookup.emails[0].email` | Whole, non-negative numbers only. There are no negative indexes, so you can't count from the end |
| `length`        | `steps.1.tags.length`                                           | Works on arrays and strings                                                                      |
| Quoted brackets | `steps.call.body["order.id"]`                                   | For a key that contains dots. Plain dots also work if that exact key exists                      |

Only an object's own keys are read, so inherited properties such as `constructor` resolve to nothing.

```json title="References in params"
{
  "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](https://handlebarsjs.com/guide/) 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](https://developer.kustomer.com/docs/workflows/expressions-reference#handlebars-helpers) for dates, HMAC signing, JSON and formatting.

> **Use triple braces for text:** `{{ … }}` HTML-escapes the value, so `Tom & Jerry's` becomes `Tom &amp; Jerry&#x27;s`. That's rarely what you want in a note, a URL or a JSON body. Use `{{{ … }}}` to insert the value unchanged. All the helper examples use triple braces for this reason.

```json title="Templates in params"
{
  "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 sample run context in JSON, with step 1 as a conversation update trigger and a lookup step holding a customer's first name and emails, next to a list of example expressions. Selecting one traces the keys it walks: /#steps.lookup.emails returns the array itself, /#steps.lookup.emails.0.email returns jane\@example.com, /#steps.1.channels.length returns the number 1, and the typo /#steps.lokup.id stops at steps and resolves to undefined without an error. Two templates render strings: one adds ' VIP customer.' only while vipBool is true, and {{steps.1.name}} HTML-escapes Tom & Jerry's to Tom & Jerry's while triple braces leave it unchanged. Two eq conditions pass or fail as you flip values: changes.attributes.status.after equals done, and priority 4 equals the string 4 because eq casts the right side to the left side's type._

## Conditions

Conditions guard [transitions](https://developer.kustomer.com/docs/workflows#anatomy). Each one is `{ "op": …, "values": […] }`. Each value can be a literal, a reference or a template.

```json title="Single comparison"
{ "op": "eq", "values": ["/#steps.1.changes.attributes.status.after", "done"] }
```

```json title="Combined"
{
  "op": "and",
  "values": [
    { "op": "eq", "values": ["/#steps.1.changes.attributes.status.after", "done"] },
    { "op": "contains", "values": ["/#steps.1.channels", "email"] },
    {
      "op": "or",
      "values": [
        { "op": "gte", "values": ["/#steps.1.priority", 4] },
        { "op": "eq", "values": ["/#steps.lookup.custom.tierStr", "gold"] }
      ]
    }
  ]
}
```

```json title="Always"
{ "op": "true", "values": [true] }
```

### Operators

| `op`                     | In the builder                                                     | Values     | Passes when                                                                                                                      |
| ------------------------ | ------------------------------------------------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `and`                    | Multiple Conditions (And)                                          | conditions | Every nested condition passes                                                                                                    |
| `or`                     | Multiple Conditions (Or)                                           | conditions | Any nested condition passes                                                                                                      |
| `exists`                 | Exists                                                             | `[a]`      | `a` is truthy                                                                                                                    |
| `dne`                    | Does not exist                                                     | `[a]`      | `a` is falsy ("does not exist")                                                                                                  |
| `true`                   | not offered                                                        | `[a]`      | `a` is `true` or the string `"true"`. `[true]` makes a branch that always passes                                                 |
| `eq`                     | Equals                                                             | `[a, b]`   | `a` equals `b`, after casting `b` to `a`'s type                                                                                  |
| `neq`                    | Not equal                                                          | `[a, b]`   | `eq` would fail                                                                                                                  |
| `gt`, `gte`, `lt`, `lte` | Greater than, Greater than or equal, Less than, Less than or equal | `[a, b]`   | Numeric comparison of `Number(a)` and `Number(b)`                                                                                |
| `gt-date`, `lt-date`     | After, Before                                                      | `[a, b]`   | `a` is after or before `b`, both parsed as dates                                                                                 |
| `contains`               | Contains                                                           | `[a, b]`   | `a` is an array that includes `b`, or a string containing `b` (case-sensitive). If `b` is an array, every item must be contained |
| `not-contains`           | Does not contain                                                   | `[a, b]`   | `a` doesn't contain `b`. If `b` is an array, at least one item is missing                                                        |

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.

> **Casting rules that catch people out**
>
> - **`exists` is truthiness.** `0`, `false` and `""` don't "exist". Use `eq` to test for zero or false.
> - **`neq` passes when the field is missing.** `neq [/#steps.1.custom.tierStr, "gold"]` is true for customers without a tier. Add an `exists` check with `and` if that matters.
> - **`eq` casts the right side to the left side's type.** `["/#steps.1.priority", "4"]` compares numbers, but `["4", "/#steps.1.priority"]` compares strings. Put the reference first.
> - **A missing left side isn't cast.** If the field is missing, nothing is cast and the comparison is strict, so `eq [<missing field>, false]` fails. Use `neq true` to catch both `false` and missing.
> - **Invalid dates never compare.** If either side of `gt-date` or `lt-date` doesn't parse, the condition fails.
> - **For "is false", use `neq true`.** Don't use a `false` operator.
> - **A path that doesn't resolve is treated as `""`.** The condition is evaluated against an empty string; the run doesn't fail.

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

```json
{ "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:

```json title="changes in a customer.update trigger"
{
  "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" }
    }
  }
}
```

| You want                  | Condition                                                                                                                                        |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Status just became done   | `eq [/#steps.1.changes.attributes.status.after, "done"]`                                                                                         |
| Status changed at all     | `exists [/#steps.1.changes.attributes.status]`                                                                                                   |
| Became VIP (custom field) | `and` of `eq [/#steps.1.changes.attributes.custom.after.tierStr, "gold"]` and `neq [/#steps.1.changes.attributes.custom.before.tierStr, "gold"]` |
| A tag was added           | `contains [/#steps.1.changes.attributes.tags.after, "<tagId>"]` and `not-contains [/#steps.1.changes.attributes.tags.before, "<tagId>"]`         |
| Assigned to someone new   | `exists [/#steps.1.changes.attributes.assignedUsers]`                                                                                            |

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](https://developer.kustomer.com/docs/events#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.

```bash
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" }'
```

```json title="Response (abridged)"
{ "data": { "id": "6513b2f4a4b1e1001a6b3f20", "attributes": { "name": "analyticsToken" } } }
```

**Variable**

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

```json
{ "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`](https://developer.kustomer.com/api/workflows/get-all-variables-for-a-workflow).

> **Names resolve to nothing:** `{{{variables.analyticsToken}}}` looks right but renders an empty string, because variables are keyed by ID. Variables also belong to one workflow, so when you copy a workflow's JSON into another workflow or another org, every variable reference breaks until you create the variables there and update the IDs.

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](https://developer.kustomer.com/api/workflows/get-all-variables-for-a-workflow).

> **Keep secrets out of params:** Anything written directly into a workflow's params is visible to everyone who can view workflows, and is returned by `GET /v1/workflows`. Put keys and tokens in variables.

## 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](https://developer.kustomer.com/docs/workflows/debugging).

The classic workflow builder has no editor for error cases. Add `errorCases` in **Edit workflow JSON** or with the [API](https://developer.kustomer.com/docs/workflows/workflow-json#create-and-update-with-the-api).

```json title="Continue past a validation error, stop on anything else"
{
  "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.values` is empty matches every error.
- An error case's `condition.op` must be `and` or `or`, 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's `errors` array, with the fields `status`, `code`, `title`, `detail` and `source`.

| `errorAction.type` | Effect                                                                                                                                               |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `continue`         | Move on to the step's transitions as if it had succeeded, but with no output. Guard the next transition with `exists` on the step's output, as above |
| `stop-processing`  | End the run quietly. It isn't recorded as a failure                                                                                                  |
| `retry-workflow`   | Behaves exactly like `continue`. Nothing is retried                                                                                                  |
| `retry-action`     | Not handled. The step fails as if no case had matched                                                                                                |

Use `continue` and `stop-processing`. The schema accepts `retry-action` and `retry-workflow`, but neither retries anything; branch explicitly instead.

> **Not every failure reaches error cases**
>
> - **A `5xx` from a Kustomer action**, or a temporary platform problem, skips error cases. Kustomer runs the workflow again from step `1`, so steps that already succeeded run again. These retries aren't recorded on the workflow's error log.
> - **The run timeout** ends the run whatever its error cases say. It's checked between steps.
> - **The [REST actions](https://developer.kustomer.com/docs/workflows/code-steps#handle-responses-and-failures) never fail on a status code**, so check `/#steps.<id>.response.statusCode` in a transition instead.

## App settings

Workflows that ship in an [app](https://developer.kustomer.com/docs/apps/settings) read the installing org's settings with the `setting` helper, which resolves before the template renders:

```json
{ "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`.
