Subchapter 19.16
references/automations/special-actions.mdMarkdown18 KBView on GitHub
TL;DR
offsetExpression: "{{N}}" number literal + offsetTimeUnit) OR absolute (). Never both.dueDateExpressionmaxActivationsExpression: "{{1}}", for new steps, identifier = var() of a trigger contact/visitor identity field; preserve existing newer identifiers (§2).export default function (payload) { return <boolean>; }, the fallback for logic the condition panel can’t render; dynamicVariableExpressions MUST list every payload field the code reads — the runtime supplies ONLY those.setVariablesInfo / splitInfo are not in the public schema, so after Create/Update read the automation back and confirm the node still has its *Info — if the public API dropped it from an automation you just created in this task, tell the user and, with their OK, delete that new automation and rebuild it with a fallback (duplicate steps instead of SPLIT; formulas in the consuming fields instead of SET_VARIABLES). On an update of an existing automation, never delete or rebuild the user’s steps yourself: stop, report what was dropped, and ask.wix_automations-data_manipulation_code) can’t be created through the public API (§4).{{N}} (delay offsets, rate-limit values) must meet the panels’ whole-number limits. Reject {{0.5}}, {{.5}}, {{01}} and {{ 3 }} for both relative delays and rate-limit durations; convert fractional durations into supported smaller units (Automations Mapping Expressions (opens in a new tab) §1).*Info only, [] not omitted, namespaces per §3 there). Expression syntax: Automations Mapping Expressions (opens in a new tab).Pauses the flow before the next step. Place it anywhere except before the root RATE_LIMIT.
Relative — wait N units after the previous step finishes:
{
"id": "<uuid-1>",
"type": "DELAY",
"namespace": "DELAY-2",
"displayName": "Wait 3 days before follow-up",
"delayInfo": {
"offsetExpression": "{{3}}",
"offsetTimeUnit": "DAYS",
"postActionIds": ["<uuid-2>"]
}
}Absolute — wait until a date/time:
{
"id": "<uuid-1>",
"type": "DELAY",
"namespace": "DELAY-2",
"displayName": "Wait until 1 day before the session",
"delayInfo": {
"dueDateExpression": "{{subtractFromDate(var(\"start_date\");0;0;1)}}",
"postActionIds": ["<uuid-2>"]
}
}MUST (builder + API):
dueDateExpression. Absolute ⇒ omit offsetExpression and offsetTimeUnit.offsetExpression is a whole-number literal in braces ("{{3}}") — no var(), no formula, no decimals, never bare "3". Anything else loads in the panel as 1.offsetTimeUnit ∈ MINUTES | HOURS | DAYS | WEEKS | MONTHS (no seconds). Never write UNKNOWN_TIME_UNIT on a DELAY — but an absolute-mode DELAY reads back with offsetTimeUnit: "UNKNOWN_TIME_UNIT" filled in by the server; that is expected, don’t “fix” it. Panel limits: MINUTES/HOURS 1–999, DAYS 1–365, WEEKS 1–52, MONTHS 1–12 — convert larger values to a bigger unit.dueDateExpression (≤1000 chars) is ONE of: a bare ISO date-time literal with offset, NOT in braces ("2025-08-25T13:57:00+03:00" — the builder’s own format), "{{var(\"<date-time field>\")}}", or a single date-time formula (addToDate(date;years;months;days), subtractFromDate(...), now()). The field must exist in this node’s aggregated schema. A due date already in the past runs the next step immediately.postActionIds ≤1 id; [] ends the flow. To fan out after a delay, point it at a SPLIT.Choosing:
subtractFromDate.scheduledEventOffset (Automations Schemas and Scheduling (opens in a new tab) §5). Delays only move execution LATER.subtractFromDate(var("start_date");0;0;1)), then the reminder. A trigger offset would delay the first step too.Stops the run when the same identity already triggered it within the window; otherwise continues to its child.
{
"id": "<uuid-1>",
"type": "RATE_LIMIT",
"namespace": "RATE_LIMIT-1",
"rateLimitInfo": {
"maxActivationsExpression": "{{1}}",
"rateLimitDurationExpression": "{{24}}",
"rateLimitDurationTimeUnit": "HOURS",
"uniqueIdentifierExpression": "{{var(\"contactId\")}}",
"postActionIds": ["<uuid-2>"]
}
}MUST:
rootActionIds[0] and its postActionIds[0] is the rest of the flow. Never under a condition, delay, split, or any other step (the builder shows it as the trigger’s “frequency” footer; anywhere else the node spins forever). At most one per automation.maxActivationsExpression: "{{1}}". The builder models only “once every X” and rewrites any other value to {{1}} as soon as the user opens the panel — never promise “N times per X”.uniqueIdentifierExpression = exactly {{var("<path>")}} where <path> is a field of the TRIGGER payload schema (any object depth, not inside an array) annotated identityType: "contact" or "visitor" for compatibility with both frequency panels. Not enriched props (contact.email), not action outputs, not computed or static values (the footer disappears). If the trigger has no such field, do not claim rate limiting is impossible: the newer panel also supports string member/user identity fields and string format: uuid fields without an identityType. Availability of that creation UI varies; ask the user to configure the frequency in their builder instead of guessing support. On UPDATE, preserve an existing identifier of these newer types and its expression exactly; do not replace it with contact/visitor, remove the RATE_LIMIT, or treat it as invalid merely because it is outside the conservative creation rule. Confirm its path still exists in the trigger schema; a missing path needs resolution before saving.rateLimitDurationExpression: "{{N}}" (whole number) + rateLimitDurationTimeUnit ∈ MINUTES | HOURS | DAYS | WEEKS | MONTHS, same per-unit limits as DELAY. “Only ever once” (the panel’s unchecked “reset every” box) — only when the user explicitly wants it — is: no rateLimitDurationExpression and rateLimitDurationTimeUnit: "UNKNOWN_TIME_UNIT"; this is the one place UNKNOWN_TIME_UNIT is written (the builder writes exactly this shape). Use {{24}} HOURS, not {{1440}} MINUTES (the builder rewrites that one on load).Recipes: once per contact per day → {{1}}, {{24}} HOURS, contact field. Once per contact ever → {{1}}, no duration expression, UNKNOWN_TIME_UNIT.
The fallback for logic the visual condition panel can’t render (array indexing, per-element math, string splitting, date parts, regex). Must-know: dynamicVariableExpressions MUST list every payload field the code reads (the runtime supplies ONLY those); the builder’s JSDoc header is required; code ≤ 1000 chars. Full spec and examples: Automations Conditions (opens in a new tab) §5; when to use it: Automations Conditions (opens in a new tab) §4.
Computes named values for later steps.
{
"id": "<uuid-1>",
"type": "SET_VARIABLES",
"namespace": "setVariable",
"displayName": "Set customer greeting and tax",
"setVariablesInfo": {
"outputMapping": {
"3f2b8c1e-7a4d-4e9b-9c21-5d6f0a1b2c3d": "Dear {{var(\"contact.name.first\")}},",
"8a9e4f20-1b3c-4d5e-8f6a-7b8c9d0e1f2a": "{{multiply(toNumber(var(\"totals.subtotal\"));divide(8;100))}}",
"c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f": "{{gt(toNumber(var(\"totals.total\"));1000)}}"
},
"outputSchema": {
"type": "object",
"properties": {
"3f2b8c1e-7a4d-4e9b-9c21-5d6f0a1b2c3d": {
"type": "string",
"title": "Customer greeting"
},
"8a9e4f20-1b3c-4d5e-8f6a-7b8c9d0e1f2a": {
"type": "number",
"title": "Tax amount"
},
"c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f": {
"type": "boolean",
"title": "Premium customer"
}
},
"required": [
"3f2b8c1e-7a4d-4e9b-9c21-5d6f0a1b2c3d",
"8a9e4f20-1b3c-4d5e-8f6a-7b8c9d0e1f2a",
"c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f"
]
},
"postActionIds": ["<uuid-2>"]
}
}MUST:
namespace is the literal setVariable. Downstream: {{var("setVariable.8a9e4f20-1b3c-4d5e-8f6a-7b8c9d0e1f2a")}} (the tax amount).outputMapping and outputSchema.properties have exactly the same keys (a mapping key without a property is dropped on save). Keys are opaque, stable ids: prefer a fresh uuid v4 per key — what the builder writes for new variables; descriptive keys also load and round-trip, but get a “key should be a uuid” warning. Keys must be unique across ALL variable steps. Downstream: setVariable.<key>.{type, title, format?}: type ∈ string | number | boolean, format ∈ date | date-time | email | uri. title is REQUIRED (the visible name; a title-less variable is invisible downstream and dropped on save) and MUST be unique across ALL variable and code-variable steps in the automation (the panel refuses duplicate names). Add "required": [<every key>] to outputSchema — the panel writes it on save; other property fields such as description are stripped. Exception on UPDATE: preserve an existing wixCustomType: "IMAGE_URL" together with type: "string" and format: "uri"; it distinguishes an image variable from a general URL. The builder preserves this metadata even when its image-variable picker is unavailable. Do not normalize it away or rename its stable variable key.setVariable, with distinct stable keys and titles. If Validate reports a missing same-step variable, move the dependent mapping to the second node; do not replace that dependency with a fresh calculation from the trigger.{{var(...)}}, and functions from the builder list in Automations Mapping Expressions (opens in a new tab), using eq/gt/gte/lt/lte for NUMBERS only (they reject string and boolean arguments — for a boolean use the value itself or not(...), for strings stringEq); no unclosed {{. A number/boolean variable holds a JSON literal of that type or exactly one whole {{…}} — never plain text. NEVER a formula in a date-time variable (the panel replaces it with “now”); a literal date-time is re-saved in the site timezone. Anything else → the §4 alternatives (code variables can’t be created publicly).Sequential dependency example: SET_VARIABLES A → SET_VARIABLES B → consuming action.
A stores a numeric value under fresh UUID key <key-a> using the verified trigger field.
B stores a numeric tenth of that saved value under another fresh UUID key <key-b>, with
outputMapping["<key-b>"] = '{{divide(var("setVariable.<key-a>");10)}}'.
For each node, include its own key in outputSchema.properties and required, with the
requested title and type: "number". A’s postActionIds points to B, and B’s points to the
consumer. Keep both nodes and B’s reference to A’s key in the saved read-back; a single node
that computes both values independently does not preserve the requested sequence.
Code variables are not available through the public API. The builder’s code-variable step (APP_DEFINED wix_automations-data_manipulation_code, namespace: "setVariable") is not in Resolve Actions, and Create/Update refuse an automation containing a new one (PermissionDenied “Unauthorized automation creation or update”) even when Validate says VALID. For a value the panel can’t express (regex, splitting, day of year, joining a list, time between dates), in this order: a formula in the consuming field or a SET_VARIABLES step, if the §2 function list covers it; a CODE_CONDITION when the value only decides a branch (it creates fine); a “Generate or analyze text” (wix_automations-llm_call) step with an override output schema to extract or compute a text value; otherwise save without it and tell the user to add a code variable in the builder. An existing code variable in an automation you update: leave it untouched and keep its keys.
Variables are transformation, not extraction: they cannot pull one element out of an unindexable array. A code condition can inspect the declared array to decide a branch. For a scalar needed downstream, first find wix_automations-llm_call with Resolve Actions, then read the selected site’s Get Runtime Action response’s action.inputSchema (Automations API Catalog (opens in a new tab) §4). Use that LLM text action only after verifying how this schema accepts all required source data: a scalar prompt needs a verified serialized-text source, not a raw array-returning expression, an invented context field or an assumed array-to-string conversion. If no supported input or scalar source exists, explain that specific blocker. Do not add extraction for an initializer field whose own contract explicitly supports array indexes.
Every path ALWAYS runs, in parallel. It is not a condition.
{
"id": "<uuid-1>",
"type": "SPLIT",
"namespace": "SPLIT-2",
"displayName": "Notify customer and team",
"splitInfo": {
"paths": [
{
"id": "<uuid-p1>",
"name": "Notify customer",
"displayName": "Notify customer",
"postActionId": "<uuid-2>"
},
{
"id": "<uuid-p2>",
"name": "Notify team",
"displayName": "Notify team",
"postActionId": "<uuid-3>"
}
]
}
}MUST:
splitInfo — no postActionIds.configuration.actions.name 1–100 chars is the label; optional displayName ≤60 overrides it (the builder sets it only on rename — same text is fine). postActionId = first action of that path, or omitted for an empty path.Editing an existing SPLIT: add a branch by appending one path (new id, new first action); remove one by deleting that path plus every node below it; if one path would remain, delete the SPLIT and connect its parent (or rootActionIds) straight to that path’s first action — the builder does the same. Never linearize paths into a sequence — that changes behavior. Adding a SPLIT in the builder UI may be feature-gated for the user, but drawing and editing an existing one always works.
The API defines MERGE + conditionInfo.mergeActionId, but the builder cannot draw them. To let both condition branches continue into shared steps, DUPLICATE those steps into each branch (Automations Graph and Data Model (opens in a new tab) §2).
displayName.