Where expressions appear
Three kinds of fields in the designer take dynamic values:
The first two use the same formula language; body fields are plain prose with embedded
{{ }} references.
The formula language
A formula is a single expression: references, literals, operators, and function calls.$ and use dotted paths — $instance.amount, $document.kw.Status, $object.attr.Total. Typing $ opens autocomplete with everything the workflow can see; a keyword or attribute whose name contains spaces uses the bracket form $[Invoice Number]. Hovering a reference shows its label and type.
Keywords are lists (more below), so two suffixes do the common work: .Value is the first value, .Count is the number of values.
Operators are the usual infix set — ==, !=, <, <=, >, >=, +, -, *, /, % — with and / or (or && / ||) as connectives and ! for negation. Comparisons are date-aware.
Literals: numbers, "strings" (single or double quotes), true, false, null, arrays [1, 2, 3], and objects { key: expression }.
Functions are called by their namespaced name, category.name(arguments) — the form autocomplete offers and the editor displays. The bare name (cat(…) for string.cat(…)) also parses, but the editor rewrites it to the namespaced form:
Inside
some / all / none, $item is the current element (and $item.Field a field of it).
The editor gives live feedback as you type: syntax errors are underlined at the offending character, unknown references are flagged, and the expression is type-checked against what the field needs — a guard must produce a boolean, a number parameter a number.
What you can reference
The catalog depends on the workflow’s anchor and scope:Instances start from events with an empty variable bag, so
$instance.<name> only resolves after something in the workflow has set it. An unset variable resolves to null — a guard reading it will silently misfire. Read from the document or object when in doubt.Keywords are lists — cardinality rules
The most common source of subtly wrong conditions. A standalone keyword is always a list, even when the keyword type holds a single value:
Comparing the whole list to a single value (
$document.kw.Status == "X") is flagged by validation — use .Value or array.in instead.
Keyword groups follow the same pattern one level deeper: a single-instance group is a record whose fields are themselves lists ($document.group.Address.City.Value), and a multi-instance group is a list of records — test its instances with array.some($document.group.LineItems, $item.Amount.Value > 100). WorkView attributes are single-valued: compare them directly, no .Value.
Keyword values are usually text; when comparing to a number, convert first: number.toNumber($document.kw.Amount.Value) >= 1000.
Trigger filters
A trigger filter runs before any instance exists, so it evaluates against the event rather than workflow state, and its references differ:- Document events expose
$document.documentId,$document.documentTypeId,$document.documentTypeName, plus$document.keywords.<Name>and$document.groups.<Name>— note the namespace differs from the in-workflow$document.kw.<Name>form - WorkView events expose the event’s payload fields directly
$instance.*and$secrets.*are not available
Body templates
Free-text fields — an email body, a note text — open the template editor: normal prose with references embedded as{{path}} (no $ prefix). Typing {{ opens the same reference autocomplete, names with spaces use {{[Invoice Number]}}, and hovering a token shows the reference’s label and type.
{{= … =}}, which holds JSONLogic (see below).
Under the hood: JSONLogic
Formulas are the editing surface; the stored form is JSONLogic. You meet it when editing a definition in the designer’s JSON view, or when generating definitions programmatically (for example with an AI agent):- Guards and trigger filters are stored as raw JSONLogic objects:
{ ">=": [ { "var": "instance.amount" }, 10000 ] } - Computed action parameters are stored as
{{= JSONLogic =}}strings:"{{= { \"var\": \"instance.amount\" } =}}"— the body must be valid JSON, so a constant string keeps its quotes ({{= "Approved" =}}) - References drop the
$({"var": "instance.amount"}— always namespaced, never bare{"var": "amount"}) - The list suffixes compile away:
.Valuebecomes index.0({"var": "document.kw.Status.0"}) and.Countbecomes thecountoperator - Object literals compile to the
recordOfoperator
Rules that bite
- Secrets never appear in guards or filters. Referencing
$secrets.*in a condition is a publish-blocking error; this prevents secret values leaking through observable branching. - References are validated advisorily. Publish checks keyword, group, and attribute names against the workflow’s scope and warns on anything it can’t resolve — fix the typo or the scope.
Where to read next
Tasks & the end-user experience
What workflows look like for the people who complete tasks — the Tasks page, task forms, and workflow badges in search results.
Action reference
The parameters these expressions feed into.
