Subchapter 19.13
references/automations/conditions.mdMarkdown22 KBView on GitHub
TL;DR
CONDITION must be something the builder’s condition panel can draw: exactly ONE group in orExpressionGroups, operator "AND" or , 1–20 . Each expression is ONE function from §2 (optionally inside — only the negatable ones), with the field as and a value."OR"booleanExpressionsnot(…)var()CODE_CONDITION (§5).stringContains(["<id>"];var("field")), never a display name.truePostActionIds / falsePostActionIds: at most ONE id each; branch children have this condition as their only parent (Automations Graph and Data Model (opens in a new tab)).{
"id": "<uuid-1>",
"type": "CONDITION",
"namespace": "CONDITION-1",
"displayName": "VIP order?",
"conditionInfo": {
"orExpressionGroups": [
{
"operator": "AND",
"booleanExpressions": [
"{{stringEq(var(\"customer_tier\");\"premium\")}}",
"{{numberGt(var(\"order_count\");5)}}"
]
}
],
"truePostActionIds": ["<uuid-2>"],
"falsePostActionIds": []
}
}MUST:
orExpressionGroups with an explicit operator ("AND" = all hold, "OR" = any). Several groups are flattened into one on first open and their logic is lost; a missing operator is saved as OR.and(…)/or(…) inside an entry.var() path exists in THIS step’s aggregated schema (trigger + ancestors; Automations Graph and Data Model (opens in a new tab) §4). A path the panel can’t find marks the step invalid and its canvas label stays empty.{{ stringEq(… makes the function unreadable).conditionInfo on the step; both branch arrays present; never both empty (dead end). Set a short displayName.The builder ships two panel layouts (a dropdown and a newer property picker) and users get either. Everything below survives both; when a user edits a condition, the property picker may rewrite arraySome/arrayEvery into arrayIncludes* forms — that is outside your control; never write arrayIncludes* yourself (see the list below).
Classify the field from its schema, in this order: format: "date" → date · format: "date-time" → date-time · type: "string" + format: "number" → number · has enum (or string/number oneOf of consts) → enum / number-enum · integer → number · string with itemsSelectionConfiguration (providerKey or tag) → item selection · otherwise its type.
| Field type | Operator (UI) | Expression (¬ = may wrap in not(…)) |
|---|---|---|
| string | is / is not ¬ | stringEq(var("f");"v") |
| contains / doesn’t contain ¬ | stringHasSubString(var("f");"v") | |
| number | equals / not equals ¬ | numberEq(var("f");5) |
| greater / less than | numberGt(var("f");5) · numberLt(var("f");5) | |
| date | is / is not ¬ | dateEq(var("f");"2026-12-25") |
| before / after | dateBefore(…) · dateAfter(…) | |
| date-time | is / is not ¬ | dateTimeEq(var("f");"2026-12-25T10:00:00Z") |
| before / after | dateTimeBefore(…) · dateTimeAfter(…) | |
| boolean | is true / is false | boolEq(var("f");true) · boolEq(var("f");false) |
| enum (string) | is / is not ¬ | stringEq(var("f");"PAID") |
| is one of / is not one of ¬ | stringContains(["PAID","PENDING"];var("f")) — list FIRST | |
| number enum | is ¬ / is one of ¬ | numberEq(var("f");2) · numberContains([1,2];var("f")) |
| item selection (id) | is one of / is not one of ¬ | stringContains(["<id-1>","<id-2>"];var("f")) |
| array of primitives | includes any of / doesn’t ¬ | arraySome(var("f");contains(["a","b"];$_)) |
| includes only | arrayEvery(var("f");contains(["a","b"];$_)) | |
| includes all of | one arraySome(var("f");contains(["a"];$_)) entry per value in an AND group (see below) | |
| array of objects | same as array of primitives | var("f") → arrayMap(var("f");"<item.property>") — ONLY the property named by the array’s defaultConditionedProperty |
| any type | is empty / is not empty ¬ | isEmpty(var("f")) |
NEVER:
not(…) around anything not marked ¬ (numberGt, numberLt, date*Before/After, boolEq, arrayEvery) — the panel loses the operator. Negating boolEq(…;true) can use boolEq(…;false). NEVER negate a strict comparison by swapping Gt/Lt or Before/After: not(x > N) means x <= N, including equality. Preserve that boundary with comparison + equality in a single OR group when the full logic fits (§4), or use CODE_CONDITION. For example, “not more than 100” includes 100; “not before a date” includes that date. Do not invent an inclusive visual operator.arrayIncludesAnyOf/AllOf/Only(…) — the dropdown layout reads all three as “includes only” and re-saves them with that meaning.arrayEvery(["a","b"];contains(var("f");$_)) — the property-picker layout cannot read it. For “includes all of” use one arraySome entry per value with operator: "AND"; if the group must be OR, chain conditions (§4) or use code.arrayMap to any property other than defaultConditionedProperty (or on an array without one) — the dropdown layout re-points it or fails to save. The path is relative to the item ("service.id") and must exist in the item schema; otherwise → CODE_CONDITION.lower(var("f")), month(var("d"))), var() or now() as the value, two fields compared, eq/gt/contains at top level, isEmptyString, startsWith, if, and/or. These are valid formulas but not drawable.Literal rules (the value inputs reject or rewrite others):
| Value | Rule |
|---|---|
| string | ≤ 2500 chars; must not contain " or ' (the input flags it) — test a quote-free part with stringHasSubString, or use code |
| number | Unquoted; between −100000000 and 100000000; decimals such as 0.5 and -0.25 are supported; no omitted leading zero (.5), leading zeros (05) or exponents |
| boolean | true/false unquoted (a quoted "yes" is re-saved as a broken bare word) |
| enum | Exactly one of the schema’s enum/const values, same case |
| date | "YYYY-MM-DD"; date-time: full ISO with zone ("2026-12-25T10:00:00Z"); never relative (now()) — relative windows → code or a set-variables boolean |
Date values and time zone: the panel writes a picked date/time as wall-clock time in the site
time zone with its offset (a date → that day’s 00:00 site time), and URI-encodes it like every
literal ("2026-12-31T23%3A59%3A00%2B02%3A00"). Its reader decodes first, so an encoded and an
unencoded ISO literal load the same (a "YYYY-MM-DD" literal is unchanged by encoding). Treat a
calendar date the user names as a site-time-zone date: “valid until Dec 31” → compare with the end
of that day in the site’s zone (Site Properties timeZone, Automations Schemas and Scheduling (opens in a new tab) §4).
Semantics: stringEq/stringHasSubString/stringContains are case-insensitive. No ≥/≤: for integers numberGt(var("f");N-1); otherwise numberGt + numberEq in an OR group (only if the group may be OR) or code. $_ appears bare, only inside contains(…).
A field with itemsSelectionConfiguration (service, form, product, label, coupon, location, pipeline stage, …) carries an id or key. A display name passes validation and silently never matches. Get ids from the owning vertical’s documented public list, search or get endpoint for this entity type, or the user. Confirm a supplied id with a successful read of that entity. If the field’s selection configuration restricts eligible entities to a parent (such as a form) or fixed criteria, accept only ids that meet those restrictions. Never construct an entity-picker endpoint URL from the provider key or call the builder’s internal picker endpoint directly. Ids are whatever that API returns — label keys look like custom.vip-customers, not uuids.
service_id: {{stringContains(["<service-id>"];var("service_id"))}}lineItems[].rootCatalogItemId: {{arraySome(arrayMap(var("lineItems");"rootCatalogItemId");contains(["<product-id>"];$_))}} — visual only when that property is the defaultConditionedProperty; else code: (payload.lineItems || []).some(i => ids.includes(i.rootCatalogItemId))labelKeys.items: {{arraySome(var("labelKeys.items");contains(["<label-key>"];$_))}}Never synthesize [0], .0, firstItem or a dotted path through an array. “The order contains product X” is a whole-array test — keep it one. If the user needs one specific item, clarify. If you cannot obtain the ids, the entity set is unknown, not empty: tell the user and ask which entity to use.
(A AND B) OR C / mixed AND+OR / anything needing more than one group → one CODE_CONDITION (§5) with exactly that logic — the default; it keeps a single true/false branch. Chaining two visual conditions (A AND B → false: condition C) is an alternative only when the true branch is one small step, since that step must then be duplicated (tree only — duplicate, never join; Automations Graph and Data Model (opens in a new tab)). Never write several orExpressionGroups.CODE_CONDITION, or compute a boolean with startsWith / endsWith in a preceding set-variables step and test it with boolEq. Never silently substitute a substring test; explain any proposed change in semantics and obtain the user’s agreement before building it.CODE_CONDITION (§5), or compute a boolean with a set-variables step (Automations Delays Variables and Branches (opens in a new tab)) and test it with boolEq(var("setVariable.<key>");true)not(isEmpty(…)), then the test), or codePrefer a visual CONDITION when it expresses the logic exactly — users can keep editing it. Don’t force complex logic into it; if the user insists, explain what can’t be drawn. Never say the logic is impossible when code can do it.
Before constructing a calendar condition: obtain the IANA time zone from current tool-returned site metadata for the exact target site, or read Site Properties (Automations Schemas and Scheduling (opens in a new tab) §4) if that metadata is absent. A zone from another site, documentation, memory or an example is not evidence. Validate the returned zone and replace <SITE_TIME_ZONE> before saving; a confirmed literal is expected in saved code.
{
"id": "<uuid-3>",
"type": "CODE_CONDITION",
"namespace": "CONDITION-2",
"codeConditionInfo": {
"snippet": {
"language": "JAVASCRIPT",
"code": "/**\n * @param {Payload} payload - Automation payload with contact birthdate.\n * @returns {boolean} whether today is the contact's birthday at the site.\n */\nexport default function (payload) {\n const b = payload.contact?.birthdate;\n if (typeof b !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(b)) return false;\n const [y, m, d] = b.split('-').map(Number);\n const days = [31, y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n if (!y || !m || m > 12 || !d || d > days[m - 1]) return false;\n try {\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone: '<SITE_TIME_ZONE>',\n calendar: 'gregory',\n numberingSystem: 'latn',
},
"dynamicVariableExpressions": ["{{var(\"contact.birthdate\")}}"],
"truePostActionIds": ["<uuid-4>"],
"falsePostActionIds": []
}
}The birthday example requires a confirmed IANA site time zone (Automations Schemas and Scheduling (opens in a new tab) §4).
A birthdate is a date-only YYYY-MM-DD: reject impossible Gregorian dates (including
February 29 in a non-leap birth year and year 0000), then compare its literal month/day
with today’s calendar parts in that zone. Do not parse the birthdate as a UTC instant or use the runtime’s local
getMonth() / getDate(). An unresolved or invalid zone blocks saving; the runtime guard is
only a fallback. A February 29 birthday matches February 29, not an invented non-leap-year date.
Minimal array check (the code reads only the declared array):
"displayName": "Check if order has 3+ items",
"codeConditionInfo": {
"snippet": { "language": "JAVASCRIPT",
"code": "/**\n * @param {Payload} payload\n * @returns {boolean}\n */\nexport default function (payload) {\n return (payload.lineItems || []).length >= 3;\n}\n" },
"dynamicVariableExpressions": ["{{var(\"lineItems\")}}"],
"truePostActionIds": ["<uuid-2>"], "falsePostActionIds": [] }Numeric values in code. Payload numbers can arrive as strings, including fields with
format: number. A bare typeof value === 'number' rejects those valid values. Convert only
numbers or nonblank strings, then require Number.isFinite before arithmetic or comparison.
Do not turn missing, null or blank values into zero unless the user’s rule explicitly requires it.
For a same-line quantity/price check, use the actual schema paths in place of these example names
and declare the entire lineItems array in dynamicVariableExpressions:
{
"id": "<uuid-5>",
"type": "CODE_CONDITION",
"namespace": "CONDITION-3",
"displayName": "Same item has quantity 3+ and price over 50",
"codeConditionInfo": {
"snippet": {
"language": "JAVASCRIPT",
"code": "/**\n * @param {Payload} payload\n * @returns {boolean}\n */\nexport default function (payload) {\n const number = value =>\n typeof value === 'number'\n ? value\n : typeof value === 'string' && value.trim() !== ''\n ? Number(value)\n : NaN;\n return (\n Array.isArray(payload.lineItems) &&\n payload.lineItems.some(item => {\n const quantity = number(item?.quantity),\n price = number(item?.unitPrice);\n return (\n Number.isFinite(quantity) &&\n Number.isFinite(price) &&
},
"dynamicVariableExpressions": ["{{var(\"lineItems\")}}"],
"truePostActionIds": ["<uuid-6>"],
"falsePostActionIds": []
}
}MUST:
type: "CODE_CONDITION" with only codeConditionInfo; namespace follows the condition pattern (CONDITION-N, unique).language is always JAVASCRIPT. The code default-exports a synchronous function (payload) with exactly one parameter, starts with the JSDoc block the builder’s template has (/** @param {Payload} payload … @returns {boolean} … */ — required), and returns an explicit boolean. No imports, no network calls. Whole code ≤ 1000 characters; newlines escaped in JSON.?., Array.isArray, || [], typeof, try/catch for deep paths); a missing field must not throw. Numeric conversion follows the rule above, not an unconditional || 0 default.payload.field, payload['field-with-hyphens'], action outputs payload['<namespace>'].field, payload.contact.email, payload.setVariable['<key>']. JS may index arrays and use .some/.every/.filter and Date math; new Date() gives the current time for calendar checks.dynamicVariableExpressions: one {{var("<path>")}} for EVERY field the code reads — the runtime hydrates ONLY these, so an undeclared field is undefined. Arrays: declare the array itself (payload.lineItems.some(…) → {{var("lineItems")}}), never items or indexes; reading an object whole needs the object declared; if a path has no valid var(), declare its closest valid parent. Never payload[computedKey] or the whole payload. [] only when the code reads no field. Every path must exist in this node’s aggregated schema.Other special steps (delay, rate limit, set variables, split): Automations Delays Variables and Branches (opens in a new tab).
AND/OR; 1–20 expressions; no nested and/or; no stray whitespace.not() only on ¬ rows; no arrayIncludes*, no collection-first arrayEvery..5, 05 or exponent numbers — 0.5 and -0.25 are valid; exact enum values; unquoted booleans).var() path exists in this step’s aggregated schema; arrayMap only onto defaultConditionedProperty.namespace unique.CODE_CONDITION meeting §5.