---
title: "Create workflow"
method: POST
path: "/v1/workflows"
tags: ["Workflows"]
---

# Create workflow

`POST /v1/workflows`

Create a branching conversation workflow (draft) from a node/edge graph. Created in `draft` status; activate it to start matching inbound messages. The graph is validated structurally; completeness (a trigger node + reachable entry) is required at activation.

## Request body

- object
  - `profileId` string, required
  - `accountId` string, required
  - `platform` 'whatsapp' | 'instagram' | 'facebook' | 'telegram' | 'twitter' | 'bluesky' | 'reddit'
  - `name` string, required
  - `description` string
  - `nodes` WorkflowNode[]
    - `id` string, required — Stable node id referenced by edges
    - `type` 'trigger' | 'send_message' | 'wait_for_reply' | 'condition' | 'set_variable' | 'delay' | 'webhook' | 'ai' | 'handoff' | 'start_call' | 'a_b_split' | 'set_field' | 'enroll_sequence' | 'add_tag' | 'remove_tag' | 'end', required — Node kind. The 16 supported types break into four groups: messaging (send_message), control flow (trigger, condition, delay, wait_for_reply, a_b_split, end), data ops (set_variable, set_field, add_tag, remove_tag, enroll_sequence), integrations (webhook, ai, handoff, start_call).
    - `config` object — Type-specific settings. All string fields support `{{variable}}` interpolation against the run's variable bag (resolved at execution time). **trigger**: `{ triggerType: inbound_message|api_call|whatsapp_event, keywords:[string], matchType: any|contains|exact|regex, onlyFirstMessage:boolean, eventType: message_sent|message_delivered|message_read|message_failed|reaction }`. Default `triggerType` is `inbound_message` for legacy nodes. `eventType` is only honored when `triggerType` is `whatsapp_event` (WhatsApp-only). **send_message**: `{ messageType: text|template|media|interactive, text, template:{name,language,variableMapping}, media:{mediaType:image|video|audio|document, url,caption}, interactive }`. `template` and `interactive` are WhatsApp-only. `interactive.type` is inferred from the payload shape when omitted; payloads with neither `type` nor an inferable shape are rejected. **wait_for_reply**: `{ timeoutMinutes:int (max 43200), saveAs:string }`. Resume via the `'reply'` edge on inbound, or `'timeout'` edge after `timeoutMinutes` of silence. **condition**: `{ rules:[{ id, variable, operator: equals|not_equals|contains|not_contains|starts_with|ends_with|exists|not_exists|matches, value }] }`. First matching rule takes its `id` as the sourceHandle; otherwise `'default'`. **set_variable**: `{ assignments:[{ name, value }] }`. Run-scoped (lives only for this execution; use `set_field` for persistent values). **delay**: `{ delayMinutes:int (max 43200) }`. Suspends the run, resumes via timer. **webhook**: `{ url, method: GET|POST|PUT|PATCH|DELETE, headers, bodyTemplate, saveAs }`. SSRF-guarded (private/loopback/metadata IPs rejected). Response saved as `{ status, ok, body }` to `vars[saveAs]`. Edge: `'success'` on 2xx, `'error'` otherwise. **ai**: `{ provider: anthropic|openai|google|mistral|groq|openrouter, model, preset: smart|tools|cheap, systemPrompt, userPromptTemplate, saveAs, temperature, maxTokens, outputType: text|json, tools:[{ name, description, parameters }] }`. Set `provider` + `model` for BYOK (uses your stored API key); omit `provider` for the legacy Telnyx path. Edges: `'success'`, `'tool:<name>'` (model picked a tool), `'error'`. **handoff**: `{ note, assignTo }`. Terminates the run as `exited`, flags the conversation for a human operator. **start_call**: `{ to, forwardTo, requirePermissionFirst, recordingEnabled, saveAs }`. WhatsApp-only. `forwardTo` can be `tel:+E164`, `sip:user@host`, or `wss://…` (AI voice agent). Edges: `'success'`, `'permission_required'`, `'failed'`. **a_b_split**: `{ percentage: number 0-100 (default 50) }`. Random branch picker. Edges: `'a'` (with probability `percentage/100`), `'b'`. **set_field**: `{ field, value }`. Persistent custom field on the Contact (vs `set_variable` which is run-scoped). Field name is sanitized to `[A-Za-z0-9_]`. No-op on `api_call` runs (no contact). **enroll_sequence**: `{ sequenceId, saveAs }`. Enrolls the run's contact into a Sequence. Edges: `'success'`, `'error'`. **add_tag** / **remove_tag**: `{ tag }`. Push or pull a tag on the Contact. No-op on `api_call` runs. **end**: no config. Terminates the run as `completed`.
    - `position` object — Canvas coordinates (ignored by the executor; used by the visual builder).
      - `x` number
      - `y` number
    - `label` string — Optional display name shown on the builder canvas and inspector, falling back to the node type when absent. The nodes array is replaced wholesale on update, so it must be resent to be kept.
  - `edges` WorkflowEdge[]
    - `id` string, required
    - `source` string, required — Source node id
    - `target` string, required — Target node id
    - `sourceHandle` string, nullable — Selects a branch output of a multi-output node. Null (or omitted) = the node's single/default output. Known handles per node type: - **condition** — a rule's `id`, or `'default'` (no rule matched) - **wait_for_reply** — `'reply'` (contact replied) | `'timeout'` (no reply in window) - **webhook** — `'success'` (2xx) | `'error'` (non-2xx / fetch failed) - **ai** — `'success'` (text/JSON response) | `'tool:<toolName>'` (model invoked that tool) | `'error'` (upstream failure / non-JSON in JSON mode) - **start_call** — `'success'` | `'permission_required'` | `'failed'` - **a_b_split** — `'a'` | `'b'` - **enroll_sequence** — `'success'` | `'error'`
  - `entryNodeId` string — The trigger node id; derived from the single trigger node if omitted

## Response `200`

Workflow created

- object
  - `success` boolean
  - `workflow` object
    - `id` string
    - `name` string
    - `description` string
    - `platform` string
    - `status` string
    - `nodeCount` integer
    - `entryNodeId` string
    - `createdAt` string, date-time

## Other responses

- `400` — Invalid graph (duplicate node ids, edges referencing missing nodes, or a WhatsApp-only node on another platform)
- `401` — Unauthorized

---

[API](https://skmtc.net/zernio/apis/zernio-api.md) · [All operations](https://skmtc.net/zernio/apis/zernio-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/zernio/zernio-api/revisions/0942091eaa8c/schema)
