# Get AI Agent
Source: https://docs.pingmee.co.il/api-reference/ai-agents/get
GET /ai-agents/{id}
Returns a single AI agent by ID, including vector store files when available. Path `id` must be URL-encoded.
Call this endpoint on the AI service: `https://ai.pingmee.co.il`.
# Get AI Agents
Source: https://docs.pingmee.co.il/api-reference/ai-agents/get-all
GET /ai-agents
Returns a flat paginated list of AI agents for the authenticated business. Pass `flat=true` to skip folder grouping. Distinct from human agents under `/agents`.
Call this endpoint on the AI service: `https://ai.pingmee.co.il` with `flat=true`. Returns up to 25 agents per page (override with `limit`). Use `lastEvaluatedKey` as `exclusiveStartKey` for the next page. Pass `getFiles=true` only when you need vector store files for each agent.
# Update AI Agent
Source: https://docs.pingmee.co.il/api-reference/ai-agents/update
PUT /ai-agents/{id}
Updates an AI agent. Supported fields: `name`, `description`, `roles`, and `vectorStore.required`. Changed roles regenerate their OpenAI function tools.
**Field length limits** (same as the Pingmee app; exceeding them returns `400`):
| Field | Max length |
| --- | --- |
| `description` | 5000 |
| `roles[].name` | 40 |
| `roles[].description` | 1500 |
| `roles[].instructions` | 1500 |
| `roles[].webhook` | 300 |
Path `id` must be URL-encoded.
Call this endpoint on the AI service: `https://ai.pingmee.co.il`.
You can update `name`, `description`, `roles`, and `vectorStore.required`. When a role changes, its OpenAI function tool is regenerated automatically.
## Field length limits
These match the Pingmee app. Requests that exceed them return `400`.
| Field | Max length |
| - | - |
| `description` | 5000 |
| `roles[].name` | 40 |
| `roles[].description` | 1500 |
| `roles[].instructions` | 1500 |
| `roles[].webhook` | 300 |
# Get Phone Numbers
Source: https://docs.pingmee.co.il/api-reference/business/get-phone-numbers
GET /phone-numbers
List connected WhatsApp (and Green-API) phone numbers for the authenticated business. Call this before creating a workflow: if more than one number is returned, ask the user which one to bind. Copy the chosen row's `id` into the workflow `associatedToBusinessId` (not `associatedBusinessId`, which is the WABA id) and set `platformType`. For a starter/outbound bot (or a named catalog template), pass the same `id` as `phoneNumberId` on GET `/templates`. Skip GET `/templates` for inbound reply bots (`pingmeeTrigger`) unless the user named a template. Facebook pages and Instagram accounts are not in this list. See `/build-workflows/document` (platform binding) and `/build-workflows/overview`.
Connected WhatsApp (and Green-API) numbers for the authenticated business. Use this **before creating a workflow**.
1. Call this endpoint.
2. If `phoneNumbers` has more than one row, **ask the user** which number the bot should send from.
3. Copy that row’s `id` into the workflow’s `associatedToBusinessId` and set `platformType` (`whatsapp` or `greenAPI`). Do **not** use `associatedBusinessId` (that is the WABA id).
4. For a **starter** / outbound bot (or a named catalog template), pass the same `id` as `phoneNumberId` when [fetching templates](/api-reference/business/get-templates). Skip GET `/templates` for inbound reply bots unless the user named a template.
See [Platform binding](/build-workflows/document#platform-binding).
This list does **not** include Facebook pages or Instagram accounts. There is no public list-pages API.
# Get WhatsApp Templates
Source: https://docs.pingmee.co.il/api-reference/business/get-templates
GET /templates
List WhatsApp message templates for a connected phone number (`type=whatsapp` and `phoneNumberId` required). `phoneNumberId` is the workflow `associatedToBusinessId` (GET `/phone-numbers` row `id`). Call this for **starter/outbound** bots (`workflowTrigger`, POST `/workflows/{id}/trigger`, campaign-style send, or any send outside the 24-hour window), or when the user named a catalog template. Do **not** call this for inbound reply bots (`pingmeeTrigger` `Messages Created` / `New Conversation Started`) — those use `messageType: custom`. When you do fetch: pick an APPROVED template, copy it into the WhatsApp node's `templateInformation`, set `messageType: template` and `templateType: whatsapp`, and bind `bodyVariables` / `headerVariables`. Skip IMAGE / VIDEO / DOCUMENT header templates unless `attachmentS3Id` is already on the component — there is no public upload API. Infer intent: `/build-workflows/overview`. Field mapping: `/build-workflows/nodes-and-edges`.
Approved Meta templates for one connected phone number. Required query params: `type=whatsapp` and `phoneNumberId` (the workflow’s `associatedToBusinessId`).
**When to call this**
* **Starter / outbound** — the bot **opens** the conversation (`workflowTrigger`, [POST /trigger](/api-reference/workflows/trigger), campaign-style send, or any send outside the 24-hour window).
* The user asked for a **named** catalog template (even on a reply bot).
**Skip this call** for inbound reply bots (`pingmeeTrigger` `Messages Created` / `New Conversation Started`, keyword router). Those use `messageType: "custom"` — infer intent on [Build a workflow](/build-workflows/overview#infer-starter-vs-reply).
When you do fetch:
1. Bind the phone first ([GET phone numbers](/api-reference/business/get-phone-numbers)).
2. GET this list and keep `status === "APPROVED"`.
3. Copy the template object into the WhatsApp node’s `templateInformation`.
4. Set `messageType: "template"` and `templateType: "whatsapp"`.
5. Bind BODY / text HEADER placeholders with `bodyVariables` / `headerVariables`.
Skip templates whose HEADER `format` is IMAGE, VIDEO, or DOCUMENT unless `attachmentS3Id` is already on that component — there is no public upload API.
Field mapping: [WhatsApp templates](/build-workflows/nodes-and-edges#whatsapp-templates).
# Assign Agents
Source: https://docs.pingmee.co.il/api-reference/conversation/agents/assign
POST /conversations/{id}/agents
Associate agents to a conversation. Path `id` is the conversation `participantsIdentifiers` (URL-encoded).
# Get Agent
Source: https://docs.pingmee.co.il/api-reference/conversation/agents/get
GET /agents/{id}
Returns a single agent by ID (email). Path `id` must be URL-encoded.
# Get Agents
Source: https://docs.pingmee.co.il/api-reference/conversation/agents/get-all
GET /agents
Returns all agents associated with the authenticated account.
# Unassign Agents
Source: https://docs.pingmee.co.il/api-reference/conversation/agents/unassign
DELETE /conversations/{id}/agents
Remove agents from a conversation. Path `id` is the conversation `participantsIdentifiers` (URL-encoded).
# Set Answer Mode
Source: https://docs.pingmee.co.il/api-reference/conversation/answer-mode
POST /conversations/{id}/answer-mode
Set the answer mode for a conversation. Path `id` is the conversation `participantsIdentifiers` (URL-encoded).
# Get Conversation
Source: https://docs.pingmee.co.il/api-reference/conversation/get
GET /conversations/{id}
Get a single conversation by participants identifiers. Path `id` must be URL-encoded.
# Get Conversations
Source: https://docs.pingmee.co.il/api-reference/conversation/get-all
POST /conversations
Get conversations using cursor-based pagination.
# Set Status
Source: https://docs.pingmee.co.il/api-reference/conversation/status
POST /conversations/{id}/status
Set the status of a conversation. Path `id` is the conversation `participantsIdentifiers` (URL-encoded).
# Get Conversation Statuses
Source: https://docs.pingmee.co.il/api-reference/conversation/statuses-get-all
GET /conversation/statuses
Returns all custom conversation statuses for the authenticated business. These are business-defined status labels (title and color), not the conversation `statusCase` (`open` / `closed` / `standby`).
Returns the custom conversation status definitions configured for your business (title and color).
This is different from [Set Status](/api-reference/conversation/status), which sets a conversation's `statusCase` (`open` / `closed` / `standby`).
# Assign Tags to Conversation
Source: https://docs.pingmee.co.il/api-reference/conversation/tags-assign
POST /conversations/{id}/tags
Assigns one or more tags to a conversation. Path `id` is the conversation `participantsIdentifiers` (URL-encoded).
# Get Conversation Tags
Source: https://docs.pingmee.co.il/api-reference/conversation/tags-get
GET /conversations/{id}/tags
Returns the tag IDs assigned to a conversation. Path `id` is the conversation `participantsIdentifiers` (URL-encoded).
# Remove Tags from Conversation
Source: https://docs.pingmee.co.il/api-reference/conversation/tags-remove
DELETE /conversations/{id}/tags
Removes one or more tags from a conversation. Path `id` is the conversation `participantsIdentifiers` (URL-encoded).
# Create Customers
Source: https://docs.pingmee.co.il/api-reference/customers/create
PUT /customers
Creates one or more customers.
# Delete Customer
Source: https://docs.pingmee.co.il/api-reference/customers/delete
DELETE /customers/{id}
Deletes a single customer based on the ID supplied
# Get Customer
Source: https://docs.pingmee.co.il/api-reference/customers/get
GET /customers/{id}
Returns a Customer
# Get Customers
Source: https://docs.pingmee.co.il/api-reference/customers/get-all
POST /customers
Returns all customers using pagination
# Update Customer
Source: https://docs.pingmee.co.il/api-reference/customers/update
POST /customers/{id}
Updates editable customer fields. This is a partial update — only include the fields you want to change. `customerName` is not updatable here.
Updatable fields: `customerNickname`, `email`, `description`, `phoneNumber`.
Call this endpoint on the campaigns service: `https://campaigns.pingmee.co.il`.
Partial update — send only the fields you want to change. For example, to update description alone:
```json theme={null}
{
"description": "VIP customer, prefers Hebrew"
}
```
Updatable fields: `customerNickname`, `email`, `description`, `phoneNumber`.
# Get Messages
Source: https://docs.pingmee.co.il/api-reference/messages/get-messages
POST /getMessages
Fetches messages for a conversation identified by the business phone number and the customer (target) phone number. Results are ordered newest first. Omit `startKey` on the first request. For each following batch, set `startKey` to the `lastEvaluatedKey` value returned by the previous response. `resetConversationUnreadCount` defaults to false for API callers, so the unread count stays unchanged. The Pingmee app sends true by default and marks the conversation read.
**Pagination:** For the first request, omit `startKey`. To fetch the next batch, take **`lastEvaluatedKey`** from the previous response and send it as **`startKey`** in the next request. That object has the shape `{ "messageId": "", "createdAt": }` (Unix time in milliseconds).
**Unread:** `resetConversationUnreadCount` is **false by default** on the API. Omit it or send `false` and the conversation stays unread. The Pingmee app sends **true by default**, which marks the conversation read.
# Send Message
Source: https://docs.pingmee.co.il/api-reference/messages/send-message
POST /sendMessage
Send a message
# Send Template
Source: https://docs.pingmee.co.il/api-reference/messages/send-template
POST /templates/send
Send a WhatsApp template message
# Create Tag
Source: https://docs.pingmee.co.il/api-reference/tags/create
PUT /conversation/tags
Creates a new conversation tag. Send `{ title, color }` only — the server assigns `id` and `associatedTo`. Duplicate title in the same workspace returns 400. There is no POST /conversation/tags.
Create a workspace conversation tag. Body is `{ "title", "color" }`. The server assigns `id` and `associatedTo`. Duplicate `title` in the same workspace returns **400**.
Copy the returned object into `updateConversation.selectedTags` — do not invent tag ids. Prefer [GET tags](/api-reference/tags/get-all) and reuse an existing tag when one already matches.
# Delete Tag
Source: https://docs.pingmee.co.il/api-reference/tags/delete
DELETE /conversation/tags/{id}
Deletes a conversation tag by id
# Get Tag
Source: https://docs.pingmee.co.il/api-reference/tags/get
GET /conversation/tags/{id}
Returns a conversation tag
# Get Tags
Source: https://docs.pingmee.co.il/api-reference/tags/get-all
GET /conversation/tags
Returns all conversation tags
# Delete Task
Source: https://docs.pingmee.co.il/api-reference/tasks/delete
DELETE /tasks/{taskId}
Deletes a single task by ID.
# Get Task
Source: https://docs.pingmee.co.il/api-reference/tasks/get
GET /tasks/{taskId}
Returns a single task by ID.
# Get Tasks
Source: https://docs.pingmee.co.il/api-reference/tasks/get-all
POST /tasks/list
Returns tasks using cursor-based pagination. Default page size is 50; the maximum is 100.
Send an empty object `{}` for the first page. Page size defaults to **50** and is capped at **100**. If `lastEvaluatedKey` is present in the response, pass it back in the next request to load the following page.
# Create Task Status
Source: https://docs.pingmee.co.il/api-reference/tasks/statuses-create
PUT /tasks/status
Creates a custom task status. `title` and `color` are required. Titles must be unique for the business.
`title` and `color` are required. Titles must be unique for the business.
# Delete Task Status
Source: https://docs.pingmee.co.il/api-reference/tasks/statuses-delete
DELETE /tasks/status/{id}
Deletes a custom task status by ID.
# Get Task Statuses
Source: https://docs.pingmee.co.il/api-reference/tasks/statuses-get-all
GET /tasks/statuses
Returns all custom task statuses for the authenticated business. These are business-defined status labels (title and color), not the built-in task `statusCase` values (`open` / `standby` / `completed`).
Returns the custom task status definitions configured for your business (title and color).
This is different from a task's `statusCase` (`open` / `standby` / `completed`, or a custom status ID from this list).
# Update Task
Source: https://docs.pingmee.co.il/api-reference/tasks/update
PATCH /tasks/{taskId}
Partial update — send only the fields you want to change. The server always refreshes `updatedAt` and `agentIdentification`, and increments `unreadCount`.
Updatable fields: `title`, `body`, `statusCase`, `assignedAgentIds`, `viewedByAgentIds`, `dueDate`, `participantsIdentifiers`, `phoneNumberId`.
Partial update — send only the fields you want to change. For example, to update title and status:
```json theme={null}
{
"title": "Call back tomorrow",
"statusCase": "standby"
}
```
Updatable fields: `title`, `body`, `statusCase`, `assignedAgentIds`, `viewedByAgentIds`, `dueDate`, `participantsIdentifiers`, `phoneNumberId`.
The server always refreshes `updatedAt` and `agentIdentification`, and increments `unreadCount`.
# Create Webhook
Source: https://docs.pingmee.co.il/api-reference/webhook/create
POST /webhook
Creates a new webhook subscription.
# Delete Webhook
Source: https://docs.pingmee.co.il/api-reference/webhook/delete
DELETE /webhook/{webhookId}
Deletes a webhook by ID. Path `webhookId` must be URL-encoded.
# Get Webhooks
Source: https://docs.pingmee.co.il/api-reference/webhook/get
GET /webhook
Returns all webhooks for the authenticated account.
# Reset Workflow Analytics
Source: https://docs.pingmee.co.il/api-reference/workflows/analytics-reset
POST /workflows/{workflowId}/analytics/reset
Zeros every canvas analytics counter on this workflow (`totalStarted`, `totalCompleted`, `totalDropped`, `nodeStats`, `edgeStats`, `buttonStats`). No request body. Requires the Expert AI plan (`403` otherwise). Destructive — call only when the user explicitly asks. Returns the updated workflow document. Use the new `lastModified` on the next graph PUT. Path `workflowId` must be URL-encoded. Same Cookie + `x-api-key` as other Workflows calls. See `/build-workflows/analytics`.
Zeros every canvas analytics counter on this workflow (`totalStarted`, `totalCompleted`, `totalDropped`, `nodeStats`, `edgeStats`, `buttonStats`). URL-encode `workflowId` in the path. No request body.
Requires the **Expert AI** plan (`403` otherwise) and the same Cookie + `x-api-key` as other Workflows calls.
Destructive — only when the user explicitly asks. Returns the updated workflow document. Use the new `lastModified` on the next graph [PUT](/api-reference/workflows/update).
How to **read** analytics (GET, not this endpoint) and change the graph: [Workflow analytics](/build-workflows/analytics).
# Create Workflow
Source: https://docs.pingmee.co.il/api-reference/workflows/create
POST /workflows/{workflowId}
Create a workflow. Client supplies `workflowId` in the path (URL-encode it). Required query: `folderId`. Body requires `name`. Before saving, GET `/phone-numbers`, ask the user which number to bind, and set `associatedToBusinessId` to that row's `id` plus `platformType`. Infer intent: if the bot **opens** a WhatsApp conversation (starter/outbound, `workflowTrigger`, POST `/trigger`), GET `/templates` and use an APPROVED template on the first WhatsApp node; inbound reply bots (`pingmeeTrigger`) use `messageType: custom` and should not fetch templates unless the user named one. Graph fields (`data`, `parsedData`, `trigger`, `triggerType`, `variables`) are optional — you can send a full document. When sending `data.nodes`, also send `autoLayout: true` so the server Clean-layouts roots and snaps sub-nodes (flag is not stored). The server overwrites `id`, `associatedTo` (from the Cookie), `folderId`, `isActive` (always `false`), `created`, `lastModified`, `executionCount`, `lastExecution`, and `analytics`. Returns `{ message, created, lastModified }`.
Before building `data` / `parsedData`, read `/build-workflows/overview` and the related Build workflows cookbook pages. Recommended save sequence: optional POST `/folders/{id}` → POST this endpoint (name + phone ids + optional graph + autoLayout true) → PUT `{ isActive: true }`. Graph PUTs must echo `lastModified`.
You supply `workflowId` in the path (URL-encode it). `folderId` is a **required** query parameter — create a folder first or reuse one from [List workflows](/api-reference/workflows/get-minimal).
The body needs at least `name`. Also send `associatedToBusinessId` (phone-number `id` from [GET phone numbers](/api-reference/business/get-phone-numbers)) and `platformType`. You can also send a full graph (`data`, `parsedData`, `trigger`, `triggerType`, `variables`). When you send `data.nodes`, also send **`autoLayout: true`** so the server Clean-layouts roots and snaps button positions. The flag is not stored. The server always stores `isActive: false` and sets `associatedTo` from the Cookie.
Returns `{ message, created, lastModified }`. Keep `lastModified` for later graph PUTs.
Before building `data` or `parsedData`, read [Build workflows](/build-workflows/overview). Fetch phones, ask the user which number to bind, then **infer intent**: a WhatsApp **template** only if this bot **opens** the conversation; inbound reply bots use `messageType: "custom"`. An AI that only writes `data` will not run; one that only writes `parsedData` will not open correctly on the board.
## Recommended save sequence
1. Optional: [Create folder](/api-reference/workflows/create-folder) — `POST /folders/{id}` with `{ "name": "..." }`
2. `POST /workflows/{workflowId}?folderId=...` with `name` and optional graph
3. [Update workflow](/api-reference/workflows/update) — `PUT` `{ "isActive": true }` (no graph required)
Graph PUTs must echo `lastModified` from this create response (or a later GET), or the API returns **409**.
# Create Folder
Source: https://docs.pingmee.co.il/api-reference/workflows/create-folder
POST /folders/{id}
Create a workflow folder. Client supplies `id` in the path (URL-encode it). Body requires `{ name }`. Use this folder id as `folderId` when creating a workflow, or reuse `folderId` from POST `/workflows` (minimal list).
Create a workflow folder so you can pass `folderId` when [creating a workflow](/api-reference/workflows/create). You supply `id` in the path (URL-encode it).
```json theme={null}
{
"name": "Onboarding"
}
```
You can skip this call and reuse a `folderId` from [List workflows](/api-reference/workflows/get-minimal).
# Get Workflow
Source: https://docs.pingmee.co.il/api-reference/workflows/get
GET /workflows/{workflowId}
Returns the full workflow document, including canvas `data`, execution `parsedData`, `trigger`, `variables`, `settings`, and (on the Expert AI plan) `analytics`. If `analytics` is omitted, the plan may not include canvas analytics — do not invent counts. There is no separate analytics GET. Path `workflowId` must be URL-encoded. See `/build-workflows/analytics`.
Returns the **full** workflow document — not the minimal `{ id, name, folderId }` rows from [List workflows](/api-reference/workflows/get-minimal).
The response includes canvas `data`, execution `parsedData`, `trigger`, `variables`, `settings`, `lastModified`, and (on the Expert AI plan) `analytics`. URL-encode `workflowId` in the path.
If `analytics` is missing, the plan may not include canvas analytics — do not invent counts. How to interpret the object and PUT graph changes: [Workflow analytics](/build-workflows/analytics).
To build or edit `data` / `parsedData`, read [Build workflows](/build-workflows/overview) first.
# Get Workflows
Source: https://docs.pingmee.co.il/api-reference/workflows/get-minimal
POST /workflows
List workflows (minimal).
# Trigger Workflow
Source: https://docs.pingmee.co.il/api-reference/workflows/trigger
POST /workflows/{workflowId}/trigger
Manually trigger a bot workflow for a conversation. This call is a **starter**: it opens the thread from the business side, so the workflow's first WhatsApp node must be `messageType: template` + `templateType: whatsapp` with an APPROVED catalog template. Optional body `triggerData` is copied onto the trigger node output and read with `return $.triggerData.;`. Path `workflowId` must be URL-encoded.
Manually start a bot workflow for a conversation. Path `workflowId` must be URL-encoded.
Optional `triggerData` is a JSON **object** copied onto the trigger node's output. Downstream WhatsApp / HTTP / Task fields read it with an Expression. The `$…` id is the trigger **node** id, not the workflow id:
```
return $trig_1.triggerData.websiteLink;
```
```json theme={null}
{
"phoneNumberId": "phone_number_id_example",
"customerPhoneNumberId": "customer_phone_id_example",
"triggerData": {
"websiteLink": "https://example.com"
}
}
```
Bind that expression in a template slot or HTTP `{{n}}` — see [`triggerData`](/build-workflows/variables#triggerdata) and a dual-graph snippet in [Examples](/build-workflows/examples#workflow-trigger--triggerdata).
This call is a **starter**: it **opens** a conversation from the business side. The workflow’s first WhatsApp node must be `messageType: "template"` + `templateType: "whatsapp"` with an APPROVED catalog template — [WhatsApp templates](/build-workflows/nodes-and-edges#whatsapp-templates). (Inbound reply bots that use `pingmeeTrigger` do **not** go through this endpoint and may use `messageType: "custom"`.) `phoneNumberId` in the body should match the workflow’s `associatedToBusinessId`.
`startWithNodeId` that skips the trigger does **not** seed `output.triggerData`. A [workflow pointer](/build-workflows/sub-workflows) does not forward this payload to the child — POST `/trigger` on the workflow that should receive it.
# Update Workflow
Source: https://docs.pingmee.co.il/api-reference/workflows/update
PUT /workflows/{workflowId}
Update a workflow. `name`, `isActive`, `settings`, and `associatedToBusinessId` can be sent alone — including `{ isActive: true }` to activate without sending a graph. If `data` or `parsedData` is present, this is a graph update: also send `trigger`, `triggerType`, `variables`, `autoLayout: true`, and `lastModified` from the last GET or create, or the API returns 409. Always send both `data` and `parsedData` plus `trigger` / `triggerType` on graph save. `autoLayout` runs board Clean and snaps sub-nodes; it is not stored. Do not send `analytics`. A graph PUT prunes stored analytics for node, edge, and button ids that are no longer on the graph — GET fresh analytics before rewriting.
Before building `data` / `parsedData`, read `/build-workflows/overview` and `/build-workflows/analytics`. Path `workflowId` must be URL-encoded.
Partial update — send only the fields you want to change. URL-encode `workflowId` in the path.
`name`, `isActive`, `settings`, and `associatedToBusinessId` can be sent alone. Create always stores a workflow inactive; activate without sending a graph:
```json theme={null}
{
"isActive": true
}
```
Set `associatedToBusinessId` (and `platformType` on **create**) from [GET phone numbers](/api-reference/business/get-phone-numbers). PUT persists `associatedToBusinessId` without a graph; `platformType` is stored from the create body.
## Graph updates and optimistic lock
If `data` or `parsedData` is present, this is a graph update. Also send `trigger`, `triggerType`, `variables`, **`autoLayout: true`**, and `lastModified` from the last [GET](/api-reference/workflows/get) or [create](/api-reference/workflows/create). Always send **both** `data` and `parsedData` plus `trigger` / `triggerType`. `autoLayout` runs the board Clean (root dagre) and snaps sub-nodes; it is not stored.
When `lastModified` does not match the server, the API returns **409** with the current `lastModified`. Reload and retry.
A graph PUT **prunes** stored `analytics` for node / edge / button ids that are no longer on the graph. GET fresh analytics before you rewrite, and do not send `analytics` in the body. See [Workflow analytics](/build-workflows/analytics).
Before building `data` or `parsedData`, read [Build workflows](/build-workflows/overview) and the related cookbook pages.
# Workflow analytics
Source: https://docs.pingmee.co.il/build-workflows/analytics
Fetch canvas analytics from GET, interpret drop-off, then PUT graph changes
An AI can **read** a workflow’s canvas analytics and **change the graph** from them. There is no separate analytics GET — counters live on the [GET workflow](/api-reference/workflows/get) document as `analytics`.
## Fetch
`GET https://workflows.pingmee.co.il/workflows/{workflowId}` (URL-encode the id). Same Cookie + `x-api-key` as other Workflows calls.
`analytics` is present on the **Expert AI** plan. If the field is **missing**, do not invent numbers — tell the user the plan may not include canvas analytics. If it is present but `totalStarted` is `0`, there is nothing to analyze yet (new workflow or a reset).
List (`POST /workflows`) returns minimal `{ id, name, folderId }` only — it does not include `analytics`.
**GET a fresh document immediately before you edit.** A graph PUT prunes stats for removed node / edge / button ids. After you rewrite ids, old keys no longer match.
## Shape
```json theme={null}
{
"totalStarted": 120,
"totalCompleted": 48,
"totalDropped": 31,
"lastUpdated": 1770000000000,
"nodeStats": {
"trig_1": { "enteredCount": 120, "completedCount": 120, "exitedCount": 118 },
"wa_menu": { "enteredCount": 110, "completedCount": 90, "exitedCount": 85 },
"btn_book": { "enteredCount": 40, "completedCount": 0, "exitedCount": 40 }
},
"edgeStats": {
"trig_1->wa_menu:out": {
"sourceNodeId": "trig_1",
"targetNodeId": "wa_menu",
"sourceHandle": "out",
"traversalCount": 110
},
"btn_book->http_1:in": {
"sourceNodeId": "btn_book",
"targetNodeId": "http_1",
"sourceHandle": "in",
"traversalCount": 40
}
},
"buttonStats": {
"btn_book": {
"subNodeId": "btn_book",
"parentNodeId": "wa_menu",
"label": "Book",
"clickCount": 40
}
}
}
```
| Field | Meaning |
| - | - |
| `totalStarted` | Executions that began. |
| `totalCompleted` | Executions that finished successfully. |
| `totalDropped` | Executions that ended in failure or expiry (not success). |
| `lastUpdated` | Optional. Epoch **milliseconds** of the last recorded event. |
| `nodeStats[nodeId]` | `enteredCount` / `completedCount` / `exitedCount` for that canvas or sub-node id. |
| `edgeStats[key]` | How often an edge ran. Key is `{source}->{target}` or `{source}->{target}:{sourceHandle}`. |
| `buttonStats[subNodeId]` | WhatsApp **answer** sub-node clicks. Key is the answer id, not the parent message. |
Still in progress: `max(0, totalStarted - totalCompleted - totalDropped)`.
Conversion (what the board shows): `totalCompleted / totalStarted`.
### Edge keys and `sourceHandle`
Root outgoing edges use `sourceHandle: "out"`. Sub-node outgoing edges (answers, If `true`/`false`, switch cases, fallback) use `"in"`. Older runs may key a branch by the **sub-node id** instead of `"in"` — look up both `{source}->{target}:in` and `{source}->{target}:{sourceNodeId}`.
If/Else and switch paths are **node + edge** stats on the branch sub-node, not `buttonStats`.
### Buttons
`buttonStats` maps to [answer sub-nodes](/build-workflows/sub-nodes). Compare `clickCount` among siblings that share `parentNodeId`. A click also increments the answer’s `nodeStats.enteredCount` and the parent’s `exitedCount`.
## Interpret
Use only fields that exist. Rank by counts; do not invent percentages the API does not store.
| Signal | How to read it |
| - | - |
| Drop-off | High `totalStarted` vs low `totalCompleted`. Or a node with high `enteredCount` and low `completedCount` / `exitedCount`. |
| Hottest / coldest nodes | Highest vs lowest `enteredCount` (or `enteredCount / totalStarted`). Cold nodes after a hot parent are skip / drop points. |
| Edges | Low `traversalCount` vs the source node’s `enteredCount` means few people took that path. |
| Buttons | Low `clickCount` vs siblings on the same parent — weak label or unused option. |
## Act
1. GET the workflow. Keep `analytics`, `lastModified`, `data`, `parsedData`, and `trigger`.
2. If `analytics` is missing, stop (plan gating). If `totalStarted` is `0`, say there is no data yet.
3. Propose concrete graph edits, for example: reroute or rewrite low-click buttons, add a fallback / timeout, change copy, split an If condition, or add a path after a drop-off node.
4. [PUT](/api-reference/workflows/update) **both** `data` and `parsedData` plus `trigger` / `triggerType` / `variables`, echoing `lastModified`. On **409**, GET again and retry.
5. If the workflow is inactive, activate with `PUT { "isActive": true }` only (no graph fields).
6. Do **not** send `analytics` on PUT. The server prunes stats for node / edge / button ids that no longer exist on the new graph; totals stay. **New or renamed ids have no stats until new executions.**
See [Hard rules](/build-workflows/rules) and [Canvas vs execution](/build-workflows/canvas-vs-execution).
## Reset (optional)
[POST /workflows//analytics/reset](/api-reference/workflows/analytics-reset) zeros every counter. It is **destructive**. Call it only when the user explicitly asks. Expert AI only (`403` otherwise). The response is the updated workflow; use its `lastModified` on the next graph PUT.
# Canvas vs execution
Source: https://docs.pingmee.co.il/build-workflows/canvas-vs-execution
How React Flow data and parsedData + trigger stay in sync
The board saves **two graphs**. IDs must match across `data.nodes`, `parsedData`, `trigger`, and every edge `source` / `target`.
```mermaid theme={null}
flowchart LR
canvas["data.nodes + data.edges"]
exec["parsedData + trigger"]
canvas -->|"transformReactFlowOutput"| exec
runtime["Runtime starts at trigger.connections.out[0]"]
exec --> runtime
```
## Canvas `data` (React Flow `toObject()`)
Root nodes:
```json theme={null}
{
"id": "wa_menu",
"type": "whatsapp",
"category": "Action",
"position": { "x": 320, "y": 80 },
"draggable": true,
"selected": false,
"data": {}
}
```
Sub-nodes are **siblings** in `data.nodes`, not nested. They carry `parentId`. Positions are **relative to the parent**, not canvas coordinates. They are **not draggable** — the parent moves them.
### Layout via API (`autoLayout`)
Send **`autoLayout: true`** on graph [create](/api-reference/workflows/create) and [PUT](/api-reference/workflows/update). The server runs the same two board operations and **does not store** the flag:
1. **Sub-nodes** — `draggable: false`, `extent: "parent"`, `position.x = 138`, `position.y` from the GUI stack (answers, then fallback/timeout).
2. **Roots** — dagre left-to-right (`ranksep` \~120, `nodesep` \~60), same as the board **Clean** button. Children keep the relative positions from step 1.
Agents should still set `parentId` on buttons. You do **not** need to compute canvas x/y for roots or buttons. `autoClean: true` is accepted as an alias.
```json theme={null}
{
"autoLayout": true,
"lastModified": 1770000000,
"data": { "nodes": [], "edges": [] },
"parsedData": [],
"trigger": {},
"triggerType": "Messages Created",
"variables": {}
}
```
Edges:
| Field | Value |
| - | - |
| `type` | `"buttonedge"` |
| `targetHandle` | `"in"` |
| Root **output** `sourceHandle` | `"out"` |
| Root **input** handle | `"in"` |
| Sub-node **output** `sourceHandle` | `"in"` — NestedSubNode quirk. Downstream edges from a button / true / false / case use `sourceHandle: "in"`. |
```json theme={null}
[
{
"source": "trig_1",
"target": "wa_menu",
"sourceHandle": "out",
"targetHandle": "in",
"type": "buttonedge"
},
{
"source": "ans_tours",
"target": "ptr_tours",
"sourceHandle": "in",
"targetHandle": "in",
"type": "buttonedge"
}
]
```
When a node has sub-nodes, do **not** draw a root `out` edge from that parent. Execution continues from the sub-node `connections.out`.
## Execution `parsedData` + `trigger`
`transformReactFlowOutput` builds this from the canvas:
* The trigger is **removed** from `parsedData` and stored as `workflow.trigger`.
* `triggerType` is `trigger.data.eventType`.
* `parsedData` is **parent nodes only**. Sub-nodes hang off `parent.subNodes[]`.
* Each node has `connections: { out: string[] }` — target ids only. No handle ids. Branching is “which sub-node’s `connections.out`”.
* Runtime starts at `trigger.connections.out[0]`.
```json theme={null}
{
"triggerType": "Messages Created",
"trigger": {
"id": "trig_1",
"type": "pingmeeTrigger",
"category": "Trigger",
"title": "pingmeeTrigger",
"data": { "eventType": "Messages Created", "platform": "whatsapp" },
"connections": { "out": ["wa_menu"] }
},
"parsedData": [
{
"id": "wa_menu",
"type": "whatsapp",
"category": "Action",
"title": "whatsapp",
"data": { "waitForUserResponse": true, "platform": "whatsapp" },
"connections": {},
"subNodes": [
{
"id": "ans_tours",
"type": "answer",
"parentId": "wa_menu",
"title": "Tours",
"data": { "label": "Tours", "index": 0 },
"connections": { "out": ["ptr_tours"] }
}
]
}
]
}
```
Parent `title` is usually the `NodeType` string. Sub-node `title` is the canvas `data.label`. If/Else matches `title.toLowerCase() === "true"` / `"false"`.
## Mapping checklist
| Canvas | Execution |
| - | - |
| Trigger node in `data.nodes` | `workflow.trigger` only |
| Parent in `data.nodes` | Item in `parsedData` |
| Child with `parentId` | `parent.subNodes[]` |
| Edge `source → target` | `source.connections.out` includes `target` |
| Handle ids on edges | Dropped — only target ids remain |
## How to wire a graph
Build the canvas first, then **copy** each edge into execution `connections.out`. IDs must be identical in `data.nodes`, `data.edges[].source` / `target`, `parsedData`, `trigger`, and every `connections.out` entry.
### 1. Give every node a stable id
Use short unique strings (`trig_1`, `wa_menu`, `ans_tours`). Reuse the **same** string everywhere. Sub-nodes live in `data.nodes` as siblings with `parentId` set to the parent’s id.
### 2. Draw canvas edges (`data.edges`)
Every edge:
| Field | Value |
| - | - |
| `type` | `"buttonedge"` |
| `targetHandle` | `"in"` always |
| `sourceHandle` | `"out"` from a **root** node (trigger, HTTP, wait, pointer, message **without** sub-nodes) |
| `sourceHandle` | `"in"` from a **sub-node** (answer, `true` / `false`, switch case, fallback, timeout) |
| `source` / `target` | Node ids that exist in `data.nodes` |
| `id` | Optional unique string (board / analytics). Safe to send `e__`. |
```json theme={null}
[
{
"id": "e_trig_1_wa_menu",
"source": "trig_1",
"target": "wa_menu",
"sourceHandle": "out",
"targetHandle": "in",
"type": "buttonedge"
},
{
"id": "e_ans_tours_ptr_tours",
"source": "ans_tours",
"target": "ptr_tours",
"sourceHandle": "in",
"targetHandle": "in",
"type": "buttonedge"
}
]
```
Do **not** also draw a root `"out"` edge from a parent that has sub-nodes. The engine ignores parent `connections.out` once `subNodes` exist and continues from the **chosen** child’s `connections.out`.
**Fallback must not point at its parent.** Never add a canvas edge `fallback → parent` and never put the parent id in fallback `connections.out`. Two modes (mutually exclusive — if `shouldSendReplyMessage` is true, runtime **ignores** `connections.out` and re-queues the same message node after sending the text):
| Mode | JSON | When |
| - | - | - |
| **A. Prompt again** | `shouldSendReplyMessage: true`, `unknownAnswerReplyMessage: "Please select from the options."`, `connections: {}`, **no** canvas edge | Customer typed instead of tapping a button |
| **B. Branch away** | `shouldSendReplyMessage` omitted/false, `connections.out` to a **different** node (human handoff, `answerMode: "ai"`, another explanation, or a [workflow pointer](/build-workflows/sub-workflows)) | Escalate / switch channel of handling |
### 3. Copy each edge into execution
For every canvas edge `source → target`, find that source in `trigger`, `parsedData[]`, or `parent.subNodes[]` and append `target` to `connections.out` (array of target **ids** only — no handles).
| Canvas | Execution |
| - | - |
| Trigger `out` → first action | `workflow.trigger.connections.out: ["wa_menu"]` |
| Root HTTP `out` → next | That HTTP node’s `connections.out: ["upd_conv"]` |
| Answer / true / case `in` → next | That **sub-node’s** `connections.out` (parent’s `connections` stays `{}`) |
Runtime starts at `trigger.connections.out[0]`. If that array is missing, the run never starts.
### 4. Sanity check
* Every `source` / `target` / `connections.out` id exists.
* Trigger is **not** in `parsedData`.
* Sub-nodes are **not** extra `parsedData` items; they hang off `parent.subNodes[]`.
* Parent with answers / true-false / cases has **no** root `out` edge.
Paired snippets: [Examples](/build-workflows/examples).
# Workflow document
Source: https://docs.pingmee.co.il/build-workflows/document
Top-level fields on GET / POST / PUT /workflows/{workflowId}
A workflow is a `BaseSubFolder` plus a dual graph. These keys appear on [GET /workflows/](/api-reference/workflows/get) and on create/update bodies.
| Field | Role |
| - | - |
| `id` | Client-supplied on create (path). Server copies the path id. |
| `name` | Required on create. Human title. |
| `folderId` | Required on create as the `folderId` **query** param. |
| `associatedTo` | Owner. **Server overwrites** from the Cookie. Do not invent it. |
| `associatedToBusinessId` | **Required for WhatsApp send.** WhatsApp **phone-number id** (`GET /phone-numbers` item `id`) or Facebook page id. See [Platform binding](#platform-binding). |
| `associatedToPlatformId` | Optional alias of the **same** id (phone-number id / page id). Runtime QR links fall back to this if `associatedToBusinessId` is blank. Prefer setting `associatedToBusinessId`. |
| `platformType` | `whatsapp` \| `facebookMessenger` \| `instagram` \| `web` \| … Set this on **create** together with the phone id. PUT currently persists `associatedToBusinessId` but not `platformType`. |
| `trigger` | Execution trigger node. **Not** inside `parsedData`. Runtime output can include `triggerData` from [POST /trigger](/api-reference/workflows/trigger) — see [Variables](/build-workflows/variables#triggerdata). |
| `triggerType` | Copy of `trigger.data.eventType` (for example `"Messages Created"`). Often omitted on `workflowTrigger` children. |
| `variables` | Workflow-level map. Usually `{}` unless you set bindings here. |
| `data` | React Flow canvas: `{ nodes, edges, … }`. Required for the board. |
| `autoLayout` | Request-only. `true` runs board Clean (root dagre) and snaps sub-node positions. Not stored. Send on every graph create/PUT. `autoClean` is an alias. |
| `parsedData` | Execution parents only (`WorkflowNode[]`). Required to run. |
| `settings` | Optional `sharedTimeout` / `sharedFallback` (see [Sub-workflows](/build-workflows/sub-workflows)). |
| `isActive` | Create always stores `false`. Activate with a separate PUT. |
| `lastModified` | Unix seconds. Echo on graph PUT or get **409**. |
| `created` | Unix seconds. Server-set on create. |
| `analytics` | GET-only canvas counters (Expert AI). See [Workflow analytics](/build-workflows/analytics). |
Omit `analytics`, `executionCount`, `lastExecution`, and customer-identifying `notes` when you author a graph.
## What create overwrites
`POST /workflows/{workflowId}?folderId=` spreads your body, then forces:
```json theme={null}
{
"id": "",
"associatedTo": "",
"isActive": false,
"name": "",
"folderId": "",
"created": 1770000000,
"lastModified": 1770000000,
"executionCount": 0,
"lastExecution": 0,
"notes": "",
"data": {}
}
```
`data` stays as you sent it when present; otherwise `{}`. You **can** send `parsedData`, `trigger`, `triggerType`, `variables`, `settings`, `associatedToBusinessId`, `associatedToPlatformId`, and `platformType` on create (the duplicate-workflow path does this). The workflow still starts inactive.
## Minimal create body
```json theme={null}
{
"name": "Welcome menu",
"associatedToBusinessId": "phone_number_id_example",
"platformType": "whatsapp",
"variables": {},
"triggerType": "Messages Created",
"trigger": {},
"data": { "nodes": [], "edges": [] },
"parsedData": []
}
```
Fill `trigger`, `data`, and `parsedData` using [Canvas vs execution](/build-workflows/canvas-vs-execution) before you activate.
## Platform binding
The board picker stores `associatedToBusinessId = selectablePlatform.id` and `platformType = selectablePlatform.type`. An AI must do the same: **fetch**, **ask if ambiguous**, **write the ids**.
### WhatsApp (documented API)
1. `GET https://business.pingmee.co.il/phone-numbers` — [Get phone numbers](/api-reference/business/get-phone-numbers).
2. Response body is `{ "phoneNumbers": [ … ] }`. Each row includes:
* `id` — **this** is `associatedToBusinessId` (Meta phone-number id, not the display digits).
* `display_phone_number` / `verified_name` — show these to the user.
* `type` — `whatsapp` or `greenAPI`; copy into `platformType` (`greenAPI` workflows still use WhatsApp-shaped message nodes).
* `associatedBusinessId` — WABA id. **Do not** put this in `associatedToBusinessId`.
3. If the array is empty, stop and tell the user to connect a WhatsApp number in Pingmee. If it has **more than one** row, ask which number this workflow should send from. Do not guess.
4. If this is a **starter** / outbound bot (or the user named a catalog template), [GET templates](/api-reference/business/get-templates) uses `phoneNumberId=`. Fetch templates only **after** the phone is chosen. Skip GET `/templates` for inbound reply bots (`pingmeeTrigger`) unless the user asked for a named template.
`associatedToPlatformId` is the same phone-number id when you set it. The GUI often omits it; `associatedToBusinessId` is enough.
### Facebook / Instagram / web
There is **no** public “list pages / list Instagram accounts” endpoint in this API. `GET /phone-numbers` is WhatsApp (and Green-API) only. For Messenger / Instagram, only set `associatedToBusinessId` to a page id the user already gave you. Do not invent a page id.
## Graph vs metadata PUT
| Body | `lastModified` |
| - | - |
| `name`, `isActive`, `settings`, and/or `associatedToBusinessId` only | Not required |
| `data` or `parsedData` present | Required; must match the stored value |
A graph PUT also writes `trigger`, `triggerType`, and `variables` from the body.
# Examples
Source: https://docs.pingmee.co.il/build-workflows/examples
Sanitized dual-graph patterns from real WorkflowsTable documents
Each accordion is one pattern. Snippets are **generic**: names, phones, domains, template bodies, Fireberry fields, agent ids, and real workflow ids are replaced. Structure matches production graphs — node types, handle ids, `connections.out`, Variable `{ id, name, parentId, valueType }`, sub-node parenting, and pointer placeholders such as `"wf_child_support"`.
Reply bot: the customer messages first (`pingmeeTrigger` with `eventType: "Messages Created"` and a keyword gate). **No Meta template** — first WhatsApp node is `messageType: "custom"` with a BODY. Do not call [GET /templates](/api-reference/business/get-templates) unless the user named a catalog template. For a bot that **opens** the thread, see [Conversation opener](#conversation-opener-meta-template).
**Canvas**
```json theme={null}
{
"nodes": [
{
"id": "trig_1",
"type": "pingmeeTrigger",
"category": "Trigger",
"position": { "x": 80, "y": 80 },
"data": {
"eventType": "Messages Created",
"platform": "whatsapp",
"keywords": ["hello", "start"],
"isActive": true
}
},
{
"id": "wa_hello",
"type": "whatsapp",
"category": "Action",
"position": { "x": 320, "y": 80 },
"data": {
"platform": "whatsapp",
"type": "default",
"messageType": "custom",
"isActive": true,
"templateInformation": {
"components": [
{ "type": "BODY", "text": "Hi, thanks for messaging us. How can we help?" }
]
}
}
}
],
"edges": [
{
"source": "trig_1",
"target": "wa_hello",
"sourceHandle": "out",
"targetHandle": "in",
"type": "buttonedge"
}
]
}
```
**Execution**
```json theme={null}
{
"triggerType": "Messages Created",
"trigger": {
"id": "trig_1",
"type": "pingmeeTrigger",
"category": "Trigger",
"title": "pingmeeTrigger",
"data": {
"eventType": "Messages Created",
"platform": "whatsapp",
"keywords": ["hello", "start"]
},
"connections": { "out": ["wa_hello"] }
},
"parsedData": [
{
"id": "wa_hello",
"type": "whatsapp",
"category": "Action",
"title": "whatsapp",
"data": {
"platform": "whatsapp",
"type": "default",
"messageType": "custom",
"templateInformation": {
"components": [
{ "type": "BODY", "text": "Hi, thanks for messaging us. How can we help?" }
]
}
},
"connections": {},
"subNodes": []
}
]
}
```
The bot **starts** the thread (`workflowTrigger` + [POST /trigger](/api-reference/workflows/trigger)). This is a **starter**, so Meta requires an approved template. Fetch [GET /templates](/api-reference/business/get-templates) for the workflow’s `associatedToBusinessId`, copy one APPROVED object into `templateInformation`, set `messageType: "template"` **and** `templateType: "whatsapp"` (runtime keys off `templateType`). Bind `{{n}}` with `bodyVariables` / `headerVariables`. Reply bots (inbound `pingmeeTrigger`) use `messageType: "custom"` instead — [Pingmee trigger + keywords](#pingmee-trigger--keywords).
Document fields (set on create): `associatedToBusinessId` = phone-number `id` from [GET /phone-numbers](/api-reference/business/get-phone-numbers), `platformType: "whatsapp"`.
**Canvas**
```json theme={null}
{
"nodes": [
{
"id": "trig_1",
"type": "workflowTrigger",
"category": "Trigger",
"position": { "x": 80, "y": 80 },
"data": { "isActive": true }
},
{
"id": "wa_open",
"type": "whatsapp",
"category": "Action",
"position": { "x": 320, "y": 80 },
"data": {
"platform": "whatsapp",
"type": "default",
"messageType": "template",
"templateType": "whatsapp",
"templateInformation": {
"id": "template_id_from_get",
"name": "welcome_opener",
"language": "en",
"category": "UTILITY",
"status": "APPROVED",
"allow_category_change": false,
"components": [
{ "type": "BODY", "text": "Hello {{1}}, thanks for reaching out." }
]
},
"bodyVariables": {
"1": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
}
}
}
}
],
"edges": [
{
"id": "e_trig_1_wa_open",
"source": "trig_1",
"target": "wa_open",
"sourceHandle": "out",
"targetHandle": "in",
"type": "buttonedge"
}
]
}
```
**Execution**
```json theme={null}
{
"associatedToBusinessId": "phone_number_id_example",
"platformType": "whatsapp",
"trigger": {
"id": "trig_1",
"type": "workflowTrigger",
"category": "Trigger",
"title": "workflowTrigger",
"data": { "isActive": true },
"connections": { "out": ["wa_open"] }
},
"parsedData": [
{
"id": "wa_open",
"type": "whatsapp",
"category": "Action",
"title": "whatsapp",
"data": {
"platform": "whatsapp",
"messageType": "template",
"templateType": "whatsapp",
"templateInformation": {
"id": "template_id_from_get",
"name": "welcome_opener",
"language": "en",
"category": "UTILITY",
"status": "APPROVED",
"allow_category_change": false,
"components": [
{ "type": "BODY", "text": "Hello {{1}}, thanks for reaching out." }
]
},
"bodyVariables": {
"1": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
}
}
},
"connections": {},
"subNodes": []
}
]
}
```
A text HEADER with `{{1}}` uses `headerVariables` the same way. IMAGE / VIDEO / DOCUMENT headers are not available via API unless `attachmentS3Id` is already on the component. For `triggerData` bindings on the same shape, see [Workflow Trigger + triggerData](#workflow-trigger--triggerdata).
`bodyVariables` copy the trigger catalog. Answers are siblings on the canvas (`parentId`) and `subNodes` at runtime. Outgoing handle is `"in"`.
**Canvas (trimmed)**
```json theme={null}
{
"nodes": [
{
"id": "wa_menu",
"type": "whatsapp",
"parentId": null,
"data": {
"platform": "whatsapp",
"waitForUserResponse": true,
"messageType": "template",
"templateType": "whatsapp",
"templateInformation": {
"name": "welcome_menu_template",
"language": "en",
"components": [
{ "type": "BODY", "text": "Hello {{1}}, choose an option." },
{
"type": "BUTTONS",
"buttons": [
{ "type": "QUICK_REPLY", "text": "Tours" },
{ "type": "QUICK_REPLY", "text": "Help" }
]
}
]
},
"bodyVariables": {
"1": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
}
}
}
},
{
"id": "ans_tours",
"type": "answer",
"parentId": "wa_menu",
"draggable": false,
"extent": "parent",
"position": { "x": 138, "y": 120 },
"data": { "label": "Tours", "index": 0, "editable": true, "removable": true }
},
{
"id": "ans_help",
"type": "answer",
"parentId": "wa_menu",
"draggable": false,
"extent": "parent",
"position": { "x": 138, "y": 155 },
"data": { "label": "Help", "index": 1, "editable": true, "removable": true }
}
],
"edges": [
{
"source": "ans_tours",
"target": "ptr_tours",
"sourceHandle": "in",
"targetHandle": "in",
"type": "buttonedge"
}
]
}
```
**Execution (that parent only)**
```json theme={null}
{
"id": "wa_menu",
"type": "whatsapp",
"connections": {},
"subNodes": [
{
"id": "ans_tours",
"type": "answer",
"parentId": "wa_menu",
"title": "Tours",
"data": { "label": "Tours", "index": 0 },
"connections": { "out": ["ptr_tours"] }
},
{
"id": "ans_help",
"type": "answer",
"parentId": "wa_menu",
"title": "Help",
"data": { "label": "Help", "index": 1 },
"connections": { "out": ["ptr_support"] }
}
]
}
```
Manual payload on a `workflowTrigger` (same output shape on `pingmeeTrigger` / social triggers). POST `{ "triggerData": { "websiteLink": "https://example.com" } }` then bind `return $trig_1.triggerData.websiteLink;` in a WhatsApp slot. The `$…` id is the trigger **node** id. See [`triggerData`](/build-workflows/variables#triggerdata).
**Canvas**
```json theme={null}
{
"nodes": [
{
"id": "trig_1",
"type": "workflowTrigger",
"category": "Trigger",
"position": { "x": 80, "y": 80 },
"data": { "isActive": true }
},
{
"id": "wa_link",
"type": "whatsapp",
"category": "Action",
"position": { "x": 320, "y": 80 },
"data": {
"platform": "whatsapp",
"messageType": "template",
"templateType": "whatsapp",
"templateInformation": {
"name": "welcome_link_template",
"language": "en",
"components": [
{ "type": "BODY", "text": "Here is your link: {{1}}" }
]
},
"bodyVariables": {
"1": {
"id": "expr_link",
"name": "websiteLink",
"parentId": "trig_1",
"valueType": "string",
"expression": "return $trig_1.triggerData.websiteLink;"
}
}
}
}
],
"edges": [
{
"source": "trig_1",
"target": "wa_link",
"sourceHandle": "out",
"targetHandle": "in",
"type": "buttonedge"
}
]
}
```
**Execution**
```json theme={null}
{
"trigger": {
"id": "trig_1",
"type": "workflowTrigger",
"category": "Trigger",
"title": "workflowTrigger",
"data": { "isActive": true },
"connections": { "out": ["wa_link"] }
},
"parsedData": [
{
"id": "wa_link",
"type": "whatsapp",
"category": "Action",
"title": "whatsapp",
"data": {
"platform": "whatsapp",
"messageType": "template",
"templateType": "whatsapp",
"templateInformation": {
"name": "welcome_link_template",
"language": "en",
"components": [
{ "type": "BODY", "text": "Here is your link: {{1}}" }
]
},
"bodyVariables": {
"1": {
"id": "expr_link",
"name": "websiteLink",
"parentId": "trig_1",
"valueType": "string",
"expression": "return $trig_1.triggerData.websiteLink;"
}
}
},
"connections": {},
"subNodes": []
}
]
}
```
Then [POST /trigger](/api-reference/workflows/trigger):
```json theme={null}
{
"phoneNumberId": "phone_number_id_example",
"customerPhoneNumberId": "customer_phone_id_example",
"triggerData": { "websiteLink": "https://example.com" }
}
```
The same Expression works in an HTTP `bodyVariables` slot. Save both graphs, activate, then trigger.
Parent holds `conditions`. Children titled `true` / `false`. Real booking bots use `"Is Empty"` on a trigger field (for example customer name) before an HTTP or Fireberry write.
```json theme={null}
{
"id": "if_1",
"type": "if",
"category": "Condition",
"data": {
"evaluationMode": "ALL",
"conditions": [
{
"id": "c1",
"firstValue": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
},
"comparison": "Is Empty"
}
]
},
"connections": {},
"subNodes": [
{
"id": "ev_true",
"type": "conditionEvaluation",
"parentId": "if_1",
"title": "true",
"data": { "label": "true", "index": 0 },
"connections": { "out": ["upd_cust"] }
},
{
"id": "ev_false",
"type": "conditionEvaluation",
"parentId": "if_1",
"title": "false",
"data": { "label": "false", "index": 1 },
"connections": { "out": ["http_1"] }
}
]
}
```
Canvas edges from those children use `sourceHandle: "in"`.
Product-line bots switch on a catalog field, then pointer into a region workflow. Conditions sit on **each case child**.
```json theme={null}
{
"id": "sw_1",
"type": "switch",
"connections": {},
"subNodes": [
{
"id": "case_bike",
"type": "switchCaseEvaluation",
"parentId": "sw_1",
"title": "Bikes",
"data": {
"label": "Bikes",
"evaluationMode": "ALL",
"conditions": [
{
"id": "c_bike",
"firstValue": {
"id": "var_interest",
"name": "message.type",
"parentId": "trig_1",
"valueType": "string"
},
"comparison": "Equals",
"secondValue": "bikes"
}
]
},
"connections": { "out": ["ptr_bikes"] }
},
{
"id": "sw_fb",
"type": "noSelectionFallback",
"parentId": "sw_1",
"title": "Fallback",
"data": { "label": "Fallback" },
"connections": { "out": ["wa_default"] }
}
]
}
```
Reminder and booking flows POST JSON with `{{1}}` bound to a trigger variable (often a campaign parameter expression).
```json theme={null}
{
"id": "http_1",
"type": "httpRequest",
"category": "Action",
"data": {
"method": "POST",
"url": "https://api.example.com/leads",
"contentType": "application/json",
"body": "{\"name\":\"{{1}}\",\"phone\":\"{{2}}\"}",
"bodyVariables": {
"1": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
},
"2": {
"id": "var_cust_phone",
"name": "customer.phoneNumber",
"parentId": "trig_1",
"valueType": "string"
}
}
},
"connections": { "out": ["upd_conv"] }
}
```
Root canvas edge: `sourceHandle: "out"`.
Inbound bots create-or-update a CRM row keyed by phone. Table and field names below are generic stand-ins.
```json theme={null}
{
"id": "crm_1",
"type": "fireberry",
"category": "Action",
"data": {
"action": "Create If None Exist",
"table": { "name": "Contacts", "objectType": "1" },
"objectField": { "fieldName": "telephone", "label": "Phone" },
"objectVariable": {
"id": "var_cust_phone",
"name": "customer.phoneNumber",
"parentId": "trig_1",
"valueType": "string"
},
"queries": [
{
"id": "q1",
"field": { "fieldName": "telephone", "label": "Phone" },
"value": {
"id": "var_cust_phone",
"name": "customer.phoneNumber",
"parentId": "trig_1",
"valueType": "string"
}
}
]
},
"connections": { "out": ["wa_menu"] }
}
```
Other `action` values seen: `"Create"`, `"Update Record"`.
Region menus create a follow-up task, then pointer. Title uses `{{1}}`.
```json theme={null}
{
"id": "task_1",
"type": "task",
"category": "Action",
"data": {
"task": {
"title": "Follow up {{1}}",
"body": "Lead asked about this product line.",
"statusCase": "open"
},
"titleVariables": {
"1": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
}
}
},
"connections": { "out": ["ptr_region"] }
}
```
Used on long booking trees between a message and an `updateConversation`.
```json theme={null}
{
"id": "wait_1",
"type": "wait",
"category": "Action",
"data": {
"timerInfo": {
"hours": "0",
"minutes": "15",
"seconds": "0",
"numberOfSeconds": 900
}
},
"connections": { "out": ["upd_conv"] }
}
```
Follow-up and booking bots tag the thread, flip answer mode, and bind customer fields to the trigger catalog (not raw PII).
```json theme={null}
{
"id": "upd_conv",
"type": "updateConversation",
"data": {
"answerMode": "manual",
"status": "open",
"selectedTags": [{
"id": "tag_new_lead",
"title": "New lead",
"color": "#16A34A",
"associatedTo": "business@example.com"
}],
"selectedAgents": [{ "id": "agent_example", "name": "Support agent" }]
},
"connections": { "out": ["upd_cust"] }
}
```
```json theme={null}
{
"id": "upd_cust",
"type": "updateCustomer",
"data": {
"customerNickname": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
},
"phoneNumber": {
"id": "var_cust_phone",
"name": "customer.phoneNumber",
"parentId": "trig_1",
"valueType": "string"
}
},
"connections": {}
}
```
Reminder flows call `ai` after HTTP / Fireberry. Summarize publishes a `summary` output for later templates.
```json theme={null}
{
"id": "ai_1",
"type": "ai",
"category": "Action",
"data": {
"aiOption": "summarizeConversation",
"variables": {
"var_summary": {
"id": "var_summary",
"name": "summary",
"parentId": "ai_1",
"valueType": "string"
}
}
},
"connections": { "out": ["wa_summary"] }
}
```
`aiOption` may also be `"answerWithAI"` (no `summary` catalog).
Lead router: Pingmee trigger → menu answers → three pointers. Each child is a separate document that starts with `workflowTrigger` and is active.
**Parent execution (trimmed)**
```json theme={null}
{
"triggerType": "Messages Created",
"trigger": {
"id": "trig_1",
"type": "pingmeeTrigger",
"category": "Trigger",
"title": "pingmeeTrigger",
"data": { "eventType": "Messages Created", "platform": "whatsapp" },
"connections": { "out": ["wa_menu"] }
},
"parsedData": [
{
"id": "wa_menu",
"type": "whatsapp",
"connections": {},
"subNodes": [
{
"id": "ans_a",
"type": "answer",
"parentId": "wa_menu",
"title": "Option A",
"connections": { "out": ["ptr_a"] }
}
]
},
{
"id": "ptr_a",
"type": "workflowPointer",
"category": "Operation",
"title": "workflowPointer",
"data": {
"workflowId": "wf_child_support",
"workflowName": "Child support",
"workflowFolderId": "folder_main"
},
"connections": { "out": ["upd_conv"] }
}
]
}
```
**Child execution**
```json theme={null}
{
"id": "wf_child_support",
"isActive": true,
"trigger": {
"id": "trig_child",
"type": "workflowTrigger",
"category": "Trigger",
"title": "workflowTrigger",
"data": { "isActive": true },
"connections": { "out": ["wa_hello"] }
},
"parsedData": [
{
"id": "wa_hello",
"type": "whatsapp",
"title": "whatsapp",
"data": { "platform": "whatsapp" },
"connections": {},
"subNodes": []
}
]
}
```
Parent canvas edges: trigger `out` → menu; answer `in` → pointer. After the child finishes, parent resumes at `upd_conv`.
Landing-style child: `workflowTrigger` → WhatsApp with **expression** parameters from [`triggerData`](/build-workflows/variables#triggerdata) → answers → Fireberry → pointer to a shared closer. Also shows `settings.sharedTimeout`. This is the shape to copy when you need both bindings and a sub-workflow. For the payload-only pattern, see [Workflow Trigger + triggerData](#workflow-trigger--triggerdata).
**Document (trimmed)**
```json theme={null}
{
"id": "wf_landing",
"name": "Landing",
"folderId": "folder_main",
"associatedToBusinessId": "phone_number_id_example",
"platformType": "whatsapp",
"variables": {},
"isActive": true,
"settings": {
"sharedTimeout": {
"enabled": true,
"workflowId": "wf_child_support",
"timerInfo": {
"hours": "3",
"minutes": "0",
"seconds": "0",
"numberOfSeconds": 10800
}
},
"sharedFallback": { "enabled": false }
},
"trigger": {
"id": "trig_land",
"type": "workflowTrigger",
"category": "Trigger",
"title": "workflowTrigger",
"data": { "isActive": true },
"connections": { "out": ["wa_offer"] }
},
"parsedData": [
{
"id": "wa_offer",
"type": "whatsapp",
"category": "Action",
"title": "whatsapp",
"data": {
"platform": "whatsapp",
"waitForUserResponse": true,
"messageType": "template",
"templateType": "whatsapp",
"templateInformation": {
"name": "welcome_menu_template",
"language": "en",
"components": [
{ "type": "BODY", "text": "Hello {{1}}, see {{2}}." }
]
},
"bodyVariables": {
"1": {
"id": "expr_name",
"name": "product_name",
"parentId": "trig_land",
"valueType": "string",
"expression": "return $trig_land.triggerData.product_name;"
},
"2": {
"id": "expr_link",
"name": "websiteLink",
"parentId": "trig_land",
"valueType": "string",
"expression": "return $trig_land.triggerData.websiteLink;"
}
}
},
"connections": {},
"subNodes": [
{
"id": "ans_yes",
"type": "answer",
"parentId": "wa_offer",
"title": "Book",
"data": { "label": "Book", "index": 0 },
"connections": { "out": ["crm_1"] }
},
{
"id": "ans_later",
"type": "answer",
"parentId": "wa_offer",
"title": "Later",
"data": { "label": "Later", "index": 1 },
"connections": { "out": ["ptr_followup"] }
}
]
},
{
"id": "crm_1",
"type": "fireberry",
"title": "fireberry",
"data": {
"action": "Create If None Exist",
"table": { "name": "Contacts", "objectType": "1" },
"objectField": { "fieldName": "telephone", "label": "Phone" },
"objectVariable": {
"id": "var_cust_phone",
"name": "customer.phoneNumber",
"parentId": "trig_land",
"valueType": "string"
}
},
"connections": { "out": ["ptr_followup"] }
},
{
"id": "ptr_followup",
"type": "workflowPointer",
"category": "Operation",
"title": "workflowPointer",
"data": {
"workflowId": "wf_child_support",
"workflowName": "Child support",
"workflowFolderId": "folder_main"
},
"connections": {}
}
]
}
```
**Matching canvas edges**
```json theme={null}
{
"nodes": [
{ "id": "trig_land", "type": "workflowTrigger", "position": { "x": 80, "y": 80 }, "data": {} },
{ "id": "wa_offer", "type": "whatsapp", "position": { "x": 320, "y": 80 }, "data": { "waitForUserResponse": true } },
{ "id": "ans_yes", "type": "answer", "parentId": "wa_offer", "data": { "label": "Book", "index": 0 } },
{ "id": "ans_later", "type": "answer", "parentId": "wa_offer", "data": { "label": "Later", "index": 1 } },
{ "id": "crm_1", "type": "fireberry", "position": { "x": 560, "y": 40 }, "data": {} },
{ "id": "ptr_followup", "type": "workflowPointer", "position": { "x": 800, "y": 80 }, "data": { "workflowId": "wf_child_support" } }
],
"edges": [
{ "source": "trig_land", "target": "wa_offer", "sourceHandle": "out", "targetHandle": "in", "type": "buttonedge" },
{ "source": "ans_yes", "target": "crm_1", "sourceHandle": "in", "targetHandle": "in", "type": "buttonedge" },
{ "source": "ans_later", "target": "ptr_followup", "sourceHandle": "in", "targetHandle": "in", "type": "buttonedge" },
{ "source": "crm_1", "target": "ptr_followup", "sourceHandle": "out", "targetHandle": "in", "type": "buttonedge" }
]
}
```
`wf_child_support` must already be active and start with `workflowTrigger`. Save both graphs, then activate, then trigger — see [overview](/build-workflows/overview).
# Nodes and edges
Source: https://docs.pingmee.co.il/build-workflows/nodes-and-edges
React Flow JSON, parsedData WorkflowNode shape, handle IDs, and NodeType fragments
## Canvas node
```json theme={null}
{
"id": "http_1",
"type": "httpRequest",
"category": "Action",
"position": { "x": 480, "y": 80 },
"draggable": true,
"selected": false,
"data": {
"isActive": true,
"method": "POST",
"url": "https://api.example.com/leads",
"contentType": "application/json",
"body": "{\"name\":\"{{1}}\"}"
}
}
```
## Canvas edge
```json theme={null}
{
"source": "trig_1",
"target": "http_1",
"sourceHandle": "out",
"targetHandle": "in",
"type": "buttonedge"
}
```
Handles: root output `"out"`, every input `"in"`, sub-node output **also** `"in"`. See [Canvas vs execution](/build-workflows/canvas-vs-execution).
## Execution `WorkflowNode`
```json theme={null}
{
"id": "http_1",
"type": "httpRequest",
"category": "Action",
"title": "httpRequest",
"data": {},
"connections": { "out": ["upd_conv"] },
"subNodes": []
}
```
`parentId` is set on sub-nodes only. `connections.out` is an array of target node ids.
## NodeType catalog
Ignore `stickyNote` (canvas decoration) and deprecated `assignTags` / `assignAgents` (use `updateConversation`).
### Triggers — category `Trigger`
Exactly **one** trigger. It lives in `data.nodes` and `workflow.trigger`, never in `parsedData`.
| `type` | Required `data` keys | Fragment |
| - | - | - |
| `pingmeeTrigger` | `eventType`, `platform` | `eventType`: `"Messages Created"` or `"New Conversation Started"`. Optional `keywords: ["hello"]`. Inbound reply — first WhatsApp node may be `messageType: "custom"`. Output catalog under `data.variables` (`customer.customerName`, `message`, …). |
| `workflowTrigger` | (none required) | Child entry / starter. Same `variables` catalog. `triggerType` often omitted. Template required only when this trigger **opens** the thread (POST `/trigger`, campaign); a pointer child in an inbound run may use `custom`. |
| `instagramTrigger` | `eventType`, `platform` | `eventType`: `"Comment on Instagram Post"`. Optional `keywords`, `post`. |
| `facebookTrigger` | `eventType`, `platform` | `eventType`: `"Comment on Facebook Post"`. Optional `keywords`, `post`. |
Manual [POST `/trigger`](/api-reference/workflows/trigger) `triggerData` is copied onto **whichever trigger type ran**. Read it with `return $.triggerData.;` — see [`triggerData`](/build-workflows/variables#triggerdata).
```json theme={null}
{
"id": "trig_1",
"type": "pingmeeTrigger",
"category": "Trigger",
"data": {
"eventType": "Messages Created",
"platform": "whatsapp",
"keywords": ["hello", "start"],
"isActive": true,
"variables": {
"var_cust_name": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
}
}
}
}
```
### Actions — category `Action`
| `type` | Required `data` keys | Fragment |
| - | - | - |
| `whatsapp` | `platform: "whatsapp"` | Infer starter vs reply first ([WhatsApp templates](#whatsapp-templates)). `messageType`: `"custom"` \| `"template"` \| `"ai"`. Meta catalog send also needs `templateType: "whatsapp"`. Wait-for-reply: `waitForUserResponse: true` plus `answer` sub-nodes (max 10). Canvas also needs `data.subNodesConfig`: `{ "subNodeType": "answer", "editable": true, "nodesEditable": true, "nodesRemovable": true, "subNodesLimit": 10 }` or the dashed **+** under the card is hidden. Graph save fills this when missing. **No public upload API** — text / template body only unless `attachmentS3Id` is already known. |
| `facebookMessenger` | `platform` | Same message shape. Answer limit 13. |
| `instagram` | `platform` | Same message shape. Answer limit 13. |
| `webchat` | `platform: "web"` | Same message shape. Answer limit 8. |
| `httpRequest` | `method`, `url` | `method`: `GET` \| `POST` \| `PUT` \| `PATCH` \| `DELETE`. `body` may contain `{{n}}` slots bound in `bodyVariables`. Optional `headers` (`["Content-Type: application/json"]`), `contentType`, `queryParams`, `responseMapping`. |
| `fireberry` | `table`, `action` | `action`: `"Create"` \| `"Create If None Exist"` \| `"Update Record"`. Match row with `objectField` + `objectVariable` and/or `queries[]`. Use generic table/field names — never copy a customer's CRM schema. |
| `task` | `task` | `task.title` / `task.body` may contain `{{n}}` bound by `titleVariables` / `bodyVariables`. Optional `task.statusCase` (`open`). |
| `wait` | `timerInfo` | `{ "hours": "0", "minutes": "15", "seconds": "0", "numberOfSeconds": 900 }`. Max \~82 000 seconds. |
| `updateConversation` | (all optional) | `selectedTags`, `selectedAgents`, `answerMode`: `"bot"` \| `"manual"` \| `"ai"`, `status`: `"open"` \| `"closed"` \| `"standby"`, `optoutFromMarketingMessage`, `autoTranslation`, `aiSummarize`. |
| `updateCustomer` | (all optional) | `phoneNumber`, `email`, `customerNickname` — each is a Variable / Expression (`parentId` = upstream node), not a raw PII string. |
| `ai` | `aiOption` | `"answerWithAI"` or `"summarizeConversation"`. Summarize exposes `data.variables` `{ name: "summary", parentId: }`. |
### WhatsApp templates
The first WhatsApp node is **not** always a template. Infer from user intent:
| User intent | Trigger | First WhatsApp node |
| - | - | - |
| Reply / keyword / inbound | `pingmeeTrigger` (`Messages Created`, `New Conversation Started`) | `messageType: "custom"` OK. Do not call GET `/templates` unless the user named a template. |
| Starter / outbound / “send this to customers” / API trigger | `workflowTrigger` | `messageType: "template"` + `templateType: "whatsapp"`. [GET /templates](/api-reference/business/get-templates). |
Meta requires an **approved template** when the business **opens** a WhatsApp conversation (outside the 24-hour customer-care window). Pingmee encodes that as a message node with both `messageType: "template"` and `templateType: "whatsapp"`. A `workflowTrigger` **child** reached from a [pointer](/build-workflows/sub-workflows) during an inbound run is still in-session — `custom` is OK.
| Field | Value for a Meta catalog send |
| - | - |
| `messageType` | `"template"` (board mode). `"custom"` is a session/interactive message; `"ai"` is answer-with-AI. |
| `templateType` | `"whatsapp"` — **this** is what the runtime checks (`MessageTemplateType.whatsapp`). Company/private templates are not Meta HSM sends. |
| `templateInformation` | Copy an **APPROVED** object from [GET /templates](/api-reference/business/get-templates) (`name`, `language`, `category`, `id`, `status`, `components`, `allow_category_change`). Do not invent `name` / `language`. |
| `bodyVariables` | Map keyed by placeholder index (`"1"`, `"2"`, …) matching `BODY` `{{n}}`. Each value is a Fixed / Expression / Custom object ([Variables](/build-workflows/variables)). |
| `headerVariables` | Same shape for a **text** HEADER `{{n}}`. IMAGE / VIDEO / DOCUMENT headers need `components[].attachmentS3Id` — agents cannot upload media (see below). |
| `buttonVariables` | Optional. Dynamic URL / coupon suffixes keyed by **button index** (0-based) in the BUTTONS component. |
**When `messageType: "template"`**
* Starter: `workflowTrigger` + [POST /trigger](/api-reference/workflows/trigger) / campaign-style send that **opens** the thread — the **first** WhatsApp node must be a template.
* Any send after the 24-hour window has closed.
* The user asked for a **named** catalog template on a reply bot.
**When `messageType: "custom"` is OK**
* Reply bot: inbound `pingmeeTrigger` with `eventType: "Messages Created"` or `"New Conversation Started"` (the customer just wrote; the window is open). First node can be custom text.
* Later nodes in the **same** run after the customer has already replied to a template.
* Pointer child continuing an inbound session.
**Fetch and map** (starter only, or a named template)
1. Bind the workflow phone ([Platform binding](/build-workflows/document#platform-binding)).
2. `GET https://api.pingmee.co.il/templates?type=whatsapp&phoneNumberId=`.
3. Keep `status === "APPROVED"`. Skip PENDING / REJECTED.
4. Prefer BODY-only or BODY + QUICK\_REPLY / URL buttons. If HEADER `format` is `IMAGE`, `VIDEO`, or `DOCUMENT` and no `attachmentS3Id` is on that component, **do not use that template** via API.
5. Paste the template into `data.templateInformation` on both the canvas node and the `parsedData` node.
6. Bind every `{{n}}` in BODY (and text HEADER). Missing keys send empty parameters and Meta rejects the send.
7. If BUTTONS include `QUICK_REPLY`, set `waitForUserResponse: true` and add matching `answer` sub-nodes (`label` = button `text`, `index` 0-based). Outgoing edges from those answers use `sourceHandle: "in"`.
Reply snippet: [Examples — Pingmee trigger + keywords](/build-workflows/examples#pingmee-trigger--keywords). Starter snippet: [Examples — conversation opener](/build-workflows/examples#conversation-opener-meta-template).
**Attachments (limitation)**
There is **no** documented public upload API for agents. The board uploads blob URLs on save into `attachmentS3Id`. An AI authoring via REST must stay **text / template-only** unless an `attachmentS3Id` is already stored on the template component from a previous GUI save. Never put `blob:` URLs or arbitrary CDN links in `templateInformation.attachment` or `header_handle`.
```json theme={null}
{
"type": "httpRequest",
"data": {
"method": "POST",
"url": "https://api.example.com/leads",
"contentType": "application/json",
"body": "{\"name\":\"{{1}}\",\"phone\":\"{{2}}\"}",
"bodyVariables": {
"1": { "id": "var_cust_name", "name": "customer.customerName", "parentId": "trig_1", "valueType": "string" },
"2": { "id": "var_cust_phone", "name": "customer.phoneNumber", "parentId": "trig_1", "valueType": "string" }
}
}
}
```
```json theme={null}
{
"type": "fireberry",
"data": {
"action": "Create If None Exist",
"table": { "name": "Contacts", "objectType": "1" },
"objectField": { "fieldName": "telephone", "label": "Phone" },
"objectVariable": { "id": "var_cust_phone", "name": "customer.phoneNumber", "parentId": "trig_1", "valueType": "string" },
"queries": [
{
"id": "q1",
"field": { "fieldName": "telephone", "label": "Phone" },
"value": { "id": "var_cust_phone", "name": "customer.phoneNumber", "parentId": "trig_1", "valueType": "string" }
}
]
}
}
```
### Conditions — category `Condition`
| `type` | Required `data` keys | Fragment |
| - | - | - |
| `if` | `conditions` | `evaluationMode`: `"ALL"` \| `"ANY"` (board). Runtime ANDs conditions in order. Two `conditionEvaluation` children titled `true` / `false`. |
| `switch` | (cases on children) | Each `switchCaseEvaluation` child has `conditions` + `evaluationMode`. Optional `noSelectionFallback`. First matching case wins. Limit 25 cases. |
```json theme={null}
{
"type": "if",
"data": {
"evaluationMode": "ALL",
"conditions": [
{
"id": "c1",
"firstValue": { "id": "var_cust_name", "name": "customer.customerName", "parentId": "trig_1", "valueType": "string" },
"comparison": "Is Empty"
}
]
}
}
```
`comparison` strings: `"Equals"`, `"Not Equal"`, `"Greater than"`, `"Greater than or equal"`, `"Less than"`, `"Less than or equal"`, `"Contains"`, `"Is Empty"`, `"Is Not Empty"`. `Contains` expects `secondValue` as `[{ "label": "hello" }]`.
### Operations — category `Operation`
| `type` | Required `data` keys | Fragment |
| - | - | - |
| `workflowPointer` | `workflowId` | Also `workflowName`, `workflowFolderId`. Target must be **active** and start with `workflowTrigger`. See [Sub-workflows](/build-workflows/sub-workflows). |
```json theme={null}
{
"type": "workflowPointer",
"data": {
"workflowId": "wf_child_support",
"workflowName": "Child support",
"workflowFolderId": "folder_main"
}
}
```
### Sub-nodes
| `type` | Parent | `data` keys |
| - | - | - |
| `answer` | Message nodes | `label`, `index`, `editable`, `removable`. Canvas: `parentId`, `draggable: false`, `extent: "parent"` (send `autoLayout: true` to snap `{ x: 138, y }`). Optional `buttonPayload`, `carouselCardIndex`. |
| `conditionEvaluation` | `if` | `label` / execution `title` must be `true` or `false`. Same canvas fields as answers (`draggable: false`, `extent: "parent"`). |
| `switchCaseEvaluation` | `switch` | `label`, `conditions`, `evaluationMode`. Same canvas fields. |
| `noSelectionFallback` | Message or `switch` | **Never** edge back to the parent. Mode A: `shouldSendReplyMessage` + `unknownAnswerReplyMessage`, empty `connections`. Mode B: `connections.out` to a different node (human / AI / pointer). |
| `awaitResponseTimeout` | Message | `label` `"Timeout"`, `shouldTimeoutExecution`, `timerInfo`. |
Full parenting rules: [Sub-nodes](/build-workflows/sub-nodes).
# Build a workflow
Source: https://docs.pingmee.co.il/build-workflows/overview
Create a complex workflow graph and save it through the Workflows API
This cookbook is for **AI agents and developers** who assemble a valid dual-graph payload and persist it with REST. One MCP is enough: [`https://docs.pingmee.co.il/mcp`](https://docs.pingmee.co.il/mcp) indexes these pages and the Workflows API.
Board UI field catalogs live on [Workflow Docs](https://docs-workflows.pingmee.co.il/docs) and are **optional** for this path.
## Goal
Create a folder, save a workflow that has both a canvas graph (`data`) and an execution graph (`parsedData` + `trigger`), activate it, then trigger it. Bind WhatsApp / HTTP / Fireberry fields with [Fixed, Expression, or Custom](/build-workflows/variables) parameter objects.
## Before you save (agents)
Do these **before** POST/PUT. Skipping them produces a graph that opens on the board but does not send from the right number, fails Meta’s 24-hour rule on starter/outbound, or drops media.
1. **Pick a phone / platform.** [GET phone numbers](/api-reference/business/get-phone-numbers) (`GET https://business.pingmee.co.il/phone-numbers`). If more than one number exists, **ask the user which one** to bind. Copy that item’s `id` into `associatedToBusinessId` and set `platformType` (`whatsapp` for those rows). Optional: set `associatedToPlatformId` to the same `id`. See [Workflow document](/build-workflows/document#platform-binding).
2. **Infer intent — the first WhatsApp message is not always a template.** Decide from what the user asked for (table below). Do **not** force a Meta template on a reply bot. See [Nodes and edges](/build-workflows/nodes-and-edges#whatsapp-templates).
3. **No media upload via this API.** There is **no public file-upload endpoint** for agents. GUI save uploads blob URLs to S3 (`attachmentS3Id`). Via REST, send **text or template body/buttons only**. Do not put blob/`https` upload URLs in `templateInformation`. If a catalog template requires an IMAGE / VIDEO / DOCUMENT header, skip it unless `components[].attachmentS3Id` is already known. See [Hard rules](/build-workflows/rules).
4. **Wire both graphs.** Copy every canvas edge into `connections.out` with matching node ids. Root output handle is `"out"`; sub-node output handle is `"in"`. Set `parentId` on buttons. Send **`autoLayout: true`** so the server Clean-layouts roots and snaps sub-nodes (`draggable: false`, `extent: "parent"`, relative x/y). Fallback never points at its parent. See [How to wire a graph](/build-workflows/canvas-vs-execution#how-to-wire-a-graph).
### Which trigger
| Use | Trigger |
| - | - |
| Listen for **customer message events** (keywords, new chat, tag/status change) | `pingmeeTrigger` |
| This graph is a **sub-workflow**, a **“send this bot to a customer”** send, or a **campaign** / [POST `/trigger`](/api-reference/workflows/trigger) starter | `workflowTrigger` |
### Infer starter vs reply
| User intent | Trigger | First WhatsApp node |
| - | - | - |
| Reply / keyword / inbound (“when a customer messages us…”) | `pingmeeTrigger` (`Messages Created`, `New Conversation Started`) | `messageType: "custom"` OK. Skip [GET templates](/api-reference/business/get-templates) unless the user named a specific template. |
| Starter / outbound / “send this to customers” / campaign / API trigger | `workflowTrigger` | `messageType: "template"` + `templateType: "whatsapp"`. [GET templates](/api-reference/business/get-templates) for the chosen `phoneNumberId`, pick **APPROVED**, copy into `templateInformation`, bind `bodyVariables` / `headerVariables`. |
A `workflowTrigger` **child** reached from a [pointer](/build-workflows/sub-workflows) during an inbound run is still in-session — `custom` is OK. Template is required when that trigger **starts** the thread ([POST /trigger](/api-reference/workflows/trigger), campaign-style, outside the 24-hour window).
Use [conversation tags](/api-reference/tags/get-all) on `updateConversation.selectedTags` when the flow should mark a lead, trial, or handoff. **GET tags first** and copy an existing `{ id, title, color, associatedTo }` — do not invent ids. Create only if nothing matches: [PUT `/conversation/tags`](/api-reference/tags/create) with `{ "title", "color" }` (the server assigns `id` and `associatedTo`). There is no `POST /conversation/tags`.
`POST https://workflows.pingmee.co.il/folders/{folderId}` with `{ "name": "Inbound" }`. You supply `folderId`. Or reuse a `folderId` from [list workflows (minimal)](/api-reference/workflows/get-minimal).
`POST https://workflows.pingmee.co.il/workflows/{workflowId}?folderId={folderId}` with at least `name`, `associatedToBusinessId`, and `platformType`. You may send the full graph in the same body. Include **`autoLayout: true`** whenever you send `data.nodes`. The server **always** stores `isActive: false` and sets `associatedTo` from the Cookie. Response includes `created` and `lastModified`.
Send **both** `data` and `parsedData` plus `trigger` / `triggerType`, and **`autoLayout: true`**. Echo `lastModified` from the last GET or create, or the API returns **409**.
`PUT /workflows/{workflowId}` with `{ "isActive": true }` only. No `lastModified` needed when you are not sending `data` or `parsedData`.
[POST /workflows//trigger](/api-reference/workflows/trigger). Optional body `triggerData` lands on the trigger node — read it with `return $.triggerData.;` ([Variables](/build-workflows/variables#triggerdata)). The child of a [workflow pointer](/build-workflows/sub-workflows) must already be **active** and start with `workflowTrigger`.
After executions, [GET](/api-reference/workflows/get) the same document and read `analytics` (Expert AI). Interpret drop-off / buttons, then PUT a new graph. See [Workflow analytics](/build-workflows/analytics).
An AI that writes only `data` will not run. An AI that writes only `parsedData` will not open correctly on the board. Always send both graphs with matching IDs, and `autoLayout: true` when you send `data.nodes`. See [Canvas vs execution](/build-workflows/canvas-vs-execution).
## What to read next
Top-level Workflow keys and what the server overwrites
React Flow `data` vs execution `parsedData` + `trigger`
NodeType table, handles, required `data` keys
Sanitized dual-graph patterns, including triggerData + pointer
GET `analytics`, interpret drop-off, PUT graph changes
Parameter slots: Fixed (tree pick), Expression (`return $nodeId…`), Custom (`{ custom }`)
# Hard rules
Source: https://docs.pingmee.co.il/build-workflows/rules
Failures that look silent: 409, inactive child, missing parsedData, wrong handles
## Must do
* Send **both** `data` and `parsedData` plus `trigger` / `triggerType` on any graph save.
* Keep the same node ids on the canvas, in `parsedData`, on `trigger`, and in every edge / `connections.out`.
* Create is always inactive. Activate with `PUT { "isActive": true }` (no graph fields).
* Graph PUT: echo `lastModified` from the last GET or create, or the API returns **409**.
* Leave `associatedTo` to the server (Cookie).
* **Pick a phone before save.** [GET phone numbers](/api-reference/business/get-phone-numbers); if more than one, ask the user. Set `associatedToBusinessId` to that row’s `id` (not `associatedBusinessId` / WABA) and `platformType` on **create**. Optional `associatedToPlatformId` is the same id.
* **Infer intent — do not force a template on reply bots.** The first WhatsApp node is a Meta template **only** when the bot **opens** the conversation. Use the table in [Infer starter vs reply](#infer-starter-vs-reply).
* Copy **every** canvas edge into execution `connections.out` (same node ids). Root `sourceHandle` is `"out"`; sub-node `sourceHandle` is `"in"`; every `targetHandle` is `"in"`; edge `type` is `"buttonedge"`. See [How to wire a graph](/build-workflows/canvas-vs-execution#how-to-wire-a-graph).
* On every graph POST/PUT send **`autoLayout: true`**. The server Clean-layouts roots (dagre LR) and snaps sub-nodes (`draggable: false`, `extent: "parent"`, relative x/y). The flag is not stored. `parentId` on buttons is still required. Graph save also fills `data.subNodesConfig` on message / if / switch nodes so the board dashed **+** is visible.
* **Fallback never edges back to its parent.** Mode A: auto-reply text + empty `connections.out`. Mode B: `connections.out` to a handoff / AI / pointer node. If `shouldSendReplyMessage` is true, runtime ignores fallback `connections.out`. See [Sub-nodes](/build-workflows/sub-nodes).
* Use [conversation tags](/api-reference/tags/get-all) on `updateConversation.selectedTags` (`{ id, title, color, associatedTo }`) when the scenario needs them (lead, trial, escalate). GET existing tags; create with [PUT `/conversation/tags`](/api-reference/tags/create) `{ title, color }` only if none match.
* Exactly **one** trigger node. `pingmeeTrigger` = listen for customer message events. `workflowTrigger` = sub-workflow, send-to-customer, or campaign / POST `/trigger`.
* A [workflow pointer](/build-workflows/sub-workflows) target must exist, be **active**, and start with `workflowTrigger`.
* Parameter slots are Fixed (`Variable`), Expression (`expression`), or Custom (`custom`). Check Custom first when reading JSON. See [Variables](/build-workflows/variables).
* Copy Variable `parentId` from the producer node. Do not mint a new parent on a downstream binding.
* Incoming run payload is `triggerData` on [POST /trigger](/api-reference/workflows/trigger). Read it with `return $.triggerData.;` — the `$…` id is the trigger **node** id, not the workflow id. See [Variables](/build-workflows/variables#triggerdata).
* After **copying a node into another workflow**, rewrite every `$nodeId` inside Expression bodies to ids that exist in the destination (usually the new trigger). Stale `$oldId.triggerData…` does not resolve. Update Fixed `parentId`s the same way.
* If/Else child titles must be `true` and `false` (case-insensitive).
* WhatsApp answers: max 10. **No public media upload.** Text / template body only unless `attachmentS3Id` is already known — never put blob upload URLs in templates. Skip IMAGE / VIDEO / DOCUMENT header templates via API.
* Before rewriting a live graph from analytics: [GET](/api-reference/workflows/get) first and read `analytics`. If the field is missing, the plan may not include canvas analytics — do not invent counts. See [Workflow analytics](/build-workflows/analytics).
* After a graph PUT the server **prunes** `nodeStats` / `edgeStats` / `buttonStats` for ids no longer on the graph. New or renamed node ids have no stats until new executions. GET again before the next rewrite.
### Infer starter vs reply
| User intent | Trigger | First WhatsApp node |
| - | - | - |
| Reply / keyword / inbound | `pingmeeTrigger` (`Messages Created`, `New Conversation Started`) | `messageType: "custom"` OK. Skip GET `/templates` unless the user named a template. |
| Starter / outbound / API trigger | `workflowTrigger` | `messageType: "template"` + `templateType: "whatsapp"`. Copy an APPROVED object from [GET templates](/api-reference/business/get-templates) into `templateInformation`. |
A pointer **child** with `workflowTrigger` that continues an inbound session may still use `custom`. Template is required for POST `/trigger`, campaign-style outbound, or any send outside the 24-hour window.
## Common failures
| Symptom | Cause |
| - | - |
| Buttons sit in the wrong place / are independently draggable | Missing `parentId` or you omitted `autoLayout: true` on the graph save |
| Fallback loops or re-enters the same message via an edge | Fallback `connections.out` pointed at the parent. Mode A has **no** edge (engine re-queues). Mode B must target a **different** node |
| Workflow opens empty / nodes jump | `data.nodes` missing, or ids do not match `parsedData` |
| Trigger never fires / run starts nowhere | Trigger left inside `parsedData`, or `trigger.connections.out[0]` missing |
| Board looks right, runtime skips a branch | Parent has a root `out` edge but also sub-nodes — engine uses the sub-node `connections.out` only |
| Button / true / false edge missing at runtime | Canvas used `sourceHandle: "out"` on a sub-node (must be `"in"`) |
| If always takes false | Child `title` is not exactly `true` / `false` |
| Switch always hits fallback | Conditions are on the parent `cases` only — runtime reads each `switchCaseEvaluation.data.conditions` |
| **409** on PUT | Graph fields sent without the current `lastModified` |
| Pointer does nothing | Child inactive, wrong `workflowId`, or child does not start with `workflowTrigger` |
| Parent never continues after a pointer | `pointer.connections.out` is empty (resume target missing) |
| Create “succeeds” but never runs | Forgot the activate PUT |
| Send fails / wrong number | Missing `associatedToBusinessId`, or it is the WABA id instead of the phone-number `id` |
| Meta rejects first outbound | Used `messageType: "custom"` on a **starter** (bot opens the thread) — need `template` + `templateType: "whatsapp"` and an APPROVED catalog template. Reply bots (`pingmeeTrigger`) may use `custom`. |
| Template media broken | Blob URL in `templateInformation` — there is no public upload API; use a known `attachmentS3Id` or skip media |
| Template send missing parameters | `{{n}}` in BODY / HEADER without matching `bodyVariables` / `headerVariables` keys |
| HTTP / Task placeholder empty | `{{n}}` in the string but no matching key in `bodyVariables` / `titleVariables` |
| `triggerData` expression is empty | Used the workflow id instead of the trigger node id, skipped the trigger with `startWithNodeId`, or the POST body key does not match |
| Analytics missing on GET | Plan is not Expert AI — `analytics` is stripped. Do not invent counts. |
| Stats do not match the canvas after PUT | Graph PUT pruned removed ids; renamed nodes start at zero until new runs. GET again. |
| Expression empty after copy / paste | Copied `$oldNodeId.…` into a workflow whose trigger/node ids are different — rewrite `$` ids (and Fixed `parentId`s) |
| Condition never matches | Invented Variable `parentId`, or `comparison` not one of the `ConditionType` strings (`"Equals"`, `"Is Empty"`, …) |
## Do not send
* `analytics`, `executionCount`, identifying `notes`
* Deprecated `assignTags` / `assignAgents` (use `updateConversation`)
* Sticky notes in `parsedData` (optional in `data` only)
* Real customer names, phones, emails, CRM table names, agent ids, or other bots' `workflowId`s in examples you commit
# Sub-nodes
Source: https://docs.pingmee.co.il/build-workflows/sub-nodes
Answers, true/false, switch cases, fallback, and timeout
Sub-nodes are siblings on the canvas (`parentId`) and nested in execution (`parent.subNodes[]`). The parent’s main `out` handle is unused once sub-nodes exist — the engine follows the **chosen sub-node** `connections.out`.
Canvas edges from a sub-node use `sourceHandle: "in"` (NestedSubNode quirk).
## If / Else
Parent `if` with exactly two `conditionEvaluation` children. The engine picks by `title.toLowerCase()`:
| Child `title` | When |
| - | - |
| `true` | All parent `data.conditions` pass (runtime ANDs in order) |
| `false` | Otherwise |
```json theme={null}
{
"id": "if_1",
"type": "if",
"data": {
"evaluationMode": "ALL",
"conditions": [
{
"id": "c1",
"firstValue": { "id": "var_cust_name", "name": "customer.customerName", "parentId": "trig_1", "valueType": "string" },
"comparison": "Is Empty"
}
]
},
"connections": {},
"subNodes": [
{
"id": "ev_true",
"type": "conditionEvaluation",
"parentId": "if_1",
"title": "true",
"data": { "label": "true", "editable": false, "index": 0 },
"connections": { "out": ["http_1"] }
},
{
"id": "ev_false",
"type": "conditionEvaluation",
"parentId": "if_1",
"title": "false",
"data": { "label": "false", "editable": false, "index": 1 },
"connections": { "out": ["wa_menu"] }
}
]
}
```
## Switch
Children: `switchCaseEvaluation` (max 25) plus optional `noSelectionFallback`. **Conditions live on each case child** (`data.conditions`, `data.evaluationMode` `ALL` | `ANY`). First matching case wins; otherwise fallback `connections.out`.
```json theme={null}
{
"id": "sw_1",
"type": "switch",
"connections": {},
"subNodes": [
{
"id": "case_eu",
"type": "switchCaseEvaluation",
"parentId": "sw_1",
"title": "Europe",
"data": {
"label": "Europe",
"evaluationMode": "ALL",
"conditions": [
{
"id": "c_eu",
"firstValue": { "id": "var_region", "name": "customer.countryCode", "parentId": "trig_1", "valueType": "string" },
"comparison": "Equals",
"secondValue": "EU"
}
]
},
"connections": { "out": ["ptr_europe"] }
},
{
"id": "sw_fb",
"type": "noSelectionFallback",
"parentId": "sw_1",
"title": "Fallback",
"data": { "label": "Fallback" },
"connections": { "out": ["wa_default"] }
}
]
}
```
## Message answers, fallback, timeout
When `waitForUserResponse: true` on `whatsapp` / `facebookMessenger` / `instagram` / `webchat`:
| Sub-node | Limit / notes |
| - | - |
| `answer` | WhatsApp max **10**, Messenger/Instagram **13**, webchat **8**. `label` is the quick-reply text. Optional `buttonPayload`, `carouselCardIndex`. |
| `noSelectionFallback` | Unknown reply (typed text instead of a button). **Never** draw an edge from fallback back to its parent. Pick **one** of the two modes below. |
| `awaitResponseTimeout` | `shouldTimeoutExecution: true` plus `timerInfo`. Handle is hidden unless timeout is enabled. |
```json theme={null}
{
"id": "wa_menu",
"type": "whatsapp",
"data": { "platform": "whatsapp", "waitForUserResponse": true },
"connections": {},
"subNodes": [
{
"id": "ans_tours",
"type": "answer",
"parentId": "wa_menu",
"title": "Tours",
"data": { "label": "Tours", "index": 0, "editable": true, "removable": true },
"connections": { "out": ["ptr_tours"] }
},
{
"id": "ans_help",
"type": "answer",
"parentId": "wa_menu",
"title": "Help",
"data": { "label": "Help", "index": 1, "editable": true, "removable": true },
"connections": { "out": ["ptr_support"] }
},
{
"id": "fb_unknown",
"type": "noSelectionFallback",
"parentId": "wa_menu",
"title": "Fallback",
"data": {
"label": "Fallback",
"shouldSendReplyMessage": true,
"unknownAnswerReplyMessage": "Please select from the options."
},
"connections": {}
},
{
"id": "to_idle",
"type": "awaitResponseTimeout",
"parentId": "wa_menu",
"title": "Timeout",
"data": {
"label": "Timeout",
"shouldTimeoutExecution": true,
"timerInfo": { "hours": "0", "minutes": "30", "seconds": "0", "numberOfSeconds": 1800 }
},
"connections": { "out": ["upd_conv"] }
}
]
}
```
Matching canvas edges for the first answer (not for fallback mode A):
```json theme={null}
{
"source": "ans_tours",
"target": "ptr_tours",
"sourceHandle": "in",
"targetHandle": "in",
"type": "buttonedge"
}
```
### Fallback modes (never parent)
**Never** put the parent message id on fallback `connections.out` or draw `fallback → parent`.
| Mode | `shouldSendReplyMessage` | `connections.out` | Runtime |
| - | - | - | - |
| **A — prompt again** | `true` + `unknownAnswerReplyMessage` | `{}` / omit, **no** canvas edge | Sends that text, then **re-queues the same message node** (engine does this; you do not draw the loop) |
| **B — branch away** | `false` or omit | Target a **different** node: human (`updateConversation` `answerMode: "manual"`), AI (`"ai"`), extra explanation, or [workflow pointer](/build-workflows/sub-workflows) | Follows that edge. If both A flags and `connections.out` are set, **A wins** and `connections.out` is ignored |
Sub-node canvas fields: `draggable: false`, `extent: "parent"`, `position: { "x": 138, "y": 120 + 35 * index }` relative to the parent. Answers first, then fallback, then timeout. Prefer sending `autoLayout: true` on save so the server writes these fields.
```json theme={null}
{
"id": "ans_tours",
"type": "answer",
"parentId": "wa_menu",
"draggable": false,
"extent": "parent",
"position": { "x": 138, "y": 120 }
}
```
Workflow-level `settings.sharedTimeout` / `settings.sharedFallback` can stand in when a message node has no local timeout/fallback. See [Sub-workflows](/build-workflows/sub-workflows).
# Sub-workflows
Source: https://docs.pingmee.co.il/build-workflows/sub-workflows
Workflow pointer, workflow trigger, resume, and shared timeout/fallback
A pointer **hands off** to another workflow. It does not walk `connections.out` in the same execution.
## Rules
1. The child document **must** start with a `workflowTrigger` (in `data.nodes` and `workflow.trigger`).
2. The child **must** be `isActive: true`. If it is missing or inactive, the pointer **no-ops**: the parent does not continue along `connections.out`, and no child run is enqueued.
3. After the child finishes, the parent resumes at `pointer.connections.out[0]` (`continueFromNodeId`). If that array is empty, the parent stops after the child.
Activate the child **before** you trigger the parent.
## Pointer node
```json theme={null}
{
"id": "ptr_support",
"type": "workflowPointer",
"category": "Operation",
"title": "workflowPointer",
"data": {
"workflowId": "wf_child_support",
"workflowName": "Child support",
"workflowFolderId": "folder_main",
"isActive": true
},
"connections": { "out": ["upd_conv"] },
"subNodes": []
}
```
Look up `workflowId` / `folderId` from [list workflows (minimal)](/api-reference/workflows/get-minimal). Use placeholders like `"wf_child_support"` only in docs — production payloads need real ids from that list or from create.
## Child trigger
```json theme={null}
{
"id": "trig_child",
"type": "workflowTrigger",
"category": "Trigger",
"title": "workflowTrigger",
"data": { "isActive": true, "variables": {} },
"connections": { "out": ["wa_hello"] }
}
```
`triggerType` on children is often empty. Runtime still starts at `trigger.connections.out[0]`.
A pointer does **not** copy `triggerData` into the child. To pass a payload into a `workflowTrigger` graph, [POST `/trigger`](/api-reference/workflows/trigger) on **that** workflow — see [`triggerData`](/build-workflows/variables#triggerdata).
## Resume sequence
```mermaid theme={null}
sequenceDiagram
participant Parent
participant Pointer
participant Child
Parent->>Pointer: execute workflowPointer
Note over Pointer: nextNodeIds is always empty
Pointer->>Child: enqueue child (Message Created)
Child->>Child: run from workflowTrigger
Child->>Parent: resume at pointer.connections.out[0]
```
Create and activate the child with the same [save checklist](/build-workflows/overview) as any other workflow.
## Shared timeout and fallback
`workflow.settings` can route silence / unknown replies to another workflow (seen on real inbound bots):
```json theme={null}
{
"settings": {
"sharedTimeout": {
"enabled": true,
"workflowId": "wf_child_support",
"timerInfo": {
"hours": "3",
"minutes": "0",
"seconds": "0",
"numberOfSeconds": 10800
}
},
"sharedFallback": {
"enabled": true,
"workflowId": "wf_child_support",
"fallbackData": {
"shouldSendReplyMessage": true,
"unknownAnswerReplyMessage": "Please choose one of the options."
}
}
}
}
```
A node-level `awaitResponseTimeout` / `noSelectionFallback` that is fully configured wins over the shared setting. Shared timeout/fallback also require the target workflow to be active and start with `workflowTrigger`.
# Variables and parameters
Source: https://docs.pingmee.co.il/build-workflows/variables
Fixed, Expression, and Custom bindings; output catalog, template slots, HTTP/Task placeholders, conditions, and triggerData
Every parameter slot (WhatsApp template `{{n}}`, HTTP/Task placeholders, Fireberry fields, conditions) is **one of three JSON shapes**. The board picker has the same three tabs: **Fixed**, **Expression**, **Custom**.
Runtime discriminators are `isCustomValue`, `isExpression`, and `isVariable` in `@pingmee/aws-sharedmodel` (`automations.ts`). Check **Custom first**: a Custom object also carries `id` / `name` / `parentId` / `valueType`, so `isVariable` would match it.
`workflow.variables` is a top-level map and is usually `{}`.
## Three ways to bind a parameter
### Fixed — field from another node (`isVariable`)
The value is an input of an upstream node. In the GUI, expand a node row (label plus a `#` badge of the last four characters of the node id, for example Workflow Trigger `#a5a8`) and pick a leaf from the tree.
JSON is a `Variable`. `parentId` is the **upstream node id** that produced the value. `name` is the dotted path into that node's `output`. Downstream nodes **copy** that object. They do not invent a new parent.
```json theme={null}
{
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
}
```
### Expression — JavaScript over node outputs (`isExpression`)
The value is computed JS. Discriminator is `"expression"`. Runtime wraps the string in a function, so the body **must** `return`. Node outputs are referenced with `$` plus the **full node id**:
```
return $35ba498caa5a8.triggerData.websiteLink;
```
In the GUI, type `$` to autocomplete available parameters. Typing more characters filters the list. Suggestion labels show the last four characters of the node id; the inserted text is the full `$nodeId.path`.
`parentId` / `name` on the Expression object are unused at runtime (resolution reads `$ids` in the body). The GUI often leaves them empty. Filling them in for readability is fine.
If you copy a node into a **different workflow**, every `$nodeId` in Expression bodies **must** be rewritten to ids that exist in the destination (usually the new trigger node's id). Stale `$oldId.triggerData…` does not resolve.
[`triggerData`](#triggerdata) is the usual Expression example: keys from [POST `/trigger`](/api-reference/workflows/trigger) are not in the board catalog, so you cannot pick them as Fixed.
### Custom — static text (`isCustomValue`)
A literal string. Discriminator is `"custom"`. The GUI is a free-text field (for example `www.pingmee.co.il`). Runtime returns `custom` as-is.
```json theme={null}
{
"id": "cust_site",
"name": "",
"parentId": "",
"valueType": "string",
"custom": "www.pingmee.co.il"
}
```
## Compact examples
Same three objects work in a WhatsApp `bodyVariables` slot, an HTTP `bodyVariables` / `titleVariables` slot, and a Fireberry `objectVariable` or `queries[].value`.
**Fixed** — catalog field from the trigger:
```json theme={null}
{
"bodyVariables": {
"1": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
}
}
}
```
**Expression** — `triggerData` key (HTTP `body` / Fireberry field: same object):
```json theme={null}
{
"bodyVariables": {
"1": {
"id": "expr_link",
"name": "",
"parentId": "",
"valueType": "string",
"expression": "return $trig_1.triggerData.websiteLink;"
}
}
}
```
**Custom** — literal (Fireberry `objectVariable` shown; a WhatsApp slot is the same object under `"1"`):
```json theme={null}
{
"objectVariable": {
"id": "cust_site",
"name": "",
"parentId": "",
"valueType": "string",
"custom": "www.pingmee.co.il"
}
}
```
| Mode | Discriminator | What runtime uses |
| - | - | - |
| Fixed | `id`, `name`, `parentId`, `valueType` — no `expression`, no `custom` | `input[parentId].output` then `name` as a dotted path |
| Expression | `"expression": "return $nodeId.path;"` | `$nodeId` inside the body (not the object's `parentId`) |
| Custom | `"custom": "…"` | The `custom` string |
## Output catalog — `node.data.variables`
Triggers ship a tree generated from empty `customer` / `message` / `trigger` objects. Nested objects use `valueType: "Object"` and a nested `variables` map. That tree is what Fixed picks from.
Typical trigger names (not an exhaustive dump):
* `customer.customerName`, `customer.phoneNumber`, `customer.email`, `customer.customerNickname`
* `message` (object) plus nested message fields
* `trigger.platform`, `trigger.eventType`
WhatsApp (and other message) nodes add a self output `{ name: "#", parentId: }` and one entry per answer button (`name` = button label, `parentId` = message node).
AI `summarizeConversation` exposes `{ name: "summary", parentId: , valueType: "string" }`.
```json theme={null}
{
"variables": {
"var_cust": {
"id": "var_cust",
"name": "customer",
"parentId": "trig_1",
"valueType": "Object",
"variables": {
"var_cust_name": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
}
}
}
}
}
```
## Template slots
On message nodes, numbered maps bind WhatsApp template placeholders:
| Map | Keys |
| - | - |
| `bodyVariables` | `1`, `2`, … body `{{n}}` |
| `headerVariables` | header placeholders |
| `buttonVariables` | dynamic URL / action suffixes by button index |
| `carouselVariables` | `` `${cardIndex}-body-${i}` `` or `` `${cardIndex}-${buttonIndex}` `` |
Each value is Fixed, Expression, or Custom.
## HTTP and Task `{{n}}`
`httpRequest.data.body` and `task.task.title` / `task.task.body` use the same `{{1}}` syntax. Bindings live in `bodyVariables` and `titleVariables` (numeric keys). Each value is the same three shapes.
```json theme={null}
{
"body": "{\"name\":\"{{1}}\"}",
"bodyVariables": {
"1": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
}
}
}
```
```json theme={null}
{
"task": {
"title": "Follow up {{1}}",
"body": "Lead asked about {{1}}",
"statusCase": "open"
},
"titleVariables": {
"1": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
}
}
}
```
## Conditions
If / Switch case `firstValue` is typically Fixed. `secondValue` is often a raw string, not a parameter object.
```json theme={null}
{
"id": "c1",
"firstValue": {
"id": "var_cust_name",
"name": "customer.customerName",
"parentId": "trig_1",
"valueType": "string"
},
"comparison": "Equals",
"secondValue": "VIP"
}
```
`Is Empty` / `Is Not Empty` omit a useful `secondValue`. `Contains` uses `[{ "label": "hello" }]`.
## triggerData
`triggerData` is the JSON object you send when you start a run. The engine copies it onto the **trigger node's output**. Downstream fields (WhatsApp template slots, HTTP/Task `{{n}}`, conditions) read it with an **Expression**:
```
return $.triggerData.;
```
That `$…` id is the trigger **node's** `id`, not the workflow id. Real node ids are often hex strings:
```
return $35ba498caa5a8.triggerData.websiteLink;
```
### How it maps
1. [POST `/workflows/{workflowId}/trigger`](/api-reference/workflows/trigger) with `"triggerData": { "websiteLink": "https://example.com" }`.
2. The run starts at the workflow's trigger (`pingmeeTrigger`, `workflowTrigger`, `instagramTrigger`, or `facebookTrigger`).
3. That node's execution output becomes `{ message, customer, triggerData }`.
4. A WhatsApp or HTTP binding with `expression: "return $trig_1.triggerData.websiteLink;"` resolves to `"https://example.com"`.
Keys are **not** declared on the trigger node. They are whatever you put on the incoming object. Nested objects work with more dots (`$trig_1.triggerData.order.id`). The board catalog (`node.data.variables`) is generated from empty `customer` / `message` / `trigger` shapes and does **not** include `triggerData` — that is why you use Expression instead of Fixed.
You can also bind a Fixed Variable whose `name` is `triggerData.websiteLink` and `parentId` is the trigger node. Same output path; no `expression` field. Prefer the Expression form when you author graphs by API.
### vs catalog Variables and `bodyVariables`
| Kind | Catalog Fixed | `triggerData` Expression | Custom |
| - | - | - | - |
| Source | Trigger output fields the board already knows (`customer.customerName`, `message`, …) | Keys on the POST body | Literal string you typed |
| Binding | `{ "name": "customer.customerName", "parentId": "trig_1" }` | Same Variable shape **plus** `"expression": "return $trig_1.triggerData.websiteLink;"` | `{ "custom": "www.pingmee.co.il" }` |
| Template / HTTP / Fireberry slot | Put that object in the slot | Same | Same |
`bodyVariables` is only the slot map (`"1"`, `"2"`, …). It does not invent payload keys.
### Constraints
* JSON **object** (not an array or string). Omit the field if you have no payload.
* At most 32 768 bytes serialized, nesting depth at most 10.
* The trigger node must actually run. `startWithNodeId` that skips the trigger does **not** seed `output.triggerData`.
* A [workflow pointer](/build-workflows/sub-workflows) does **not** forward `triggerData` into the child. POST `/trigger` on the workflow you want the payload on (often a `workflowTrigger` child).
* Copying the Expression into another workflow requires a new `$nodeId`. See [Hard rules](/build-workflows/rules).
See a full dual-graph snippet in [Examples](/build-workflows/examples#workflow-trigger--triggerdata).
# Introduction
Source: https://docs.pingmee.co.il/index
Working with Pingmee Micro Services
JSON cookbook: dual graph, nodes, variables, sub-nodes, pointers. Author and save via this site’s MCP at [docs.pingmee.co.il/mcp](https://docs.pingmee.co.il/mcp).
Optional. Node catalog and board screens live on [Workflow Docs](https://docs-workflows.pingmee.co.il/docs). Not required to assemble or persist a workflow.
When authenticating a request, you should send a Cookie header instead of an Authorization header.
## Authentication & Headers
Our platform is built using multiple **microservices**.
Each request **must include the following two headers**:
* `Cookie` – Your personal access token
* `x-api-key` – Your business API key
Both values are unique to your business and are available in the Pingmee app.
### Where to find your token and API key
1. Open the Pingmee app
2. Go to **Settings** (gear icon in the sidebar)
3. Select **Developer Tools** (`כלים למפתחים`)
On this page you will find:
* **Personal token** – use this value in the `Cookie` header. You can regenerate it at any time.
* **API key** – use this value in the `x-api-key` header on every API call.
Keep your personal token and API key private. Do not share them or commit them to version control.
### Required Headers
```http theme={null}
Cookie:
x-api-key:
```
Requests missing either of these headers will be rejected.
***
### Rate Limits
Rate limits depend on your business subscription plan. Limits apply across all Pingmee API services combined.
| Plan | Rate (req/s) | Burst | Daily quota |
| - | - | - | - |
| Trial | 20 | 40 | 10,000 |
| Basic | 40 | 80 | 60,000 |
| Expanded | 100 | 200 | 125,000 |
| ExpertAI | 200 | 400 | 200,000 |
| Partner | 500 | 1,000 | Custom |
***
## Public Services
###
* **Core:** `https://api.pingmee.co.il`
* **AI:** `https://ai.pingmee.co.il`
* **Business:** `https://business.pingmee.co.il`
* **Campaigns:** `https://campaigns.pingmee.co.il`
* **Workflows:** `https://workflows.pingmee.co.il`
***
## Example Headers Usage
```json theme={null}
{
"headers": {
"Cookie": "",
"x-api-key": ""
}
}
```
# Introduction
Source: https://docs.pingmee.co.il/make
Use Pingmee in Make.com and automate your workflows
## Getting Started
We have created a Make.com module to help you integrate Pingmee with your automations. This module provides a seamless way to connect Pingmee's messaging and automation features directly within your Make.com scenarios.
To get started, you'll need to install the Pingmee module in Make.com. Click the link below to access the module invitation.
## View the Module
[Pingmee Module](https://www.make.com/en/integrations/pingmee)
Once installed, you can add the Pingmee module to your Make.com scenarios and start automating your messaging workflows.
***
## What You Can Do
With the Pingmee Make.com module, you can:
* **Send Messages** - Automatically send messages to customers
* **Manage Conversations** - Handle and respond to customer conversations
* **Create Customers** - Add new customers to your Pingmee account
* **Work with Templates** - Use message templates in your automations
* **Manage Tags** - Organize and categorize your contacts
***
## Authentication
The Pingmee module handles authentication automatically. When you first add the module to a scenario, you'll be prompted to:
1. Connect your Pingmee account
2. Authorize the connection
3. Start building your automation
Make sure you have an active Pingmee account before connecting the module.
***
## Next Steps
After installing the module:
1. Create a new scenario in Make.com
2. Add the Pingmee module as a trigger or action
3. Configure your automation workflow
4. Test and activate your scenario
For detailed API documentation and available endpoints, refer to the [API Reference](/api-reference) section.
# Introduction
Source: https://docs.pingmee.co.il/n8n
Use Pingmee in n8n and automate your workflows
## Getting Started
We have created an n8n node to help you integrate Pingmee with your automations. This node provides a seamless way to connect Pingmee's messaging and automation features directly within your n8n workflows.
This node can only be installed when n8n is self-hosted. Community nodes are not available on n8n Cloud.
## Install the Node
To install the Pingmee node in your self-hosted n8n instance:
1. Go to **Settings** → **Community nodes**
2. Click **Install**
3. Enter the package name: `n8n-nodes-pingmee`
4. Click **Install**
Alternatively, you can find the package on npm:
[n8n-nodes-pingmee on npm](https://www.npmjs.com/package/n8n-nodes-pingmee)
[n8n-nodes-pingmee on ncnodes](https://ncnodes.com/package/n8n-nodes-pingmee)
Once installed, you can add the Pingmee node to your n8n workflows and start automating your messaging workflows.
***
## What You Can Do
With the Pingmee n8n node, you can:
* **Subscribe to message/conversation webhooks** - Receive important events
* **Send Messages** - Automatically send messages to customers
* **Work with Templates** - Use message templates in your automations
***
## Authentication
To authenticate the Pingmee node, you need to create a new credential in n8n:
1. Open the Pingmee app and go to **Settings** → **Developer Tools** (`כלים למפתחים`)
2. Copy your **personal token** and **API key**
3. In n8n, create a new credential for the Pingmee node
4. Paste the values into the credential configuration
Each business has its own personal token and API key. API usage is logged per token. Make sure you have an active Pingmee account before setting up credentials.
For more details, see [Authentication & Headers](/#authentication--headers).
***
## Next Steps
After installing the node:
1. Create a new credential in n8n and add your Pingmee personal token and API key (found in Pingmee **Settings** → **Developer Tools**)
2. Create a new workflow in n8n
3. Add the Pingmee node as a trigger or action
4. Select the credential you created
5. Configure your automation workflow
6. Test and activate your workflow
For detailed API documentation and available endpoints, refer to the [API Reference](/) section.
# Workflow node documentation
Source: https://docs.pingmee.co.il/workflows-documentation
Where to author workflows via MCP, and when the board UI catalog is optional
To **author and save** a workflow (nodes, edges, variables, sub-nodes, pointers), use **this** site:
| Resource | URL |
| - | - |
| Build workflows cookbook | [Build a workflow](/build-workflows/overview) |
| Workflows REST | [List](/api-reference/workflows/get-minimal), [get](/api-reference/workflows/get), [create](/api-reference/workflows/create), [update](/api-reference/workflows/update), [create folder](/api-reference/workflows/create-folder), [trigger](/api-reference/workflows/trigger), [reset analytics](/api-reference/workflows/analytics-reset) |
| MCP | [docs.pingmee.co.il/mcp](https://docs.pingmee.co.il/mcp) |
That MCP is sufficient. Register one server:
```json theme={null}
{
"mcpServers": {
"pingmee-api": {
"url": "https://docs.pingmee.co.il/mcp"
}
}
}
```
| Question type | Use |
| - | - |
| Create / GET / PUT a workflow, dual-graph JSON, variables, sub-nodes, pointers | `pingmee-api` (this site) |
| Bind a WhatsApp phone; fetch templates only for starter/outbound (or a named template) | [GET phone numbers](/api-reference/business/get-phone-numbers), [GET templates](/api-reference/business/get-templates) — [infer starter vs reply](/build-workflows/overview#infer-starter-vs-reply) |
| Analyze canvas analytics then improve the graph | GET `analytics` on the workflow, then PUT — [Workflow analytics](/build-workflows/analytics) |
| Board UI labels, form screenshots, Hebrew node guides | Optional Workflow Docs (below) |
## Optional — Workflow Docs (board UI)
[docs-workflows.pingmee.co.il](https://docs-workflows.pingmee.co.il/docs) is a human UI catalog (drawer, forms, screenshots). You do **not** need its MCP to build or save a graph.
| Resource | URL |
| - | - |
| Documentation | [docs-workflows.pingmee.co.il/docs](https://docs-workflows.pingmee.co.il/docs) |
| LLM index (`llms.txt`) | [docs-workflows.pingmee.co.il/llms.txt](https://docs-workflows.pingmee.co.il/llms.txt) |
| Full corpus (`llms-full.txt`) | [docs-workflows.pingmee.co.il/llms-full.txt](https://docs-workflows.pingmee.co.il/llms-full.txt) |
| MCP (optional) | [docs-workflows.pingmee.co.il/mcp](https://docs-workflows.pingmee.co.il/mcp) |
What's on Workflow Docs: trigger/action/condition form copy, nodes drawer, board toolbar, sticky notes. English and Hebrew (`/docs` and `/he/docs`).
If you add the optional MCP:
```json theme={null}
{
"mcpServers": {
"pingmee-api": {
"url": "https://docs.pingmee.co.il/mcp"
},
"pingmee-workflows": {
"url": "https://docs-workflows.pingmee.co.il/mcp"
}
}
}
```
Workflow Docs tools: `search_workflows`, `query_docs_filesystem_workflows`, `list_pages`.