# Build with the API (/docs/workflows/build-with-the-api)

> Create and run workflows over the REST API, the same workflows the dashboard builds. The rules an integration or an AI agent needs to get it right.

Everything the visual builder does, the API does too. You build a workflow as
a graph of nodes, join them with edges, test it, then publish it. The dashboard
and the API read and write the same workflow, so a graph the API creates opens
in the canvas, and a workflow a person drew can be edited by the API.

  Every call takes `Authorization: Bearer <YOUR_API_KEY>`. The key scopes every
  request to your own workspace: list and read return only your workflows, and a
  workflow in another workspace answers `404`, never another account's data.

**Base URL**  &nbsp;·&nbsp; All routes live under `/api/v1/workflows`
and `/api/v1/workflow-node-types`.

## The tools, in the order you use them [#the-tools-in-the-order-you-use-them]

1. [`listWorkflowNodeTypes`](/docs/api-reference/workflows/listWorkflowNodeTypes) with `q` (for example `whatsapp`) to find node types.
2. [`getWorkflowNodeType`](/docs/api-reference/workflows/getWorkflowNodeType) for the fields one type needs. Ask for `detail=full` only when you want the examples.
3. [`listWorkflowFieldOptions`](/docs/api-reference/workflows/listWorkflowFieldOptions) for any field of type `ref` (a number, an integration, a template, an agent). Never guess an id. If it returns `no_options`, ask the user for the value.
4. [`createWorkflow`](/docs/api-reference/workflows/createWorkflow) with a `name`. It starts with one `trigger` node. You may pass a complete first `graph` instead, in the shape [`getWorkflow`](/docs/api-reference/workflows/getWorkflow) returns under `draft`.
5. [`editWorkflow`](/docs/api-reference/workflows/editWorkflow) with the `revision` from your last read and a list of `ops`.
6. [`validateWorkflow`](/docs/api-reference/workflows/validateWorkflow), then fix what it reports with another `editWorkflow`.
7. [`testWorkflow`](/docs/api-reference/workflows/testWorkflow) only after the user agrees (see [Test runs are real](#test-runs-are-real)).
8. [`publishWorkflow`](/docs/api-reference/workflows/publishWorkflow), then [`activateWorkflow`](/docs/api-reference/workflows/activateWorkflow) or [`deactivateWorkflow`](/docs/api-reference/workflows/deactivateWorkflow).

## Draft and published [#draft-and-published]

- You always edit the **draft**. A published workflow is never changed in place.
- Real events and webhooks always run the **published** version. A test runs the draft.
- `publishWorkflow` copies the draft to the live version and leaves a fresh draft for the next round of edits. It refuses while `validateWorkflow` has errors.
- `deactivateWorkflow` pauses the live version. `activateWorkflow` resumes it.

## Connecting nodes [#connecting-nodes]

An edge runs from one node's **output** to the next node. Plain nodes have one
output, `default`. Branching nodes have named outputs, and you must say which.
`getWorkflowNodeType` lists each type's outputs. A summary:

| Node | Outputs |
|---|---|
| `conditional` | `True`, `False` |
| `switch_condition` | each case `id`, plus `else` |
| `time_condition` | `Yes`, `No` |
| `photo_analyzer` | `pass`, `fail` |
| `voice_ai` | `Completed`, `Not Completed` |
| `zoho_bigin` | `Created`, `Found`, `Ambiguous`, `Failed`, `Success` |

- One output can feed several nodes. They all run.
- `add_node` with `after` and `handle` places and connects a node in one step.
- Every node except `notes` must be reachable from the trigger, or publish is refused.

### Ops [#ops]

| Op | Fields |
|---|---|
| `add_node` | `id`, `type`, `config?`, `label?`, `position?`, `after?`, `handle?` |
| `update_node` | `id`, `config` (merged; `null` removes a key), `label?` |
| `move_node` | `id`, `position {x, y}` |
| `delete_node` | `id` (its edges go too) |
| `connect` | `from`, `to`, `handle?` (default `default`) |
| `disconnect` | `from`, `to`, `handle?` |

A batch applies completely or not at all. A failing op returns `422 invalid_op`
with its `op_index`, and nothing is saved.

## Passing data between nodes [#passing-data-between-nodes]

`{{ context.<node id>.<field> }}` reads an earlier node's output. The engine
also provides `{{ context.webhook }}` (the trigger payload) and
`{{ context.execution_link }}`. Only nodes that run earlier have output.

Node ids appear in these references, so pick readable ones: `check_interest`,
`wa_followup`, `notify_sales`. Use lowercase letters, digits and underscores,
starting with a letter. These names are reserved: `webhook`, `execution_id`,
`execution_link`, `get`, `items`, `keys`, `values`.

  A number, template, integration or agent field must hold a literal id from
  `listWorkflowFieldOptions`. A `{{ }}` template there is refused as
  `validation_failed` with the issue code `templated_ref`, because it could
  resolve to a record in another workspace.
  Only free-text fields (a message body, a `to_number`, a condition value)
  take `{{ }}`.

## Node rules worth knowing [#node-rules-worth-knowing]

- **`code`** runs a sandboxed Python snippet and must assign its output to a
  `result` variable. A dict in `result` becomes the node's output. Without it,
  the node has no output.
- **`sub_workflow`** starts another of your workflows and continues at once. The
  target must be **published and active**. A paused target fails the run.
- **`wait`, `wait_for_time`, `voice_ai` and `send_email`** pause the run and
  resume it later. There is no end node: a branch ends at a node with no
  outgoing edge, and the run finishes when every branch has ended.
- **`notes`** never connects and never runs. Add one that says what the workflow
  is for, so the next person to open the canvas understands it.

## Credentials [#credentials]

Never put secrets in config when an integration exists. Pick integrations,
numbers and templates from `listWorkflowFieldOptions`. Secret fields (a Slack
webhook URL, API headers and body params) read back as `********`. Send
`********` back unchanged and the stored value is kept. If the user has no
matching integration, tell them to connect one in the dashboard first.

## Test runs are real [#test-runs-are-real]

  `testWorkflow` runs the draft once with your `payload` as the trigger data.
  WhatsApp messages, SMS, emails and calls go out, and CRM records change.
  Before you call it, tell the user what will happen and get a yes. Use their
  own phone number or email in the payload. Then read `getWorkflowExecution`
  for each node's status, output and error.

## Errors you will see [#errors-you-will-see]

| Error | Meaning | What to do |
|---|---|---|
| `409 revision_conflict` | The draft changed since you read it, often the user saved in the dashboard | `getWorkflow`, then retry with the new `revision` |
| `422 validation_failed` with `not_found` | An id is not in this workspace | Pick from `listWorkflowFieldOptions` |
| `422 validation_failed` on publish | Missing fields, an unconnected node, or a bad handle | Fix each one, then publish again |
| `422 invalid_op` | An op is malformed or names a missing node | Fix the op at `op_index` |
| `422 validation_failed` with `templated_ref` | An id field holds a `{{ }}` template | Put a literal id from `listWorkflowFieldOptions` |
| `400 no_options` | The field holds an id from another system | Ask the user for the value |

## Worked example [#worked-example]

"After a call, if the lead is interested, send our follow-up WhatsApp template
and tell #sales in Slack."

### Create [#create]

`createWorkflow { "name": "Interested lead follow-up" }` gives you a `trigger`
node and a `revision`.

### Find the ids [#find-the-ids]

`listWorkflowFieldOptions` for `whatsapp.phone_number_id`, then for
`whatsapp.template_id` with `depends` set to that number. Confirm both with the
user.

### Build the graph [#build-the-graph]

One `editWorkflow` batch, using the `revision` from step 1:

```json
{
  "revision": "3f9a1c07be2d",
  "ops": [
    {"op": "add_node", "id": "check_interest", "type": "conditional", "after": "trigger",
     "config": {"conditions": [{"variable": "{{ context.trigger.extracted_variables.interested }}", "operator": "==", "value": "yes"}]}},
    {"op": "add_node", "id": "wa_followup", "type": "whatsapp", "after": "check_interest", "handle": "True",
     "config": {"phone_number_id": 8265, "message_type": "template", "template_id": 3676, "to_number": "{{ context.trigger.customer_number }}"}},
    {"op": "add_node", "id": "notify_sales", "type": "slack", "after": "wa_followup",
     "config": {"slack_webhook_url": "<from the user>", "message": "Follow-up sent to {{ context.trigger.customer_number }}"}},
    {"op": "add_node", "id": "about", "type": "notes",
     "config": {"note_text": "Follows up interested leads after a call."}}
  ]
}
```

The `phone_number_id` and `template_id` are literal ids from step 2. The
`to_number` and `message` are free text, so they take `{{ }}`.

### Validate, publish, attach [#validate-publish-attach]

`validateWorkflow`, fix anything reported, then `publishWorkflow`. Tell the
user to point the agent's post-call action at this workflow.