curl --request POST \
--url https://openapi.enginy.ai/v1/workflows \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"name": "<string>",
"plan": {
"nodes": [
{
"id": "<string>",
"kind": "BLOCK",
"blockId": "<string>",
"inputs": {},
"next": [
"<string>"
]
}
],
"name": "<string>"
},
"trigger": {
"type": "MANUAL"
},
"maxCreditsPerRun": 123,
"publish": true
}
'import requests
url = "https://openapi.enginy.ai/v1/workflows"
payload = {
"name": "<string>",
"plan": {
"nodes": [
{
"id": "<string>",
"kind": "BLOCK",
"blockId": "<string>",
"inputs": {},
"next": ["<string>"]
}
],
"name": "<string>"
},
"trigger": { "type": "MANUAL" },
"maxCreditsPerRun": 123,
"publish": True
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: '<string>',
plan: {
nodes: [
{
id: '<string>',
kind: 'BLOCK',
blockId: '<string>',
inputs: {},
next: ['<string>']
}
],
name: '<string>'
},
trigger: {type: 'MANUAL'},
maxCreditsPerRun: 123,
publish: true
})
};
fetch('https://openapi.enginy.ai/v1/workflows', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://openapi.enginy.ai/v1/workflows",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'plan' => [
'nodes' => [
[
'id' => '<string>',
'kind' => 'BLOCK',
'blockId' => '<string>',
'inputs' => [
],
'next' => [
'<string>'
]
]
],
'name' => '<string>'
],
'trigger' => [
'type' => 'MANUAL'
],
'maxCreditsPerRun' => 123,
'publish' => true
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://openapi.enginy.ai/v1/workflows"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"plan\": {\n \"nodes\": [\n {\n \"id\": \"<string>\",\n \"kind\": \"BLOCK\",\n \"blockId\": \"<string>\",\n \"inputs\": {},\n \"next\": [\n \"<string>\"\n ]\n }\n ],\n \"name\": \"<string>\"\n },\n \"trigger\": {\n \"type\": \"MANUAL\"\n },\n \"maxCreditsPerRun\": 123,\n \"publish\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://openapi.enginy.ai/v1/workflows")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"plan\": {\n \"nodes\": [\n {\n \"id\": \"<string>\",\n \"kind\": \"BLOCK\",\n \"blockId\": \"<string>\",\n \"inputs\": {},\n \"next\": [\n \"<string>\"\n ]\n }\n ],\n \"name\": \"<string>\"\n },\n \"trigger\": {\n \"type\": \"MANUAL\"\n },\n \"maxCreditsPerRun\": 123,\n \"publish\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://openapi.enginy.ai/v1/workflows")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"plan\": {\n \"nodes\": [\n {\n \"id\": \"<string>\",\n \"kind\": \"BLOCK\",\n \"blockId\": \"<string>\",\n \"inputs\": {},\n \"next\": [\n \"<string>\"\n ]\n }\n ],\n \"name\": \"<string>\"\n },\n \"trigger\": {\n \"type\": \"MANUAL\"\n },\n \"maxCreditsPerRun\": 123,\n \"publish\": true\n}"
response = http.request(request)
puts response.read_body{
"status": "success",
"message": "<string>",
"data": {
"id": 123,
"appUrl": "<string>",
"name": "<string>",
"isPublished": true,
"warnings": [
"<string>"
]
}
}Create workflow
Create a workflow from a plan. Creates a draft by default; set publish: true to publish immediately (strict validation applies).
Call GET /v1/workflows/blocks and GET /v1/workflows/blocks/{blockId} first to discover valid blockIds and inputs.
A workflow is a DAG of nodes described by the plan object: { name?, nodes: [] }.
Each node has a stable string id and a next array of node ids it points to (an empty string "" marks an unwired slot). There are three node kinds:
BLOCK— one workflow step.{ id, kind: "BLOCK", blockId, inputs: {}, next: [] }.blockIdcomes fromGET /v1/workflows/blocks;inputsare the block’s configurable inputs (discover them withGET /v1/workflows/blocks/{blockId}). A BLOCK has exactly onenextslot, except two-outlet blocks:WAIT_FOR_APPROVALhas[approvedTargetId, rejectedTargetId]andCHECK_LEAD_FOR_CHANGEShas[changedTargetId, unchangedTargetId]. Put configured values in a BLOCK’sinputs, never the input-catalog metadata. For a WEBHOOK trigger’s declared non-entity input, bind the matching field as{ "webhookInputKey": "<declared key>" }(for exampleinputs.companyPayload = { "webhookInputKey": "companyPayload" }). Entity inputs such asleadIdsandcompanyIdsuse"__MANUAL_INPUT"on the entry block instead.CONDITIONAL— branches the stream.{ id, kind: "CONDITIONAL", conditions: [], fallbackName?, next: [] }. Each condition is EITHER a field predicate{ field, operator, value? }(lead/company/AI-variable fields — discover them withGET /v1/workflows/condition-fields) OR a random-split branch{ type: "RANDOM_SPLIT", percentage }(deterministic A/B split; integer 1-100, percentages must sum to ≤ 100 across the node, the remainder falls through to the fallback).nexthas exactlyconditions.length + 1entries: one target per matched condition (in order) then the fallback target LAST.KING(shown as Prioritize by group) — groups records, sorts each group ascending by a formula field, and routes an exact number from every group separately from the remainder.{ id, kind: "KING", groupBy, orderBy, checkAnyDomain?, limit?, next: [] }.nextis[prioritizedTargetId, remainingTargetId].
The first node (the one no other node points to) should be a selector/import entry block. A plan may hold at most 60 nodes and no cycles. An empty plan (nodes: []) is valid for a brand-new draft.
Field/entity prefixes used inside conditions: company., anyLead., and webhookInput. for webhook-triggered workflows.
Responses include direct Enginy app URLs when available. The response includes an appUrl to the workflow editor. MCP agents should return those URLs to users whenever they are present in the response.
Required scope:
WORKFLOWS_WRITERate limit: 30 requests per minute
curl --request POST \
--url https://openapi.enginy.ai/v1/workflows \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"name": "<string>",
"plan": {
"nodes": [
{
"id": "<string>",
"kind": "BLOCK",
"blockId": "<string>",
"inputs": {},
"next": [
"<string>"
]
}
],
"name": "<string>"
},
"trigger": {
"type": "MANUAL"
},
"maxCreditsPerRun": 123,
"publish": true
}
'import requests
url = "https://openapi.enginy.ai/v1/workflows"
payload = {
"name": "<string>",
"plan": {
"nodes": [
{
"id": "<string>",
"kind": "BLOCK",
"blockId": "<string>",
"inputs": {},
"next": ["<string>"]
}
],
"name": "<string>"
},
"trigger": { "type": "MANUAL" },
"maxCreditsPerRun": 123,
"publish": True
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: '<string>',
plan: {
nodes: [
{
id: '<string>',
kind: 'BLOCK',
blockId: '<string>',
inputs: {},
next: ['<string>']
}
],
name: '<string>'
},
trigger: {type: 'MANUAL'},
maxCreditsPerRun: 123,
publish: true
})
};
fetch('https://openapi.enginy.ai/v1/workflows', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://openapi.enginy.ai/v1/workflows",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'plan' => [
'nodes' => [
[
'id' => '<string>',
'kind' => 'BLOCK',
'blockId' => '<string>',
'inputs' => [
],
'next' => [
'<string>'
]
]
],
'name' => '<string>'
],
'trigger' => [
'type' => 'MANUAL'
],
'maxCreditsPerRun' => 123,
'publish' => true
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://openapi.enginy.ai/v1/workflows"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"plan\": {\n \"nodes\": [\n {\n \"id\": \"<string>\",\n \"kind\": \"BLOCK\",\n \"blockId\": \"<string>\",\n \"inputs\": {},\n \"next\": [\n \"<string>\"\n ]\n }\n ],\n \"name\": \"<string>\"\n },\n \"trigger\": {\n \"type\": \"MANUAL\"\n },\n \"maxCreditsPerRun\": 123,\n \"publish\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://openapi.enginy.ai/v1/workflows")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"plan\": {\n \"nodes\": [\n {\n \"id\": \"<string>\",\n \"kind\": \"BLOCK\",\n \"blockId\": \"<string>\",\n \"inputs\": {},\n \"next\": [\n \"<string>\"\n ]\n }\n ],\n \"name\": \"<string>\"\n },\n \"trigger\": {\n \"type\": \"MANUAL\"\n },\n \"maxCreditsPerRun\": 123,\n \"publish\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://openapi.enginy.ai/v1/workflows")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"plan\": {\n \"nodes\": [\n {\n \"id\": \"<string>\",\n \"kind\": \"BLOCK\",\n \"blockId\": \"<string>\",\n \"inputs\": {},\n \"next\": [\n \"<string>\"\n ]\n }\n ],\n \"name\": \"<string>\"\n },\n \"trigger\": {\n \"type\": \"MANUAL\"\n },\n \"maxCreditsPerRun\": 123,\n \"publish\": true\n}"
response = http.request(request)
puts response.read_body{
"status": "success",
"message": "<string>",
"data": {
"id": 123,
"appUrl": "<string>",
"name": "<string>",
"isPublished": true,
"warnings": [
"<string>"
]
}
}Authorizations
Body
Workflow name.
1 - 120The workflow DAG. See the endpoint description for the plan format.
Show child attributes
Show child attributes
Trigger config. Defaults to MANUAL.
- WorkflowTrigger
- WorkflowTrigger
- WorkflowTrigger
- WorkflowTrigger
- WorkflowTrigger
Show child attributes
Show child attributes
Optional per-run credit ceiling.
When true, publish immediately after creating the draft (strict validation applies). Default false.