> ## Documentation Index
> Fetch the complete documentation index at: https://docs.enginy.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Workflows API Overview

> The graph model behind the Workflows API — nodes, wiring, plans, triggers, drafts, runs, and limits.

The Workflows API lets you build, publish, and run **Advanced Workflows** — the graph automations that import contacts and companies, enrich them, branch on their data, and act on the result — entirely over REST (and, through the same operations, over [MCP](/mcp/workflows)).

This page is the mental model. If you just want to ship something, jump to the [Cookbook](/api-reference/workflows-cookbook); for the full request/response contract of each call, see the interactive [Workflows endpoints](/api-reference/workflows/get-workflows) in the Endpoints section.

<Info>
  This is the **API** view of workflows. For the visual editor, templates, and per-step product reference, see
  the [Workflows](/workflows/overview) product guide. Both drive the same underlying workflow — a plan you
  publish over the API opens and runs identically in the app.
</Info>

## What you work with

| Concept               | What it is                                                                                                                                                                           |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Plan**              | The workflow graph you submit: `{ name?, nodes: [] }`.                                                                                                                               |
| **Block**             | A single workflow step (import, enrich, condition helper, action). Every `BLOCK` node references a `blockId` from the [block catalog](/api-reference/workflows/get-workflow-blocks). |
| **Node**              | One vertex in the plan graph. Three kinds: `BLOCK`, `CONDITIONAL`, `KING`.                                                                                                           |
| **Draft**             | The editable working copy of a workflow. Every create/update writes the draft.                                                                                                       |
| **Published version** | An immutable snapshot of the draft that a run executes. Publishing bumps the version number.                                                                                         |
| **Run**               | One execution of a published version over a set of records.                                                                                                                          |

## The graph model

A plan is a **directed acyclic graph**. Every node has a stable string `id` and a `next` array of the node ids it points to. The empty string `""` marks an **unwired** slot (a dangling branch a draft can tolerate but a publish cannot).

The **entry node** is the single node that no other node points to. It must be a selector or import block — the step that brings records into the workflow.

```mermaid theme={null}
flowchart LR
    A["import<br/>SELECT_LEADS_FROM_LIST"] --> B["verify<br/>VERIFY_LEAD_EMAIL"]
    B --> C{"gate<br/>CONDITIONAL"}
    C -->|has email| D["campaign<br/>ADD_LEADS_TO_CAMPAIGN"]
    C -->|fallback| E["review<br/>ADD_LEADS_TO_GROUP"]
```

### Node kinds

<AccordionGroup>
  <Accordion title="BLOCK — one workflow step" icon="cube">
    ```json theme={null}
    { "id": "verify", "kind": "BLOCK", "blockId": "VERIFY_LEAD_EMAIL", "inputs": {}, "next": ["campaign"] }
    ```

    * `blockId` comes from `GET /v1/workflows/blocks`.
    * `inputs` are the block's configured inputs — discover them with `GET /v1/workflows/blocks/{blockId}`.
    * A `BLOCK` has **exactly one** `next` slot, **except** `WAIT_FOR_APPROVAL`, which has **two**: `[approvedTargetId, rejectedTargetId]`.
  </Accordion>

  <Accordion title="CONDITIONAL — branch on record data" icon="code-branch">
    ```json theme={null}
    {
      "id": "gate",
      "kind": "CONDITIONAL",
      "conditions": [{ "field": "professionalEmail", "operator": "IS_NOT_EMPTY" }],
      "fallbackName": "No email",
      "next": ["campaign", "review"]
    }
    ```

    * `next` has **exactly `conditions.length + 1`** entries: one target per condition (**in order**), then the fallback target **last**.
    * Discover fields and operators with `GET /v1/workflows/condition-fields`.
    * At most **20 conditions** on a single node.

    A condition can also be a **random split** (deterministic A/B split) instead of a field predicate:

    ```json theme={null}
    {
      "id": "ab",
      "kind": "CONDITIONAL",
      "conditions": [{ "type": "RANDOM_SPLIT", "percentage": 50 }],
      "fallbackName": "Variant B",
      "next": ["variant-a", "variant-b"]
    }
    ```

    * `percentage` is an integer **1–100**; sibling split percentages on one node must sum to **≤ 100**.
    * Any remainder falls through to the **fallback** branch (so one `RANDOM_SPLIT` of 50 is a 50/50 A/B split).
    * Each record gets a stable roll per run — re-running a step never reshuffles records between branches.
  </Accordion>

  <Accordion title="KING — dedupe / group the stream" icon="crown">
    ```json theme={null}
    { "id": "dedupe", "kind": "KING", "groupBy": "company", "orderBy": 0, "next": ["enrich", ""] }
    ```

    * `next` is `[leftTargetId, rightTargetId]`.
    * Use it to collapse duplicates or keep one record per group before an expensive step.
  </Accordion>
</AccordionGroup>

### Wiring rules

* The `next` slot count is fixed per node kind: `BLOCK` → 1 (or 2 for `WAIT_FOR_APPROVAL`), `CONDITIONAL` → `conditions.length + 1`, `KING` → 2.
* Every id in a `next` array must reference an existing node id, or be `""` for an unwired slot.
* No cycles. A plan may hold at most **60 nodes**.
* The entry node is inferred (the node nothing points to) — you don't flag it.

<Warning>
  Drafts tolerate `""` (unwired) slots and unconfigured inputs so you can sketch. **Publishing is strict** —
  every slot must be wired to a real node and every required input filled. Validate before you publish with
  `GET /v1/workflow/{workflowId}/validate-draft`.
</Warning>

## Plan anatomy

A complete, publishable plan for **import a list → check email deliverability → add to a campaign**:

```json theme={null}
{
  "name": "Verify emails from a list",
  "nodes": [
    {
      "id": "import",
      "kind": "BLOCK",
      "blockId": "SELECT_LEADS_FROM_LIST",
      "inputs": { "leadsGroupId": 55762 },
      "next": ["verify"]
    },
    { "id": "verify", "kind": "BLOCK", "blockId": "VERIFY_LEAD_EMAIL", "inputs": {}, "next": ["campaign"] },
    {
      "id": "campaign",
      "kind": "BLOCK",
      "blockId": "ADD_LEADS_TO_CAMPAIGN",
      "inputs": { "campaignId": 8842, "excludeContactedLeads": false },
      "next": [""]
    }
  ]
}
```

What makes it valid:

* `import` is the entry node — nothing points to it, and it's a selector block.
* `verify` leaves `inputs` empty: its `leadId` input is the record flowing from the previous step, so you **omit** it. Only *selector/standalone* steps carry an explicit entity id.
* `campaign` is terminal — its single `next` slot is `""`.
* Every required input a *publish* needs is present (`SELECT_LEADS_FROM_LIST.leadsGroupId`, `ADD_LEADS_TO_CAMPAIGN.campaignId`).

<Tip>
  **Configured vs. flowing inputs.** Entity ids (`leadId`, `leadIds`, `companyId`, `companyIds`,
  `leadsGroupId`, `companyGroupId`) are the record stream. Set them only on the entry/selector step; on every
  downstream step, omit them so the step receives the previous step's output. `GET /v1/workflows/blocks/   {blockId}` spells out, per input, whether to configure it or let it flow.
</Tip>

## Triggers

The optional `trigger` decides when a run starts. It defaults to `MANUAL`. All five types:

<CodeGroup>
  ```json MANUAL theme={null}
  { "type": "MANUAL", "entity": "LEAD" }
  ```

  ```json SCHEDULE theme={null}
  {
    "type": "SCHEDULE",
    "definition": "WEEKLY",
    "startsAt": "2026-07-27T08:00:00.000Z",
    "timezone": "Europe/Madrid"
  }
  ```

  ```json WEBHOOK theme={null}
  { "type": "WEBHOOK", "inputs": [{ "key": "contacts", "type": "leadIds", "label": "Contact IDs" }] }
  ```

  ```json API theme={null}
  { "type": "API" }
  ```

  ```json INTENT theme={null}
  { "type": "INTENT", "signal": { "kind": "CONVERSATION_REPLY" } }
  ```
</CodeGroup>

| Type       | When it fires                                              | Notes                                                                                                                                                                                                   |
| ---------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MANUAL`   | You start it (via `POST /v1/workflow/{id}/run` or the app) | Optional `entity` (`LEAD` / `COMPANY`) seeds the run.                                                                                                                                                   |
| `SCHEDULE` | On a recurrence                                            | `definition` is `DAILY` / `WEEKLY` / `BI_WEEKLY` / `MONTHLY` / `QUARTERLY` / `YEARLY`; `startsAt` is ISO-8601. Attach an optional `recurrence` rule for finer control.                                  |
| `WEBHOOK`  | On delivery to the webhook URL                             | May declare typed `inputs` (max 20). The live URL is returned as `trigger.webhookUrl` on `GET`.                                                                                                         |
| `API`      | On an API-trigger call                                     | For programmatic entry from your own systems.                                                                                                                                                           |
| `INTENT`   | On an Enginy signal                                        | `signal.kind` is one of `CONVERSATION_REPLY`, `WEBSITE_VISITOR`, `LEAD_CREATED`, `COMPANY_CREATED`, `LEAD_PROPERTY_UPDATED`, `COMPANY_PROPERTY_UPDATED`, `LEAD_ADDED_TO_LIST`, `COMPANY_ADDED_TO_LIST`. |

<Note>
  A `WEBHOOK` trigger that **declares inputs** can't be started with `POST /v1/workflow/{id}/run` — deliver
  its payload to the webhook URL instead. A webhook trigger with no declared inputs can still be run manually.
</Note>

## Drafts, published versions, and concurrency

Every workflow has one editable **draft** and, once published, one live **published version**.

* **Create / update** always writes the draft. Nothing runs off a draft.
* **Publish** snapshots the draft into a new immutable version (`v1`, `v2`, …) and reconciles the trigger (e.g. arms a schedule). Runs execute the published version.
* **Editing a published workflow** is safe: the live version keeps running until you publish again.

Every `GET /v1/workflow/{workflowId}` returns a **`revision`** token (the workflow's `updatedAt`, ISO-8601). Pass it back on `PATCH …/draft` and `POST …/publish` for **optimistic concurrency**:

<Steps>
  <Step title="Read">`GET /v1/workflow/4021` → `revision: "2026-07-19T10:15:30.000Z"`.</Step>

  <Step title="Write with the revision">
    `PATCH /v1/workflow/4021/draft` with `"revision": "2026-07-19T10:15:30.000Z"`.
  </Step>

  <Step title="Handle 409">
    If someone else edited the draft in between, you get **409 Conflict** — re-read, reconcile, retry with the
    new revision. Omit `revision` to force last-write-wins (not recommended for shared workflows). `POST
            …/publish` can also return **409** when another publish is mid-flight (its trigger is already being
    updated) — that one isn't a revision conflict; just retry shortly.
  </Step>
</Steps>

## Runtime input contract

A published workflow declares what a run needs via `runtimeInputContract` on `GET /v1/workflow/{workflowId}`. It has one of three statuses, and — for `EMPTY` and `SUPPORTED` — a ready-to-send `exampleBody` you can copy straight into the `run` call:

| `status`      | Meaning                                                                                                                     | How to run                                                                                        |
| ------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `EMPTY`       | The entry step is fully configured (e.g. a fixed list).                                                                     | Call `run` with **no** `input`.                                                                   |
| `SUPPORTED`   | The entry step expects a record/list id at run time.                                                                        | Pass `input` with **one** key from `acceptedKeys`.                                                |
| `UNSUPPORTED` | The entry step can't take a run-time id — it's a `CONDITIONAL` or `KING`, or the plan declares more than one runtime input. | Not runnable via `run` — make the entry a selector/import block with a single `"__MANUAL_INPUT"`. |

To make an entry step accept a run-time id, set its entity input to the sentinel `"__MANUAL_INPUT"`:

```json theme={null}
{
  "id": "import",
  "kind": "BLOCK",
  "blockId": "SELECT_LEADS_FROM_LIST",
  "inputs": { "leadsGroupId": "__MANUAL_INPUT" },
  "next": ["verify"]
}
```

That yields a `SUPPORTED` contract whose `acceptedKeys` include the public alias (`contactListId`) and the raw key (`leadsGroupId`); the runner also accepts `leadIds` / `leadId` as a fallback. You'd then run it with, for example, `{ "input": { "contactListId": 55762 } }` or `{ "input": { "leadIds": [101, 102] } }`.

The **same sentinel drives WEBHOOK-triggered entry steps**: declare an entity-typed webhook input (e.g. `{ "key": "contacts", "type": "leadIds" }`) and set the entry step's entity input to `"__MANUAL_INPUT"` — the run is seeded from the webhook payload at delivery time. Entity-typed webhook inputs are never wired with a `{ "webhookInputKey": … }` binding; that binding style is only for non-entity fields (e.g. a `profileUrl` input filling a `profileUrl` field). For example, an `IMPORT_COMPANY_FROM_PAYLOAD` entry step with a declared `{ "key": "companyPayload", "type": "companyPayload" }` webhook input must set `inputs.companyPayload` to `{ "webhookInputKey": "companyPayload" }`. Put only configured values or bindings in node `inputs` — never copy the catalog's `key`, `type`, `required`, `valueShape`, or `resolution` metadata into the plan. Also note the delivery credentials (`trigger.webhookUrl` + `trigger.webhookSecret`) are issued on **publish** — a draft has none yet.

## Credits and limits

* **`maxCreditsPerRun`** — an optional integer ceiling on credits a single run may spend. Enrichment and AI steps consume credits; a run stops once it hits the ceiling. Set it on create or `PATCH …/draft`.
* **60 nodes** max per plan.
* **20 conditions** max per `CONDITIONAL` node.

## Access, scopes, and rate limits

Every request is authenticated with your API key in the `x-api-key` header (see [Authentication](/api-reference/introduction)). The base URL is `https://openapi.enginy.ai`.

|                     | Scope             | Rate limit (per key + endpoint) |
| ------------------- | ----------------- | ------------------------------- |
| **Read** endpoints  | `WORKFLOWS_READ`  | 100 requests / 60s              |
| **Write** endpoints | `WORKFLOWS_WRITE` | 30 requests / 60s               |

Granting `WORKFLOWS_WRITE` implies `WORKFLOWS_READ`. A workflow is only visible to a key whose creating user can access it (workspace admins see all; other users see workflows they own or were granted access to).

<Warning>
  **Feature gating.** The Workflows API requires the **Advanced Workflows** feature to be enabled for your
  workspace. If it isn't, the endpoints return **403 Forbidden**. Contact your Enginy admin to enable it.
</Warning>

## Where to next

<CardGroup cols={3}>
  <Card title="Endpoints" icon="code" href="/api-reference/workflows/get-workflows">
    The interactive reference for every workflow endpoint, generated from the OpenAPI spec.
  </Card>

  <Card title="Cookbook" icon="book-open" href="/api-reference/workflows-cookbook">
    Four end-to-end builds you can copy and run.
  </Card>

  <Card title="MCP agent guide" icon="robot" href="/mcp/workflows">
    The recommended tool flow for AI agents.
  </Card>
</CardGroup>
