Data and variables

The language of workflow data. Covers /#steps path references and {{{ }}} Handlebars templates side by side, what each produces and when to use it, every path you can address (trigger output, changes, step outputs, variables by ID, /#error, callable inputs), path syntax, missing values, type conversion, common helpers, escaping, and how builder picks translate to JSON.

Every value in a workflow is one of three things: a literal such as "done" or 4, a path reference such as "/#steps.1.status", or a template such as "Order {{{steps.1.custom.orderNumberStr}}}". References and templates read from the same place, the run's context, but they behave differently. Knowing which one to use, and what each one produces, is most of what you need to write workflows confidently.

This page is the grammar. For condition operators, see Conditions and variables. For every function and helper, see the Expressions reference.

Two syntaxes

Path referenceHandlebars template
Looks like"/#steps.1.tags""{{{steps.1.tags}}}", "Order {{{steps.1.id}}}"
Recognized whenThe whole string starts with /#Any other string. Each {{ }} tag in it is replaced
ProducesThe value with its type: string, number, boolean, array or objectAlways a string
Missing valueNothing, so a param is left outAn empty string
Mixed with textNoYes
HelpersFunctions, written /#fn:<name>,<path>,…Handlebars helpers, such as {{{toLower steps.1.subject}}}
In the builderWritten for you when you pick a fieldTyped with Toggle Code View

Both forms write the path the same way, steps.1.tags; only the wrapping differs. Here they are in one step:

A reference and a template in the same step
{
  "id": "note",
  "action": "kustomer.note.create",
  "params": {
    "conversation": "/#steps.1.id",
    "body": "Order {{{steps.1.custom.orderNumberStr}}}: {{{steps.find.name}}}"
  },
  "transitions": []
}

conversation needs an ID, so it's a reference to one. body mixes text and values, so it's a template.

A reference is the whole value

A string is a reference only when /# is the very first thing in it. There's no way to embed a reference in other text: use a template.

ValueResult
"/#steps.1.id""6512f0c2a4b1e1001a6b2c90", the conversation's ID
"Order /#steps.1.id""Order /#steps.1.id", unchanged. Here /# is just text
"Order {{{steps.1.id}}}""Order 6512f0c2a4b1e1001a6b2c90"
"steps.1.id""steps.1.id", a plain string. Without /# or braces nothing is resolved

Templates produce strings

A template is rendered to text, whatever the values inside it are. Each param is then converted to the type the action's input expects, so a template that renders "4" for a number input becomes 4. That conversion only goes so far, so pick the syntax by the type you need:

You needUseWhy
A value as is: an ID, number, boolean, array or objectA reference: "/#steps.1.tags"It keeps its type. Arrays and objects arrive whole
Text built from several valuesA template: "Order {{{steps.1.custom.orderNumberStr}}} shipped"Only templates can mix text and values
A value converted to another typeA function: "/#fn:parseFloat,steps.1.attributes.data.total"Functions return typed values

Arrays and objects don't survive templates. A template prints an array's items joined with commas, so {{{steps.1.tags}}} renders "64f1c0ffee5ca1ab1e000001,64f1c0ffee5ca1ab1e000002", and it prints an object as [object Object]. To put an object into text, such as a log message, use {{{toJSON steps.find.custom}}}.

Double or triple braces

{{ }} HTML-escapes the value. {{{ }}} inserts it unchanged.

TemplateRenders
"{{steps.1.name}}""Order 4417 hasn&#x27;t arrived"
"{{{steps.1.name}}}""Order 4417 hasn't arrived"

Notes, URLs, headers and JSON bodies all need the original text, so use triple braces everywhere unless you're building HTML and want the escaping.

Try it

Pick a value, type your own, or click a line of the context. Each one is resolved three ways, as a reference and as both kinds of template, so you can see how the type changes.

InteractiveResolve a path against a run
Trigger: step 1
Lookup step: find
Variables and errors
Templates
Gotchas
Resolves to
Referenceyou wrotestring
/#steps.1.status
→ "done"
Templatestring
{{{steps.1.status}}}
→ "done"
Template, escapedstring
{{steps.1.status}}
→ "done"

The trigger is always step 1, and its record is flattened, so attributes sit at the top level.

Run contextselect a line to use its path
{
],
},
}
}
}
},
}
],
}
}
},
},
}
}
A sample run context: step 1 is a conversation update trigger and find is a customer lookup. The error object appears only while error cases are checked, and real errors carry more fields. Only path lookups and simple {{ }} and {{{ }}} tags are evaluated here.

What you can address

A path starts at the top of the run's context:

PathWhat it isAvailable
steps.1.<field>The trigger's outputAlways
steps.<id>.<field>A step's output, by the step's idAfter that step succeeds on this run's path
variables.<variableId>A workflow variable, by its IDAlways, when the workflow has the variable
error.<field>The error from the step that just failedOnly in that step's errorCases

Trigger output: steps.1

A record trigger's output is the record, flattened: its attributes sit at the top level, relationships are plain IDs, and update triggers add a changes diff. The full example is in What the trigger outputs.

ReferenceValue
/#steps.1.idThe record's ID
/#steps.1.statusThe conversation's current status, such as "done"
/#steps.1.customerThe customer's ID. Look the customer up to read more
/#steps.1.tagsAn array of tag IDs
/#steps.1.custom.orderNumberStrA custom attribute

Other triggers put their data in different places:

  • Inbound web hooks put your payload under attributes.data, so a field called email is /#steps.1.attributes.data.email. Paste a sample payload into the trigger's Example Webhook Data so its fields appear in the builder's picker.
  • Callable workflows get only their inputs, cast to the trigger's schema. An input called conversationId is /#steps.1.conversationId, not /#steps.1.inputs.conversationId.

What changed: changes

Update triggers include a changes diff with each changed attribute's before and after. Custom attributes change as one custom entry, so the field name comes after before or after:

ReferenceValue
/#steps.1.changes.attributes.status.afterThe new status, set only when this update changed it
/#steps.1.changes.attributes.status.beforeThe previous status
/#steps.1.changes.attributes.custom.after.tierStrThe new value of the custom attribute tierStr
/#steps.1.changes.attributes.tags.afterThe full tag list after the change

Gate update triggers on changes rather than on the current value. See Conditions on what changed.

Step outputs: steps.<id>

StepOutputExample references
A Kustomer actionThe record it found or wrote, flattened like trigger output/#steps.find.id, /#steps.find.emails.0.email
A Kustomer lookup that finds nothingEmpty, so every path under it resolves to nothingexists [/#steps.find.id] is false
A step continued past by an error caseNothingSame as above
A REST stepresponse.statusCode, response.headers (lowercase names) and body/#steps.call.response.statusCode, /#steps.call.body.eligible
A regex stepmatch, captures, index and original/#steps.order-number.captures.0

A step's output is exactly what the builder's View Logs shows for it. When a value comes out empty, compare your path with the logged output. See Debugging.

Workflow variables

Workflow variables hold secrets and settings that shouldn't be written into the workflow, such as an API token. Reference a variable by its ID, not its name. The ID is the 24-character data.id returned when you create the variable.

{
  "headers": {
    "Authorization": "Bearer {{{variables.6513b2e4a4b1e1001a6b3f10}}}",
    "X-Api-Key": "/#variables.6513b2e4a4b1e1001a6b3f11"
  }
}

The name is only a label. A reference by name resolves to nothing, so the header above would be sent as "Bearer ". You rarely need to type IDs yourself:

  • In the builder, picking a variable from a field's picker inserts /#variables.<variableId>.
  • In an object field, such as a REST step's headers, the Variables button copies {{{variables.<variableId>}}} for you to paste.
  • With the API, list the workflow's variables to find each one's ID.

Values are strings of 1 to 1,024 characters. The builder and the API show them masked, with only the last 6 characters visible, so they can't be read back.

Each variable belongs to one workflow. If 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. Kustomer loads variables only when the workflow refers to them, so always write /#variables. or {{{variables. out in full.

The error: /#error

Inside a step's error cases, /#error is the error that step just raised. For a Kustomer action, it's the first item of the API's errors array:

ReferenceValue
/#error.statusThe HTTP status, such as 400
/#error.codeThe error's code
/#error.titleA short summary
/#error.detailA longer explanation
/#error.sourceWhich part of the request was at fault, when given

/#error exists only while error cases are checked. It's never at /#steps.<id>.error, and it's gone once the run moves on.

App settings

Workflows that ship in an app read the installing org's settings with the setting helper, in templates only: {{{ setting <app>.<category>.<name> }}}. It can also take a default and a type: {{{ setting <app>.<category>.<name> <default> <type> }}}. See App settings.

Path syntax

PathMeans
steps.find.emails.0.emailThe first email. A number picks an array item
steps.find.emails[0].emailThe same, with a bracket index
steps.1.tags.lengthHow many tags, as a number
steps.1.name.lengthHow many characters in the name
steps.1.attributes.data["order.id"]A key that contains dots, in quoted brackets
  • Indexes are whole numbers from 0. There are no negative indexes, so there's no "last item" shortcut.
  • Only the object's own keys are read.
  • In templates, use dots for indexes: {{{steps.find.emails.0.email}}}.
  • Function arguments are bare paths, without /#: /#fn:parseInt,steps.1.custom.qtyStr, not /#fn:parseInt,/#steps.1.custom.qtyStr.
  • Give steps IDs without dots, such as find-customer. Transition targets can't contain dots.

Missing values

A path that doesn't resolve never causes an error. What you get instead depends on where the path is used:

WhereA missing value becomes
A param set to a referenceNothing: the param is left out of the request. The one exception is a null queue, which is kept and means "unassign"
A param set to an empty objectNothing, the same way
Inside a templateAn empty string
A condition operand"". So exists fails, dne passes, neq against a value passes, and eq against a value fails
A required input of a Kustomer actionThe action may fail or return empty output. 0, false and "" count as missing too

A typo in a step ID or field name looks exactly like missing data. To handle values that really can be missing:

  • Guard with a condition. Put exists [/#steps.find.id] on the transition into the step that needs it.
  • Give templates a fallback. {{{or steps.find.firstName 'there'}}} prints the first name, or there when there isn't one.
  • Leave optional params out. /#fn:ifExists,steps.find.id,/#steps.find.name resolves to the name only when the lookup found someone.

Types and conversion

After a param is resolved, it's converted to the type of the action's input:

ParamResolves toSent to a number input as
"/#steps.1.priority"33
"{{{steps.1.priority}}}""3"3
"/#fn:parseInt,steps.1.custom.qtyStr"1212

Use functions when the source has the wrong type, such as a number stored as text in a web hook payload:

FunctionReturnsWhen the input isn't valid
/#fn:parseInt,<path>An integer0
/#fn:parseFloat,<path>A number0
/#fn:parseBool,<path>true for true or "true"false
/#fn:parseJSON,<path>The parsed object{}

An unknown function name doesn't fail. It returns the input unchanged, so check the spelling against the Expressions reference. Conditions do their own casting: eq converts the right side to the left side's type. See Operators.

Handlebars helpers you'll use most

Helpers go inside templates, with positional arguments first and then key=value options. Arguments are paths, single-quoted strings or numbers.

HelperExampleRenders
toLower{{{toLower steps.1.subject}}}where is my refund?
or{{{or steps.find.firstName 'there'}}}The first name, or there
dateFormat{{{dateFormat steps.1.createdAt format='YYYY-MM-DD'}}}2026-10-21
date{{{date 'days' count=-3}}}An ISO timestamp 3 days from now
uriEncode{{{uriEncode steps.1.attributes.data.email}}}jane%40example.com
toJSON{{{toJSON steps.find.custom}}}{"vipBool":true}
ifCond{{#ifCond steps.1.priority '>=' 4}}Urgent{{else}}Normal{{/ifCond}}Urgent or Normal
#if{{#if steps.find.custom.vipBool}}VIP{{/if}}VIP, or nothing

Conditions accept templates as well as references, which is how you make a text check case-insensitive:

JSON
{ "op": "contains", "values": ["{{{toLower steps.1.subject}}}", "refund"] }

The full list, with every option and default, is in the Expressions reference.

Escaping

Three layers can change your text: JSON, Handlebars and URLs.

  • JSON. A workflow is a JSON document, so backslashes and double quotes in strings need escaping: a regex \d+ is written "\\d+". Helper arguments use single quotes, format='LL', so they don't need escaping.
  • HTML. {{ }} HTML-escapes values. Use {{{ }}}.
  • URLs. A value that goes into a URL needs encoding: "https://api.example.com/customers?email={{{uriEncode steps.1.attributes.data.email}}}". Otherwise a space or & breaks the request.

To send JSON to your own service, give the REST step's data an object, and use references for the values. Each value keeps its type, and the step serializes the body for you. Don't build a JSON body as a template string.

Build bodies from references, not strings
{
  "data": {
    "conversationId": "/#steps.1.id",
    "priority": "/#steps.1.priority",
    "tags": "/#steps.1.tags",
    "summary": "{{{steps.find.name}}}: {{{steps.1.name}}}"
  }
}

From the builder to JSON

The builder writes all of this for you. Knowing what it writes lets you read any workflow's JSON, and fix it when the picker can't express what you need.

In the builderIn the JSON
Pick a field from an earlier step"/#steps.<stepId>.<path>" as the whole value. The field shows the step's number and the field's name, such as "Custom > VIP"
Pick a field from the trigger"/#steps.1.<path>"
Pick a variable"/#variables.<variableId>"
Variables button on an object fieldCopies {{{variables.<variableId>}}} to paste into the JSON
Pick a function"/#fn:<name>,<path>,…"
Type text with {{ }} in Toggle Code ViewA template string, such as "Order {{steps.1.custom.orderNumberStr}} shipped"
Type vip,priority into an array field["vip", "priority"]
Type 15 into a number field15
Type true into a boolean fieldtrue
Type [] into the tags field[], which clears all tags
Leave the queue field emptyUnassigns the queue
Clear an optional fieldThe param is removed
The badge on a stepNot in the JSON. The step's id is a generated short ID, such as SAk4K54Ly
Example Webhook DataNot in the JSON. It's saved only in your browser, and lists the payload's fields in the picker as /#steps.1.attributes.data.<field>

Code view text is committed when you press Enter or click away, and it's converted to the field's type at that point.

Next