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
Configure an import source from an audience
You can build a recurring search from natural-language criteria such as “100 software engineers in Barcelona city” without supplying a LinkedIn search URL.- Call
POST /v1/ai-finder/previews(preview_an_ai_finder_search) with the audience and provider, or reuse the latest matching preview. - Call
GET /v1/workflows/import-inputs?previewId=<previewId>&maxCount=100(get_workflow_import_inputs, scopeWORKFLOWS_READ). - Add an import node with the returned
blockIdandinputs: {}. Review anywarnings. - Send
importSources: [{ nodeId, ...source }]alongsideplanto both validate and create, using the resolver’s shortsource: { previewId, maxCount }reference. The server applies the exact source inputs before validation and saving, so generated URLs and filter objects do not need to be copied by the model. Validation returns the normalizedplanfor inspection when references are present.
id: "prospects", both request bodies include:
blockId matches the resolved source. References are consumed during authoring; saved workflows keep the resolved inputs and do not depend on a preview at execution time. Source values replace any conflicting values on that node. Tenant, expiry, and identity access checks apply on every resolution.
The resolver generates LinkedIn people/company search URLs and prepares Google Maps, fundraising data, StoreLeads, TheirStack, and supported CRM import inputs. It does not import records, create a destination list, or run the workflow. A scheduled import repeats the search on each run; selecting the previous results list reuses those records instead.
Previews expire after 24 hours. If unavailable, regenerate a preview from the original criteria. Unsupported sources return an error rather than guessed inputs. Some CRM import blocks cannot enforce a per-run maximum; their responses include a warning so callers can decide how to proceed. LinkedIn post/event/job URLs and Pipedrive/Odoo source configurations are outside this resolver’s supported search types.
For weekly schedules, recurrence.byWeekday uses 0 for Sunday, 1 for Monday, through 6 for Saturday. Include an explicit UTC offset or Z in startsAt so its instant corresponds to the requested wall-clock time in the schedule’s IANA timezone; an offset-free timestamp depends on the server timezone.
The graph model
A plan is a directed acyclic graph. Every node has a stable stringid 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
BLOCK — one workflow step
BLOCK — one workflow step
blockIdcomes fromGET /v1/workflows/blocks.inputsare the block’s configured inputs — discover them withGET /v1/workflows/blocks/{blockId}.- A
BLOCKhas exactly onenextslot, exceptWAIT_FOR_APPROVAL, which has two:[approvedTargetId, rejectedTargetId].
CONDITIONAL — branch on record data
CONDITIONAL — branch on record data
nexthas exactlyconditions.length + 1entries: one target per condition (in order), then the fallback target last.- Discover fields and operators with
GET /v1/workflows/condition-fields. The catalog includes entity fields plus engagement, LinkedIn, CRM, platform, activity, and run-context fields. Each field reports its compatibleoperatorIdsand fixedvalueOptionswhen applicable. - At most 20 conditions on a single node.
55901 with the built-in platform.listIds field:percentageis 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_SPLITof 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.
Prioritize by group (KING)
Prioritize by group (KING)
groupBychooses the field that forms the groups,orderBychooses the formula field used to sort each group, andlimitis the exact number of records prioritized from every group (default1).- Lower formula values are prioritized first; records without a value come last. Ties do not expand the limit.
nextis[prioritizedTargetId, remainingTargetId].- The example prioritizes up to 10 contacts per company for
campaignand sends the remaining contacts toreview. KINGremains the stable plan kind even though the user-facing step is named Prioritize by group.
Wiring rules
- The
nextslot count is fixed per node kind:BLOCK→ 1 (or 2 forWAIT_FOR_APPROVAL),CONDITIONAL→conditions.length + 1,KING→ 2. - Every id in a
nextarray 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.
Plan anatomy
A complete, publishable plan for import a list → check email deliverability → add to a campaign:importis the entry node — nothing points to it, and it’s a selector block.verifyleavesinputsempty: itsleadIdinput is the record flowing from the previous step, so you omit it. Only selector/standalone steps carry an explicit entity id.campaignis terminal — its singlenextslot is"".- Every required input a publish needs is present (
SELECT_LEADS_FROM_LIST.leadsGroupId,ADD_LEADS_TO_CAMPAIGN.campaignId).
Triggers
The optionaltrigger 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.
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/4021 → revision: "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 viaruntimeInputContract 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":
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 orPATCH …/draft.- 60 nodes max per plan.
- 20 conditions max per
CONDITIONALnode.
Access, scopes, and rate limits
Every request is authenticated with your API key in thex-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).
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.