PlunkPlunk
Concepts

Workflows

Build automated, multi-step contact journeys triggered by events, segments, schedules, or manual entry

Workflows are graph-based automations that move contacts through a sequence of steps — sending emails, waiting for activity, branching on conditions, calling webhooks, updating contact data, or exiting. Each contact that enters a workflow runs through it independently as a workflow execution.

Triggers

A workflow has a single trigger that decides how contacts enter:

Trigger typeWhen it fires
EVENTAny time a matching event is tracked on a contact (custom event via /v1/track, system events such as email.received or contact.subscribed, or segment entry/exit events).
MANUALOnly when you explicitly start an execution via POST /workflows/:id/executions.
SCHEDULEOn a recurring schedule defined by triggerConfig (e.g. cron expression).

When you create a workflow via the API, the trigger defaults to EVENT with the event name you provide. To use MANUAL or SCHEDULE, change triggerType and triggerConfig with PATCH /workflows/:id after creation.

A workflow's trigger event name cannot be changed after the first execution — pick it carefully. Other trigger configuration (delays, conditions inside steps) remains editable.

Lifecycle and the enabled flag

Workflows are created with enabled: false. No executions start until you flip enabled to true — this is the most common reason "my workflow isn't firing."

Entry rules

Each workflow decides who can enter it when its event fires. In the dashboard this is Who can enter in the workflow's settings; through the API it is allowReentry and throttle.

RuleAPIA contact enters…
Once per contact (default)allowReentry: falseOne time, ever. Use it for welcome and onboarding sequences.
Every time the event firesallowReentry: trueAgain on each event, even while an earlier run is still waiting in a delay.
At most once per periodallowReentry: true, throttle: "24h"At most once per period, and never while an earlier run is still going.

Throttling entry

throttle takes the same value as delivery.throttle on POST /v1/send, so an event you track from a busy code path starts the workflow at most once per period. Events inside the period are ignored: no run is started and nothing is stored for them.

{ "allowReentry": true, "throttle": "24h" }
{ "allowReentry": true, "throttle": { "window": "24h", "key": "event.orgId" } }
{ "allowReentry": true, "throttle": { "key": "event.period" } }
  • window: from 1 minute ("1m") to 365 days ("365d"). The period starts when the contact enters, because a run may branch or exit without sending anything.
  • key: a field to count separately for, read like a condition step's field: event.* from the event's data, data.* from the contact's data. With event.orgId, a person in two organisations enters once per period for each. If the field is missing on an event, it counts per contact.
  • A key without a window enters once per value, for example once per billing period when the event carries period: "2026-10".
  • Setting a throttle turns allowReentry on. Setting allowReentry: false or throttle: null removes it.

Starting a throttled workflow manually with POST /workflows/:id/executions returns 409 with the time the contact can enter again. Changing the throttle applies to the next entries; contacts already held keep their period.

Step types

A workflow always begins with a single auto-created TRIGGER step. You build the rest of the graph by adding steps and transitions (edges) between them.

Step typePurposeRequired config
TRIGGERAuto-created entry point. Holds the trigger configuration.eventName (for EVENT trigger)
SEND_EMAILSends an email to the contact using a template. Template variables resolve from contact data + execution context.templateId, optional from override
DELAYPauses the execution for a fixed duration before continuing.amount, unit (minutes / hours / days)
WAIT_FOR_EVENTPauses until a specified event is tracked on the contact, with a timeout fallback.eventName, timeout (seconds; 0 or omitted waits with no limit)
CONDITIONBranches the execution based on contact data or event data. Each CONDITION step has two outgoing transitions tagged yes / no.A filter expression (same shape as segment filters)
WEBHOOKCalls an external HTTPS endpoint with contact + execution context as the JSON body. url, header values, and body support {{variables}}.url, optional method, headers, body
UPDATE_CONTACTPatches contact data — useful for tagging contacts as they progress ({ stage: "activated" }).data object
EXITTerminates the execution. Optionally records an exitReason for analytics.optional reason

Transitions

Transitions are the edges of the workflow graph. For most step types a transition is a simple "next" pointer. For CONDITION steps, each transition carries a branch discriminator ("yes" or "no") so the engine knows which path to take when the condition evaluates.

When deleting a step in the middle of a chain, pass ?splice=true on DELETE /workflows/:id/steps/:stepId to automatically reconnect the surrounding transitions; otherwise the deletion leaves the graph disconnected.

Executions

Each contact entering the workflow creates a WorkflowExecution. Executions move through these states:

StatusMeaning
RUNNINGCurrently processing a step (or about to).
WAITINGPaused inside a DELAY or WAIT_FOR_EVENT step.
COMPLETEDReached the end of the graph successfully.
EXITEDHit an EXIT step. exitReason records why.
FAILEDAn unrecoverable error occurred (e.g. webhook returned non-2xx after retries, template not found).
CANCELLEDCancelled manually via the API or dashboard. Pending delays and waits do not resume it; a step already running finishes, and an email it queued is still sent.

Each execution carries a context JSON object that's merged with the contact's data when rendering templates and evaluating conditions. When you start an execution manually with POST /workflows/:id/executions, you can pass an initial context — this is how you parameterise per-execution variables (a coupon code, a referrer name) without storing them on the contact.

Locking active workflows

A workflow is locked for structural edits while it has active (RUNNING or WAITING) executions. To make breaking changes — adding/removing steps, changing the trigger event, switching trigger type — first either:

  • Disable the workflow and wait for active executions to complete (an execution waiting on a WAIT_FOR_EVENT step with no timeout only completes when its event arrives), or
  • POST /workflows/:id/executions/cancel-all to cancel everything in flight.

Cosmetic changes (renaming, editing template content referenced by steps) don't require this.

Common patterns

  • Welcome series — EVENT trigger on signed_up, send welcome email, delay 2 days, send tips email, delay 5 days, send upgrade nudge.
  • Inbound auto-reply — EVENT trigger on email.received, CONDITION on event.spamVerdict == "PASS", then SEND_EMAIL with an auto-reply template. See Receiving emails.
  • Re-engagement — Triggered by a segment exit (segment.active-users.exit), send a "we miss you" email, then wait up to 7 days for an email.open event (an open of any email counts) before continuing.
  • Webhook fan-out — EVENT trigger on purchase.completed, WEBHOOK step to your CRM, UPDATE_CONTACT to tag { tier: "customer" }, SEND_EMAIL with the receipt.

API reference

Workflows are managed through the /workflows API. See the API overview for the full route list — the API exposes endpoints for the workflow itself, its steps, its transitions, and its executions.

What's next