Automations
Every workflow node type, its config fields, its outputs and the platforms it runs on, from send_message and conditions to webhooks, AI and calls.
A workflow is made of nodes. Each node is { "id", "type", "config" }, plus an optional label and canvas position. This page lists all 17 types and the fields their config takes. Every string field accepts {{variable}} interpolation, resolved when the node runs.
| Group | Nodes |
|---|---|
| Messaging | send_message |
| Control flow | trigger, condition, delay, wait_for_reply, a_b_split, end |
| Data | set_variable, set_field, add_tag and remove_tag, enroll_sequence |
| Integrations | webhook, hubspot_upsert_contact, ai, handoff, start_call |
#trigger
Exactly one per workflow. It decides what starts a run. Outputs: one edge.
| Field | Type | Notes |
|---|---|---|
triggerType |
string | inbound_message (default), referral, comment, story_mention, reaction, whatsapp_event, lead_received, api_call |
keywords |
string[] | For inbound_message. Empty matches every message |
matchType |
string | any, contains, exact or regex |
onlyFirstMessage |
boolean | Fire only on a contact's first message |
referral |
object | { ref, matchType } with matchType exact, prefix or any (default) |
comment |
object | { keywords, matchType, platformPostId } |
reaction |
object | { emojis } to react only to some emojis |
eventType |
string | For whatsapp_event: message_sent, message_delivered, message_read, message_failed, reaction |
leadReceived |
object | { formIds, pageAccountIds, consentCheckboxKey, defaultCountry, allowCrossProfile }. Needs consentCheckboxKey and at least one of formIds or pageAccountIds |
cooldownHours |
integer | 1 to 720. Default 24 on every trigger except comment and api_call |
A whatsapp_event workflow never starts on the status of a message sent by a workflow, sequence or comment automation, so it cannot loop on itself.
#send_message
Sends one message into the run's conversation. Outputs: one edge.
| Field | Type | Notes |
|---|---|---|
messageType |
string | text, media, template, interactive or cards |
text |
string | The message text. Up to 640 characters when buttons are attached |
media |
object | { mediaType, url, caption } with mediaType image, video, audio or document. Media cannot carry buttons |
template |
object | WhatsApp: { name, language, variableMapping } for an approved template. Instagram and Facebook with messageType: "cards": { type: "generic", elements: [{ title, subtitle, imageUrl, buttons }] }, 1 to 10 cards |
interactive |
object | WhatsApp only: list, reply buttons, call-to-action URL, flow and other interactive messages |
buttons |
object[] | Instagram and Facebook, on text: 1 to 3 { type, title, url, payload, phone, workflowId } with type url, postback or phone (Facebook only) |
quickReplies |
object[] | Instagram and Facebook: 1 to 13 { title, payload, imageUrl, workflowId }. Not with buttons |
Button and quick reply titles are capped at 20 characters, card titles and subtitles at 80. A postback with no payload gets pozy:workflow:<this workflow id>, so a tap answers this workflow's pending wait_for_reply. workflowId on a button starts that workflow instead.
Workflows can only send within the platform's messaging window. messageTag is not available: HUMAN_AGENT is reserved for replies typed by a person. On WhatsApp, a run that may start outside the 24-hour window must open with a template.
#wait_for_reply
Pauses the run until the contact answers. Outputs: reply or timeout.
| Field | Type | Notes |
|---|---|---|
timeoutMinutes |
integer | Up to 43,200 (30 days). Takes the timeout edge after that much silence |
saveAs |
string | Variable name for the reply text, for example email makes {{email}} |
A button tap also counts as a reply and sets {{postback.payload}} and {{postback.title}}; a quick reply tap sets {{quickReply.payload}} and {{quickReply.title}}. Each reply clears the previous tap.
#condition
Branches on variables. Outputs: the id of the first rule that matched, otherwise default.
| Field | Type | Notes |
|---|---|---|
rules |
object[] | { id, variable, operator, value }, checked in order |
| Operators | Compare |
|---|---|
equals, not_equals, contains, not_contains, starts_with, ends_with, matches, exists, not_exists |
Text, ignoring case. With matches, value is a regular expression |
greater_than, less_than, greater_or_equal, less_or_equal |
Numbers. If either side is not a number, the condition is false |
before, after |
Dates in ISO 8601 |
has_tag, not_has_tag |
Checks whether a list variable, for example contact.tags, includes value |
is_true, is_false |
Booleans; leave value out |
{
"id": "route",
"type": "condition",
"config": {
"rules": [
{ "id": "vip", "variable": "contact.tags", "operator": "has_tag", "value": "vip" },
{ "id": "recent", "variable": "contact.lastInteractionHoursAgo", "operator": "less_than", "value": "48" }
]
}
}#delay
Waits, then continues. Outputs: one edge.
| Field | Type | Notes |
|---|---|---|
delayMinutes |
integer | Up to 43,200 (30 days) |
The run is suspended while it waits and resumes on a timer. Remember the messaging window: a long delay on Instagram or Facebook can push the next message outside it.
#a_b_split
Sends contacts down one of two branches at random. Outputs: a or b.
| Field | Type | Notes |
|---|---|---|
percentage |
number | 0 to 100, default 50. The share of runs that take a |
Tag each branch with add_tag so you can compare results later.
#end
Ends the run as completed. No config.
#set_variable
Sets run-scoped variables that exist only for this run. Outputs: one edge.
| Field | Type | Notes |
|---|---|---|
assignments |
object[] | { name, value } pairs. value can interpolate other variables |
#set_field
Writes a custom field on the contact, which persists after the run. Outputs: one edge.
| Field | Type | Notes |
|---|---|---|
field |
string | The field slug. Characters outside A-Z, a-z, 0-9 and _ are stripped |
value |
string | The value, usually a captured variable such as {{email}} |
Fires contact.field_changed when the value changes. Does nothing on api_call runs that have no contact.
#add_tag and remove_tag
Add or remove one tag on the contact. Outputs: one edge.
| Field | Type | Notes |
|---|---|---|
tag |
string | The tag |
Fire contact.tag_added and contact.tag_removed when they change something. Do nothing on api_call runs that have no contact.
#enroll_sequence
Puts the contact into a sequence. Outputs: success or error.
| Field | Type | Notes |
|---|---|---|
sequenceId |
string | The sequence to enroll in |
saveAs |
string | Variable for the result |
#webhook
Calls your server and saves the response. Outputs: success on a 2xx, otherwise error.
| Field | Type | Notes |
|---|---|---|
url |
string | Your endpoint. Private, loopback and cloud metadata addresses are refused |
method |
string | GET, POST, PUT, PATCH or DELETE |
headers |
object | Extra headers, for example your own auth token |
bodyTemplate |
string | Request body with {{variables}} |
saveAs |
string | Variable for the response, saved as { status, ok, body } |
{
"id": "lookup",
"type": "webhook",
"config": {
"url": "https://api.acme.example/orders/lookup",
"method": "POST",
"headers": { "Authorization": "Bearer acme_internal_token" },
"bodyTemplate": "{\"phone\": \"{{contact.phone}}\", \"text\": \"{{lastMessage}}\"}",
"saveAs": "order"
}
}Then use {{order.body.status}} in a later message or condition. Check the signature or token on your side, since anyone who learns the URL could call it.
#hubspot_upsert_contact
Creates or updates the contact in the profile's connected HubSpot portal. Outputs: success or error (HubSpot not connected, or HubSpot refused the write).
| Field | Type | Notes |
|---|---|---|
email, phone, firstName, lastName |
string | Usually variables captured earlier |
properties |
object | HubSpot internal property names to values, up to 50 |
saveAs |
string | Variable for { id, created } or { error }. Default hubspotContact |
Pozy updates the HubSpot contact already linked to this person, else one matching the email, then the phone, else it creates one. An existing HubSpot contact only gets the fields this node sets; empty values are skipped.
#ai
Asks a language model and saves the answer. Outputs: success, error, or tool:<name> when the model chooses one of your tools.
| Field | Type | Notes |
|---|---|---|
provider |
string | anthropic, openai, google, mistral, groq or openrouter. With model, the node runs on your own key for that provider |
model |
string | The provider's model name |
preset |
string | smart, tools or cheap |
systemPrompt |
string | Instructions for the model |
userPromptTemplate |
string | The prompt, usually with {{lastMessage}} and contact variables |
outputType |
string | text or json |
saveAs |
string | Variable for the output |
temperature, maxTokens |
number | Generation settings |
tools |
object[] | { name, description, parameters }. When the model picks one, the run follows the tool:<name> edge |
Use tools to let the model route the conversation: for example a book_demo tool whose edge leads to a booking message, and a human tool whose edge leads to a handoff.
#handoff
Ends the run as exited and flags the conversation for a person. No output edge.
| Field | Type | Notes |
|---|---|---|
note |
string | Context for whoever picks it up |
assignTo |
string | Who should handle it |
To also pass a Meta thread to another app, call thread control when workflow.run.completed arrives with execution.status: "exited".
#start_call
Places a WhatsApp call to the contact. WhatsApp only. Outputs: success, permission_required or failed.
| Field | Type | Notes |
|---|---|---|
to |
string | The number to call, usually {{contact.phone}} |
forwardTo |
string | Where to connect the call: tel:+E164, sip:user@host, or a wss:// URL for an AI voice agent |
requirePermissionFirst |
boolean | Ask for call permission before dialing |
recordingEnabled |
boolean | Record the call |
saveAs |
string | Variable for the result |
WhatsApp requires the person's permission before a business calls them. Route permission_required to a message explaining why you want to call. See WhatsApp calling.
#Validation
Saving a workflow checks that node ids are unique, that edges point at existing nodes, that WhatsApp-only nodes (start_call, template and interactive messages) are not used on other platforms, and that Instagram and Facebook-only fields (buttons, quickReplies, cards) are not used elsewhere. A problem answers 400 with a message that describes it.