Skip to main content
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). This page is the mental model. If you just want to ship something, jump to the Cookbook; for the full request/response contract of each call, see the interactive Workflows endpoints in the Endpoints section.
This is the API view of workflows. For the visual editor, templates, and per-step product reference, see the Workflows product guide. Both drive the same underlying workflow — a plan you publish over the API opens and runs identically in the app.

What you work with

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.

Node kinds

  • 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].
  • 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:
  • 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.
  • next is [leftTargetId, rightTargetId].
  • Use it to collapse duplicates or keep one record per group before an expensive step.

Wiring rules

  • The next slot count is fixed per node kind: BLOCK → 1 (or 2 for WAIT_FOR_APPROVAL), CONDITIONALconditions.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.
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.

Plan anatomy

A complete, publishable plan for import a list → check email deliverability → add to a campaign:
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).
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.

Triggers

The optional trigger decides when a run starts. It defaults to MANUAL. All five types:
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.

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:
1

Read

GET /v1/workflow/4021revision: "2026-07-19T10:15:30.000Z".
2

Write with the revision

PATCH /v1/workflow/4021/draft with "revision": "2026-07-19T10:15:30.000Z".
3

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.

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: To make an entry step accept a run-time id, set its entity input to the sentinel "__MANUAL_INPUT":
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). The base URL is https://openapi.enginy.ai. 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).
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.

Where to next

Endpoints

The interactive reference for every workflow endpoint, generated from the OpenAPI spec.

Cookbook

Four end-to-end builds you can copy and run.

MCP agent guide

The recommended tool flow for AI agents.