> ## 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 Cookbook

> Four complete, copy-pasteable workflow builds — linear, branching, webhook-triggered, and scheduled.

Four end-to-end recipes. Each is a full plan plus the curl sequence to create, publish, and run it, with abbreviated responses. Every plan uses **real block ids** and passes publish validation. For the model behind them, read the [guide](/api-reference/workflows-guide); for the per-endpoint contract, the interactive [Workflows endpoints](/api-reference/workflows/get-workflows) in the Endpoints section.

All examples assume `x-api-key: gsk_your_api_key_here` and the base URL `https://openapi.enginy.ai`. The list id `55762`, campaign id `8842`, and workflow id `4021` are placeholders — resolve real ids with `get_lists` and `get_campaigns`.

***

## 1. Simplest linear flow

**Import a contact list → check email deliverability → add to a campaign.** One straight line, no input needed at run time.

<Steps>
  <Step title="Create and publish in one call">
    ```bash theme={null}
    curl -X POST "https://openapi.enginy.ai/v1/workflows" \
      -H "x-api-key: gsk_your_api_key_here" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Verify emails from a list",
        "plan": {
          "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": [""] }
          ]
        },
        "publish": true
      }'
    ```

    ```json theme={null}
    { "status": "success", "data": { "id": 4021, "isPublished": true, "warnings": [] } }
    ```
  </Step>

  <Step title="Confirm the run contract">
    ```bash theme={null}
    curl "https://openapi.enginy.ai/v1/workflow/4021" -H "x-api-key: gsk_your_api_key_here"
    ```

    ```json theme={null}
    { "status": "success", "data": { "runtimeInputContract": { "status": "EMPTY", "exampleBody": {} }, "revision": "2026-07-19T10:16:00.000Z" } }
    ```

    `EMPTY` means the entry list is baked in — run it with no `input`.
  </Step>

  <Step title="Run it">
    ```bash theme={null}
    curl -X POST "https://openapi.enginy.ai/v1/workflow/4021/run" \
      -H "x-api-key: gsk_your_api_key_here" -H "Content-Type: application/json" -d '{}'
    ```

    ```json theme={null}
    { "status": "success", "data": { "runId": "a1f3c9e2-7b40-4d1a-9e55-2c6f0b8e1234", "status": "RUNNING", "sequenceNumber": 7 } }
    ```
  </Step>

  <Step title="Poll until it finishes">
    ```bash theme={null}
    curl "https://openapi.enginy.ai/v1/workflow-runs/a1f3c9e2-7b40-4d1a-9e55-2c6f0b8e1234" \
      -H "x-api-key: gsk_your_api_key_here"
    ```

    ```json theme={null}
    {
      "status": "success",
      "data": {
        "status": "SUCCEEDED",
        "distinctEntityCount": 42,
        "steps": [
          { "stepId": 1, "stepType": "VERIFY_LEAD_EMAIL", "statusCounts": { "SUCCEEDED": 40, "FAILED": 2 } },
          { "stepId": 2, "stepType": "ADD_LEADS_TO_CAMPAIGN", "statusCounts": { "SUCCEEDED": 40 } }
        ]
      }
    }
    ```

    Poll `status` until it's a terminal value (`SUCCEEDED`, `FAILED`, `CANCELLED`).
  </Step>
</Steps>

***

## 2. Conditional branching

**Only campaign the contacts that have an email; send the rest to a review list.** A `CONDITIONAL` node splits the stream.

The condition node has one condition and therefore **two** `next` slots: the target for the matched condition, then the fallback **last**.

```json theme={null}
{
  "name": "Email-gated routing",
  "plan": {
    "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": ["gate"] },
      {
        "id": "gate",
        "kind": "CONDITIONAL",
        "conditions": [{ "field": "professionalEmail", "operator": "IS_NOT_EMPTY" }],
        "fallbackName": "No email",
        "next": ["campaign", "review"]
      },
      {
        "id": "campaign",
        "kind": "BLOCK",
        "blockId": "ADD_LEADS_TO_CAMPAIGN",
        "inputs": { "campaignId": 8842, "excludeContactedLeads": false },
        "next": [""]
      },
      {
        "id": "review",
        "kind": "BLOCK",
        "blockId": "ADD_LEADS_TO_GROUP",
        "inputs": { "leadsGroupId": 55901 },
        "next": [""]
      }
    ]
  }
}
```

<Check>
  `gate.next` has `conditions.length + 1 = 2` entries. `next[0]` (`campaign`) runs for contacts matching the
  condition; `next[1]` (`review`) is the fallback. Discover valid `field` / `operator` values with `GET
      /v1/workflows/condition-fields`.
</Check>

Create it as a draft, validate, then publish:

```bash theme={null}
# 1) create (draft)
curl -X POST "https://openapi.enginy.ai/v1/workflows" \
  -H "x-api-key: gsk_your_api_key_here" -H "Content-Type: application/json" \
  -d @email-gated-routing.json
# → { "data": { "id": 4022, "isPublished": false } }

# 2) validate the stored draft as if publishing
curl "https://openapi.enginy.ai/v1/workflow/4022/validate-draft" -H "x-api-key: gsk_your_api_key_here"
# → { "data": { "isValid": true, "errors": [], "warnings": [] } }

# 3) publish
curl -X POST "https://openapi.enginy.ai/v1/workflow/4022/publish" \
  -H "x-api-key: gsk_your_api_key_here" -H "Content-Type: application/json" -d '{}'
# → { "data": { "version": 1, "isPublished": true } }
```

***

## 3. Webhook-triggered workflow with declared inputs

**Push a set of contact ids into a workflow from your own system.** The `WEBHOOK` trigger declares a typed input; delivering a payload to the webhook URL starts a run seeded with those contacts.

```json theme={null}
{
  "name": "Inbound contact enrichment",
  "trigger": {
    "type": "WEBHOOK",
    "inputs": [{ "key": "contacts", "type": "leadIds", "label": "Contact IDs" }]
  },
  "plan": {
    "nodes": [
      {
        "id": "import",
        "kind": "BLOCK",
        "blockId": "SELECT_LEADS",
        "inputs": { "leadIds": "__MANUAL_INPUT" },
        "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": true },
        "next": [""]
      }
    ]
  }
}
```

<Info>
  **How declared inputs bind.** An **entity** input type (`leadIds` or `companyIds`) seeds the entry block through
  the `"__MANUAL_INPUT"` sentinel — set it on the entry step's entity id, as above. A **scalar/array** input type
  instead binds to a specific node field with `{ "webhookInputKey": "<key>" }` in that field's place.
</Info>

<Steps>
  <Step title="Create and publish">
    Publishing arms the webhook.

    ```bash theme={null}
    curl -X POST "https://openapi.enginy.ai/v1/workflows" \
      -H "x-api-key: gsk_your_api_key_here" -H "Content-Type: application/json" \
      -d @inbound-contact-enrichment.json
    # → { "data": { "id": 4023 } }
    curl -X POST "https://openapi.enginy.ai/v1/workflow/4023/publish" \
      -H "x-api-key: gsk_your_api_key_here" -H "Content-Type: application/json" -d '{}'
    ```
  </Step>

  <Step title="Read the live webhook URL">
    ```bash theme={null}
    curl "https://openapi.enginy.ai/v1/workflow/4023" -H "x-api-key: gsk_your_api_key_here"
    ```

    ```json theme={null}
    {
      "status": "success",
      "data": {
        "trigger": {
          "type": "WEBHOOK",
          "webhookUrl": "https://openapi.enginy.ai/wf/6f1c2a90-2d5b-4e7a-9c11-b0d3e4f5a6b7",
          "webhookInputs": [{ "key": "contacts", "type": "leadIds" }]
        }
      }
    }
    ```
  </Step>

  <Step title="Deliver a payload to start a run">
    Send the declared key (`contacts`) to the webhook URL:

    ```bash theme={null}
    curl -X POST "https://openapi.enginy.ai/wf/6f1c2a90-2d5b-4e7a-9c11-b0d3e4f5a6b7" \
      -H "Content-Type: application/json" \
      -d '{ "contacts": [101, 102, 103] }'
    ```
  </Step>
</Steps>

<Warning>
  A webhook trigger that **declares inputs** can't be started with `POST /v1/workflow/{id}/run` (it returns
  `400`) — deliver its payload to the webhook URL instead. Watch executions with `GET /v1/workflow/4023/runs`.
</Warning>

***

## 4. Scheduled workflow

**Run a list on a recurring schedule.** A `SCHEDULE` trigger fires the workflow on its own; you never call `run`.

```json theme={null}
{
  "name": "Weekly list refresh",
  "trigger": {
    "type": "SCHEDULE",
    "definition": "WEEKLY",
    "startsAt": "2026-07-27T08:00:00.000Z",
    "timezone": "Europe/Madrid"
  },
  "plan": {
    "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": [""] }
    ]
  }
}
```

```bash theme={null}
# create + publish — the schedule arms on publish
curl -X POST "https://openapi.enginy.ai/v1/workflows" \
  -H "x-api-key: gsk_your_api_key_here" -H "Content-Type: application/json" \
  -d @weekly-list-refresh.json
# → { "data": { "id": 4024 } }
curl -X POST "https://openapi.enginy.ai/v1/workflow/4024/publish" \
  -H "x-api-key: gsk_your_api_key_here" -H "Content-Type: application/json" -d '{}'
```

<Tip>
  `definition` accepts `DAILY`, `WEEKLY`, `BI_WEEKLY`, `MONTHLY`, `QUARTERLY`, `YEARLY`. For finer control
  (specific weekdays, an end date, or an every-N-periods interval), attach an optional `recurrence` rule to
  the trigger. Each scheduled firing appears in `GET /v1/workflow/{id}/runs` with `triggerType: "SCHEDULE"`.
</Tip>

***

## Patterns worth reusing

<AccordionGroup>
  <Accordion title="Edit incrementally instead of resubmitting the whole plan" icon="pen">
    Once a workflow exists, `PATCH /v1/workflow/{id}/draft` with an `ops` array changes just what you touch —
    e.g. `{ "kind": "updateStepInputs", "id": "campaign", "inputs": { "campaignId": 9001, "excludeContactedLeads": true } }`
    — and returns the new `revision`. Pass that `revision` on the next write to catch concurrent edits (`409`).
  </Accordion>

  <Accordion title="Make a workflow accept ids at run time" icon="wand-magic-sparkles">
    Set the entry step's entity input to `"__MANUAL_INPUT"` (instead of a fixed id). `GET` then reports
    `runtimeInputContract.status: "SUPPORTED"`, and you run it with one key from `acceptedKeys`, e.g.
    `{ "input": { "leadIds": [101, 102] } }` or `{ "input": { "contactListId": 55762 } }`.
  </Accordion>

  <Accordion title="Cap spend per run" icon="coins">
    Add `"maxCreditsPerRun": 5000` to the create or `PATCH …/draft` body. Enrichment and AI steps consume credits;
    a run stops once it reaches the ceiling.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Endpoints" icon="code" href="/api-reference/workflows/get-workflows">
    Every request and response shape, generated from the OpenAPI spec.
  </Card>

  <Card title="MCP agent guide" icon="robot" href="/mcp/workflows">
    Drive these same flows from an AI agent.
  </Card>
</CardGroup>
