Automations

Workflows

Build conversation flows that run on one messaging account when a trigger fires: send messages, wait for answers, branch, call your backend and hand off to a person.

A workflow is a graph of steps that runs on one messaging account. When its trigger fires (a DM, a link click, a comment, a story mention, a call from your backend) Pozy starts a run for that contact and walks the graph: send a message, wait for the reply, branch on it, tag the contact, call your webhook, hand off to a person.

Workflows run on WhatsApp, Instagram, Facebook, X, Bluesky and TikTok. Telegram is coming soon. For a simple keyword reply under a post, a comment automation is less work.

#Your first workflow

This Instagram workflow answers anyone who writes "pricing", asks which plan they want, and tags them by answer. It is created as a draft, then activated.

curl -X POST https://api.pozy.ai/v1/workflows \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "6650c1e2a3b4c5d6e7f80911",
    "accountId": "6650c1e2a3b4c5d6e7f80a22",
    "platform": "instagram",
    "name": "Pricing questions",
    "nodes": [
      { "id": "start", "type": "trigger", "config": { "triggerType": "inbound_message", "keywords": ["pricing", "price"], "matchType": "contains", "cooldownHours": 24 } },
      { "id": "ask", "type": "send_message", "config": { "messageType": "text", "text": "Happy to help, {{contact.name}}. Which plan are you looking at?",
        "quickReplies": [ { "title": "Solo", "payload": "PLAN_SOLO" }, { "title": "Team", "payload": "PLAN_TEAM" } ] } },
      { "id": "wait", "type": "wait_for_reply", "config": { "timeoutMinutes": 1440, "saveAs": "plan" } },
      { "id": "route", "type": "condition", "config": { "rules": [ { "id": "team", "variable": "quickReply.payload", "operator": "equals", "value": "PLAN_TEAM" } ] } },
      { "id": "tag_team", "type": "add_tag", "config": { "tag": "lead-team" } },
      { "id": "handoff", "type": "handoff", "config": { "note": "Team plan lead, reply within the hour" } },
      { "id": "solo", "type": "send_message", "config": { "messageType": "text", "text": "Solo is $12 a month. Start here: https://acme.example/solo" } },
      { "id": "done", "type": "end" }
    ],
    "edges": [
      { "id": "e1", "source": "start", "target": "ask" },
      { "id": "e2", "source": "ask", "target": "wait" },
      { "id": "e3", "source": "wait", "target": "route", "sourceHandle": "reply" },
      { "id": "e4", "source": "wait", "target": "done", "sourceHandle": "timeout" },
      { "id": "e5", "source": "route", "target": "tag_team", "sourceHandle": "team" },
      { "id": "e6", "source": "route", "target": "solo", "sourceHandle": "default" },
      { "id": "e7", "source": "tag_team", "target": "handoff" },
      { "id": "e8", "source": "solo", "target": "done" }
    ]
  }'

The create call returns the draft with its id. Only an active workflow starts runs, so a workflow that "does nothing" is usually still draft or paused.

#Lifecycle

Status Meaning Move with
draft Saved, not running POST /v1/workflows/{id}/activate
active Starts runs when its trigger fires POST /v1/workflows/{id}/pause
paused Starts no new runs. Runs already in progress finish POST /v1/workflows/{id}/activate

Creating validates the structure: node ids must be unique, edges must point at existing nodes, and WhatsApp-only nodes cannot appear on other platforms. Activating also requires exactly one trigger node and a graph where every node can be reached.

#Triggers

Every workflow has one trigger node. Its config.triggerType decides what starts a run.

triggerType Starts when Platforms
inbound_message (default) The account receives a DM. Filter with keywords and matchType (any, contains, exact, regex). onlyFirstMessage: true fires once per contact All workflow platforms
referral Someone opens the chat from an ig.me or m.me link with a ref, or a click-to-message ad. Narrow with referral: { ref, matchType } (exact, prefix, any) Instagram, Facebook, WhatsApp
comment Someone comments on a post. Filter with comment: { keywords, matchType, platformPostId } Instagram, Facebook
story_mention Someone mentions the account in their story Instagram
reaction Someone reacts to a message. Filter with reaction: { emojis } Instagram, Facebook, WhatsApp, Telegram groups
whatsapp_event A status arrives for a message the account sent (eventType: message_sent, message_delivered, message_read, message_failed, reaction) WhatsApp
lead_received Someone submits a Meta lead ad form. The run messages the phone number on the form WhatsApp
api_call Your backend starts a run All workflow platforms

Cooldown. Within cooldownHours (1 to 720, 24 by default) of a run, the same contact cannot start the workflow again. Every trigger respects it except comment and api_call. If a platform delivers one event twice, the workflow still starts only once.

Comment runs have no conversation yet. Put a send_message right after the trigger: it is sent as the private reply to the comment, and the rest of the run happens in the chat that reply creates. That message can be text, with buttons or quick replies if you like, or cards. For commenters who do not follow you, Instagram delivers plain text only, so keep it simple. When an active comment automation also matches the comment, the automation replies and the workflow stays idle.

Lead runs start only for leads submitted in the last hour, after activation, that ticked the consent checkbox named in leadReceived.consentCheckboxKey and whose phone number parses. Scope them with formIds or pageAccountIds. Every message the run sends before the person answers must be an approved WhatsApp template, because they have never written to you.

#Graph, edges and branches

nodes[] and edges[] make the graph. Each node has a stable id, a type and a config; an optional label (up to 80 characters) and position are for the visual builder. Each edge links source to target. A node with several outcomes picks the edge whose sourceHandle matches:

Node sourceHandle values
condition The id of the first rule that matched, or default
wait_for_reply reply or timeout
a_b_split a or b
webhook, hubspot_upsert_contact, enroll_sequence success or error
ai success, error, or tool:<name> when the model picks a tool
start_call success, permission_required or failed

An edge without sourceHandle is the node's only or default output. Every node type and its fields are on Workflow nodes.

#Variables

Strings in a node's config may include {{variable}}. The variables below are set at the start of each run; any the trigger did not provide are empty strings:

Group Variables
Message lastMessage, inboundText, triggerText, conversationId
Contact contact.name, contact.phone, contact.email, contact.handle, contact.tags, contact.fields, contact.lastInteractionAt, contact.lastInteractionHoursAgo, contact.isFollower (Instagram)
Referral referral.ref, referral.source, referral.type, referral.adId, referral.adTitle, referral.adName, referral.campaignName
Taps postback.payload, postback.title, quickReply.payload, quickReply.title
Story, comment, reaction story.id, story.url, comment.id, comment.text, comment.platformPostId, reaction.emoji, reaction.action
Lead lead.id, lead.formId, lead.formName, lead.adId, lead.campaignId and every answer by its key, such as lead.full_name and lead.email

Values captured with saveAs and every set_variable assignment join the same set. api_call runs also get the variables you send. An array or object interpolated into text renders as JSON.

#Buttons that start or answer workflows

On Instagram and Facebook, buttons and quick replies connect workflows to each other:

  • A postback button or quick reply with no payload gets pozy:workflow:<this workflow id>. A tap answers this run's pending wait_for_reply, or restarts the workflow.
  • A button with workflowId starts that other workflow instead.
  • Anywhere else (persistent menus, ice breakers, the Get Started button, comment automation DMs), a payload of pozy:workflow:<workflowId> starts that workflow when tapped. It must be active on the same account and profile.

A tap on a different active workflow starts it and exits the contact's other live runs.

#Start a run from your backend

An api_call workflow runs when you ask it to. POST /v1/workflows/{workflowId}/trigger takes exactly one target plus optional variables:

Target Runs in
conversationId That conversation on the workflow's account
contactId The contact's conversation on the workflow's account
to WhatsApp only: a phone number in E.164. The conversation is found or created
Order shipped: start the follow-up workflow
curl -X POST https://api.pozy.ai/v1/workflows/6650c1e2a3b4c5d6e7f80d55/trigger \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contactId": "6650c1e2a3b4c5d6e7f80c44", "variables": { "orderId": "A-1042", "carrier": "UPS" } }'

It answers 201 with { "executionId": "..." }. Inside the run, {{orderId}} and {{carrier}} are available.

#Test a workflow

POST /v1/workflows/{workflowId}/executions starts a run without a real inbound message. Pass conversationId (or to on WhatsApp) and an optional text that becomes lastMessage. Use it with a conversation from your own test account.

#Follow runs

Call Returns
GET /v1/workflows/{id}/executions Runs with status (running, waiting, completed, exited, failed), the current node, what it is waiting for and the accumulated variables
GET /v1/workflows/{id}/executions/{executionId}/events The run's timeline: every node started, completed, failed or skipped, the edge taken and the duration

exited is a deliberate early end, before the last node: a handoff, or a button that sent the contact into another workflow. Rather than polling, listen for workflow.run.started, workflow.run.completed and workflow.run.failed; the failed event says which node broke and why.

#Edit, version and copy

  • PATCH /v1/workflows/{id} changes the name, description, account or graph. Every graph edit is validated, and moving the workflow to another account re-checks it against that platform. A run already in progress picks up the saved graph at its next step; a run waiting on a node you removed completes instead of failing. For big changes, pause first and activate after.
  • Each graph change records a version. List them with GET .../versions, read one with GET .../versions/{version} and roll back with POST .../versions/{version}/restore. Restoring needs the workflow paused or in draft, and snapshots the current graph first, so a restore can itself be undone.
  • POST .../duplicate copies the graph, name and account into a new draft with no run history. Use it to experiment without touching the live version.

The nodes array is replaced as a whole on update, so always send the full list.

#Errors

Error Cause
400 on create or update Invalid structure: duplicate node ids, edges to missing nodes, a WhatsApp-only node on another platform, buttons on a platform that does not support them
400 on activate No trigger node, nodes that cannot be reached, or an incomplete graph. The error message names the problem
400 on restore The workflow is active. Pause it first

#Endpoints

#Webhook events

Event When it fires
workflow.run.started Workflow run started.
workflow.run.completed Workflow run completed.
workflow.run.failed Workflow run failed.