Skip to content

Documentation

Build with Engage

Connect customer data, business events, and communication channels to build automated journeys with workflows and AI.

Workflows

What happens after an event. A workflow waits, checks whether it is still worth acting, and then acts — which is the whole product in one sentence.

Owners only

Creating a workflow, drafting one from a description, publishing a version and enabling, disabling or renaming one need the owner role — each of them changes what gets sent to customers automatically. Anyone in the workspace can read workflows and their runs.

The dashboard builds these for you - Workflows, then New workflow - from recipes such as order confirmations and abandoned carts. This page is the same thing through the API.

Drafting one from a description

POST /v1/workflows/draft takes a sentence - { "wanted": "when someone asks about a product and does not buy, wait a day and send the follow up" } - and returns a definition wired up from what this workspace actually has: its triggers, its approved templates, the checks and actions below. It needs the AI assistant switched on, it counts against the workspace’s monthly AI spend, and it is owners only.

Nothing is saved and nothing is switched on. The response carries definition for you to review, create and enable yourself, problems naming anything still wrong with it, notes for whoever reads it, and - when none of your triggers fitted - newTrigger in the shape POST /v1/triggers takes. In the dashboard the draft opens on the canvas as unsaved changes.

Triggers of your own

Engage notices four things by itself - a conversation starting, a message arriving, a conversation going quiet after a number of hours you set, a product being asked about - and it accepts any event your own systems send to POST /v1/events. Between those sits everything a business selling inside WhatsApp wants to act on, and nothing pushes it, because there is nothing to push from.

So you can write the condition instead. A trigger rule states something like “no message from them for 7 days”, “bought before but not in 90 days”, “asked about a product in the last 14 days and has not bought”, or “carries the tag vip”. Engage checks every few minutes and records an event the first time each customer matches - once per occasion, not once per check - and your workflow starts on that event like any other.

Channel is a filter, not part of the rule

A customer who has gone quiet has gone quiet wherever they were. A rule watches every channel unless you narrow it, so the one you write today still means what you meant the day another channel is connected.

FieldTypeNotes
GET /v1/triggersany memberThe workspace’s saved triggers.
POST /v1/triggersownername, signal, optional channel, params.
PUT /v1/triggers/{id}ownerEverything but the event type.
PATCH /v1/triggers/{id}ownerTurn it on or off.
DELETE /v1/triggers/{id}ownerEvents it recorded stay.

The shape

A workflow is a trigger event type and a definition: a map of named steps, and which one starts. Here is the one the product exists for — chase a customer who asked a price and went quiet.

POST /v1/workflows

{
  "name": "Chase the quiet ones",
  "triggerEventType": "price.quoted",
  "enabled": false,
  "definition": {
    "start": "wait",
    "steps": {
      "wait":    { "type": "delay", "duration": "PT24H", "next": "replied" },
      "replied": { "type": "condition",
                   "check": "customer.replied_since_trigger",
                   "then": "done", "else": "chase" },
      "chase":   { "type": "action", "action": "send_template",
                   "with": { "template": "follow_up", "language": "en_US" },
                   "next": "done" },
      "done":    { "type": "stop", "reason": "finished" }
    }
  }
}

enabled is optional. Left out, the workflow is switched on the moment it is created; false saves it switched off until you turn it on with PATCH.

Four step types, and no more

FieldTypeNotes
delaystepWaits. duration is ISO-8601 — PT2H, P3D. The run is parked in the database, not on a thread, so it survives a restart.
conditionstepBranches on a named check. then and else both name a step.
actionstepDoes something. Two are available, below.
stopstepEnds the run with a reason, which is recorded on its history.

Conditions

customer.replied_since_trigger    did they write back after the event?
customer.opted_out                have they unsubscribed?
conversation.within_window        is their 24-hour window still open?
customer.ordered_since_trigger    have they placed an order since?

customer.ordered_since_trigger is what makes cart reminders safe: start the workflow on checkout.started, wait, and stop if an order.created has arrived for that customer since. The event that started the run never counts.

The first is the one that matters. Chasing somebody who already replied is worse than not chasing at all — it tells them nobody is reading.

The list is closed. A definition naming any other check is refused when you save it, with the valid names in the error, rather than being treated as false at run time — which would quietly send the follow-up.

conversation.within_window is false when the window has shut and when there is no conversation with the customer yet. It exists so a definition can branch on the window rather than finding out at send time — but today both actions work outside it, so it is mostly useful for recording which side of the window a run landed on.

Actions

send_template   with: { template, language, variables }
record_event    with: { type }
notify_team     with: { how, to, note, subject }

language defaults to en_US, as it does for a reply. variables fills the template's {{1}}, {{2}} slots, each from exactly one source:

"with": {
  "template": "order_shipped",
  "variables": {
    "1": { "from": "customer.name", "fallback": "there" },
    "2": { "from": "event.orderNumber" },
    "3": { "text": "Lekki branch" }
  }
}
FieldTypeNotes
customer.namefromAlso customer.phone, customer.email and customer.externalId.
event.<field>fromA field of the data sent with the event that started the run. Dots reach nested fields: event.order.number.
textstringThe same words every time.
fallbackstringUsed when a from value is missing or blank.

A value that is missing with no fallback is not sent with a gap in it. The step is refused, and the run's history names the variable and the missing field. Line breaks and runs of spaces are flattened, because WhatsApp refuses them inside a variable - an address from a checkout form is where they usually come from.

A template with a website button whose link ends in a variable - a “Track order” button - needs one more key, "url", filled the same way: "url": { "from": "event.trackingNumber" }.

Stopping early

A condition step checks just before the step after it. cancelOn stops a waiting run the moment something happens, wherever it is and however many messages it had left:

{
  "start": "wait",
  "cancelOn": ["message.received", "order.created"],
  "steps": { ... }
}

When an event of one of those types is recorded for a customer, every run of this workflow waiting for that customer stops, and its history says which event stopped it. The run an event starts is never stopped by that same event - so a workflow that starts on checkout.started and also stops on it keeps only the newest cart's reminder. In the dashboard it is Stop early if: they send you a message, or they place an order.

record_event is how one workflow feeds another: the event it records can trigger a second workflow, which is how longer sequences are built without a step type for it. Subscribe to that event under Settings → Webhooks and your own systems receive it too - the same door, from the other side.

notify_team tells you rather than the customer. how is email or whatsapp, to is an address or an E.164 number, and note is whatever you want said; the customer's name and number are added for you. For email, subject is the subject line.

WhatsApp to your own team still needs a template

A message to somebody who has not written to you is business-initiated, and Meta asks for an approved template whether that somebody is a customer or your own colleague. So subject carries the template name for how: whatsapp, with the customer in {{1}} and your note in {{2}}. It is sent straight down the channel - no conversation is opened and no consent is recorded, because your colleague is not a customer and should not appear as one.

send_template needs a synced, approved template

When the template has been synced, saving checks that the variables match its slots. When it has not, the workflow saves and the name is only checked when the step runs, which may be a day later against a real customer. Sync from Meta first.

Versions

POST /v1/workflows/{id}/versions

Publishing a new definition creates a version rather than overwriting. Runs already in flight continue on the version they started with — a customer half way through a three-day sequence does not switch to new logic overnight, which would produce a follow-up that makes no sense given the message before it.

Enable, disable, rename

PATCH /v1/workflows/{id}

{ "enabled": false }

Send only the fields you mean to change

This is a real incident, not a hypothetical. A workflow was PATCHed from an API console with every field populated by the tool's placeholder defaults — triggerEventType became the literal string "string" and enabled became false. Events then matched nothing, silently, and the count of matched workflows read zero for a day before anybody worked out why.

Watching a run

GET /v1/workflows/{id}/runs
GET /v1/workflows/runs/{runId}

The second returns the run's full history: every step entered, every condition and how it evaluated, every action and what it returned. That history is the answer to “why did my customer get this?”, and it is why refusals are recorded rather than thrown away — a run that was told the window had closed says so, in order, with a timestamp.

Next

Suppression →

Who must never be messaged, and why no caller can route around it.