Endpoints

Workflows

Build, run, publish and monitor workflows, decide their approvals and start them from webhooks.

View as Markdown

A workflow is an automated process made of steps: a trigger starts a run, and the steps after it call tools, run assistants, branch, loop, send HTTP requests, wait for a person's approval and set the result. The workflow API lets your application list workflows, run them with live progress, read past runs, decide approvals, edit and publish workflows, and start published workflows from a public webhook URL.

Every call runs as the user who owns the API key, with that user's permissions, organizations and credits. Runs started through the API execute the workflow draft (the current definition); published versions are run by schedules, webhooks and booster record triggers.

Scopes and access

Scope Allows
ayeto.workflow List and read workflows, run them (also streamed), cancel runs, list and read runs, the catalog, validation, schedule preview, revision history, export, approvals (list, count, read, decide).
ayeto.workflow.write Create, update and delete workflows, delete runs, deployment info (contains webhook secrets), publish, activate, regenerate webhook secrets, restore a revision, import, copy, the builder assistant.
* Everything.

The scopes do not include each other: a key that both reads and edits workflows needs both. A key without the required scope gets 401 with API key is invalid; see authentication.

On top of the scope, each workflow checks who may do what:

Who May
Owner (the creator, or a member of the owning group) Everything.
User it is shared with for writing Read, edit, run, publish, export, copy and delete it.
User it is shared with for reading Read it, validate it, see its revisions. Run, export or copy it only when the share grants the workflow.run, workflow.export or workflow.copy permission.

A workflow that belongs to an organization runs in that organization (its credits and storage), so the user must be a member with write access there.

Concepts

Definition

The definition of a workflow is a directed acyclic graph: a list of nodes and a list of edges.

json
{
  "nodes": [
    {
      "id": "start",
      "type": "trigger.manual",
      "name": "Start",
      "params": {
        "input_schema": {
          "type": "object",
          "properties": {"customer": {"type": "string"}, "question": {"type": "string"}},
          "required": ["question"]
        }
      },
      "position": {"x": 0, "y": 0}
    },
    {
      "id": "answer",
      "type": "assistant.run",
      "name": "Draft an answer",
      "params": {
        "assistant_id": "3f6c1a2e-8b4d-4e7a-9c1f-2d5e6a7b8c90",
        "prompt": "Answer {{ input.customer }}: {{ input.question }}"
      },
      "position": {"x": 0, "y": 160}
    },
    {
      "id": "result",
      "type": "output",
      "name": "Result",
      "params": {"value": {"answer": "{{ steps.answer.output.text }}"}},
      "position": {"x": 0, "y": 320}
    }
  ],
  "edges": [
    {"source": "start", "target": "answer", "source_handle": null},
    {"source": "answer", "target": "result", "source_handle": null}
  ]
}
Field Type Description
nodes[].id string Identifier of the node, unique in the workflow: letters, digits and _, at most 64 characters, not starting with a digit. Later steps refer to its result as {{ steps.<id>.output }}.
nodes[].type string Node type, see node types.
nodes[].name string Label shown in the editor.
nodes[].params object Parameters of the node type; their JSON schema is in the catalog.
nodes[].position object {x, y} on the editor canvas, or null. Has no effect on runs.
edges[].source string Node the edge leaves.
edges[].target string Node the edge enters.
edges[].source_handle string Output of the source node the edge leaves: null for the default output, a named output (true / false of a condition, a case of a switch, item / done of a loop, approved / rejected / timeout of an approval), or error to continue when the source step fails.

A definition has at most 200 nodes, at least one trigger, no cycles and no edges into a trigger. Use validate to check it.

String parameters are templates (Jinja syntax, evaluated in a sandbox). They can read:

Variable Content
input The run input (the output of the trigger).
steps.<id>.output Result of an earlier step; also steps.<id>.success, .status, .text, .error, .file_ids.
workflow {id, name}.
run {id}.
now Start time of the run (ISO 8601, UTC).
loop Inside a loop body: item, index, count, parent.

A parameter that is a single {{ expression }} keeps the native type of the value (an object stays an object); an undefined variable is an error of the step.

Node types

The full list, with the JSON schema of every node's parameters and the tools a tool.call step can use, comes from the catalog. An overview:

Type Category What it does
trigger.manual trigger Starts a run on demand (app, API). Optional input_schema describes the run input.
trigger.schedule trigger Starts the published workflow on a cron schedule.
trigger.webhook trigger Starts the published workflow when its webhook URL is called.
trigger.booster_record trigger Starts the published workflow when a record of a booster panel database is created, updated or deleted.
tool.call action Calls an AI tool with given parameters.
assistant.run action Runs a task on an assistant in a new conversation.
http.request action Sends an HTTP request to a public URL.
notify action Sends an e-mail to the user the run goes as.
file.to_text action Reads files (documents, images, audio) as text.
human.approval logic Waits for a person to approve or reject, optionally with a form.
condition logic Continues on true or false by an expression.
switch logic Continues on one of several named outputs, or otherwise.
ai.condition logic Asks a classification model a yes/no question; continues on true or false.
ai.choice logic Asks a classification model which of several options fits.
loop logic Runs the steps behind its item output once per item of a list, then continues on done.
transform logic Builds a value from earlier results.
output logic Sets the result of the run. Has no outgoing edges.

Draft and published version

The definition stored on the workflow is the draft. Every change of it is kept as a revision. Runs started through the API (and from the app) run the draft.

Publishing copies the draft into a new immutable version (1, 2, ...) and activates the workflow. While the workflow is active, its automatic triggers (schedule, webhook, booster record) start runs of the published version, as the user who published or last activated it (run_as). Editing the draft afterwards does not change what the triggers run until you publish again.

Run statuses

A run records the input, the state of every step and the result. Its status:

Status Meaning
queued Started by a trigger, or a paused run whose waiting step got its result; waits for a background worker.
running Executing.
waiting Paused: a step waits for something outside the run (a person's approval). Other branches have finished. The run continues in the background once the step gets its result.
success Finished.
failed A step failed without an error edge, a limit was exceeded, or the run could not start.
cancelled Cancelled while waiting or queued.

Steps run as soon as all steps before them are done, so independent branches run in parallel. A step whose incoming edges are all inactive (an untaken branch) is skipped. A failed step with an error edge continues there; without one, the run fails (steps already running finish first).

Runs are deleted after a retention period (90 days by default) together with the conversations and files their steps created. Paused runs are kept until they end.

Approval flow

A human.approval step opens an approval and pauses the run. The people who may decide it are the user the run goes as and members of the workflow's organization named in the step; they are notified in the app and by e-mail. An approval shows a title, a message and optionally a form (a flat JSON schema with prefilled values). The decision (approve with the form data, or reject) becomes the output of the step and the run continues on approved or rejected. An approval nobody decides in time expires: the run continues on timeout when that output is connected, otherwise the step fails. See approvals.

Files in run input

A field of the manual trigger's input_schema declared as {"type": "object", "format": "file"} holds one file. Send it in the run input either inline as base64:

json
{
  "invoice": {
    "filename": "invoice-1042.pdf",
    "mime_type": "application/pdf",
    "data": "data:application/pdf;base64,JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGg..."
  }
}

or as the id of a file the user already has ("invoice": "7b1e2c3d-..." or {"file_id": "7b1e2c3d-..."}). data is plain base64 or a data URL; name may be used instead of filename and content_type instead of mime_type. The upload size limit and the storage quota apply.

In the run, the value becomes the file's attachment metadata, which is also how steps return files:

json
{
  "file_id": "7b1e2c3d-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
  "filename": "invoice-1042.pdf",
  "content_type": "application/pdf",
  "file_url": "https://ayeto.ai/api/v1/public/file/read?id=7b1e2c3d-4a5b-4c6d-8e7f-9a0b1c2d3e4f&secret=...",
  "size": 48213
}

file_url downloads the file. Files sent as base64 belong to the run and are deleted with it; a file named by its id stays the user's and is not deleted with the run. The same base64 form is accepted in the file fields of an approval form and inside webhook bodies.

Limits

The defaults below can differ per deployment.

Limit Default
Nodes per workflow 200
Run input 200,000 characters of JSON
Steps executed per run (every loop item counts) 500
Run time (pauses for approvals do not count) 1,800 seconds
Steps executing at the same time in one run 4
Items per loop 100
Stored output of one step 200,000 characters of JSON (2,000 inside a loop body)
Queued runs per workflow 50
Shortest schedule interval 5 minutes
Approval time to decide 72 hours by default, at most 720
Run retention 90 days
Revisions kept per workflow 100

Workflow object

The workflow returned by the read, create and update endpoints. It also has the common fields (id, created, updated, permissions, ...).

Field Type Description
name string Name.
description string Description.
organization_id UUID Organization the workflow belongs to, null for a personal workflow. Fixed at creation.
definition object The draft definition.
sharing array of object Users it is shared with: {user_email, write, scopes}. scopes may grant read shares {"workflow.run": true, "workflow.export": true, "workflow.copy": true}.
group_sharing array of object Groups it is shared with: {group_id, write, scopes}.
last_run object Latest finished or paused run: {run_id, status, finished_at, credits}, or null. Read-only.
published_version integer Latest published version, null when never published. Read-only.
published_at timestamp When it was last published. Read-only.
active boolean The automatic triggers of the published version are on. Read-only; see activate.
run_as UUID User the automatic runs go as. Read-only.

List and read workflows

Endpoints POST /api/v3/workflow/find<br>POST /api/v3/workflow/count<br>POST /api/v3/workflow/get?entity_id=<workflow id>
Scope ayeto.workflow
Rate limit default

find returns the workflows the user owns or that are shared with them, as an array of workflows, newest first unless sorted otherwise. count returns the number of matching workflows as an integer. Both take the usual query body (filters, sort_key, sort_order, limit_from, limit_to); send {} for everything. Each workflow comes with its full definition, so page through large lists.

get returns one workflow; the id is a query parameter and the body is empty.

Useful filters: ["organization_id", "==", "<organization id>"], ["organization_id", "==", null] (personal), ["active", "==", true], ["name", "regex", "invoice"].

bash
curl -X POST "https://ayeto.ai/api/v3/workflow/find" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filters": [["active", "==", true]], "sort_key": "updated.timestamp", "sort_order": 1, "limit_from": 0, "limit_to": 20}'
bash
curl -X POST "https://ayeto.ai/api/v3/workflow/get?entity_id=5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a" \
  -H "uni-api-key: $AYETO_API_KEY"
json
{
  "id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
  "name": "Customer question",
  "description": "Drafts an answer to a customer question",
  "organization_id": null,
  "definition": {"nodes": ["..."], "edges": ["..."]},
  "sharing": [],
  "group_sharing": [],
  "last_run": {"run_id": "c2a1b0d9-8e7f-4a6b-9c5d-4e3f2a1b0c9d", "status": "success", "finished_at": 1759401234567, "credits": 0.42},
  "published_version": 3,
  "published_at": 1759300000000,
  "active": true,
  "run_as": "0f1e2d3c-4b5a-4968-8776-655443322110",
  "created": {"timestamp": 1759000000000, "user_id": "0f1e2d3c-4b5a-4968-8776-655443322110"},
  "updated": {"timestamp": 1759400000000, "user_id": "0f1e2d3c-4b5a-4968-8776-655443322110"}
}

A workflow that does not exist or is not visible to the user returns 404.

Create a workflow

Endpoint POST /api/v3/workflow/create
Scope ayeto.workflow.write
Rate limit default
Field Type Required Description
name string Yes Name.
description string No Description.
organization_id UUID No Create it in this organization; the user needs write access there. Omit for a personal workflow.
definition object No The definition. Default: a single manual trigger with id start.
sharing array of object No User shares, see the workflow object.
group_sharing array of object No Group shares.

Returns the created workflow. The definition is stored as given, even when it is not valid; validate it before running. Creating a workflow also records its first revision and creates the user's builder assistant.

bash
curl -X POST "https://ayeto.ai/api/v3/workflow/create" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Customer question", "description": "Drafts an answer to a customer question"}'

Update a workflow

Endpoint POST /api/v3/workflow/update
Scope ayeto.workflow.write
Rate limit default
Field Type Required Description
id UUID Yes The workflow.
name string No New name.
description string No New description.
definition object No New draft definition; replaces the whole definition.
sharing array of object No Replaces the user shares.
group_sharing array of object No Replaces the group shares.

Only the fields you send change. The organization, publishing state and last run cannot be changed here. Every change of the definition is recorded as a revision; the published version stays as it was until you publish again. Returns the updated workflow.

To change one node, read the workflow, modify its definition and send it back. Two clients updating the same workflow at once overwrite each other's definition.

Delete a workflow

Endpoint POST /api/v3/workflow/delete?entity_id=<workflow id>
Scope ayeto.workflow.write
Rate limit default

Deletes the workflow with all its runs (and the conversations and files they created), its revisions, published versions, triggers and builder assistants. Returns the deleted id as a JSON string.

Catalog

Endpoint POST /api/v3/workflow/catalog
Scope ayeto.workflow
Rate limit default

Returns what workflows can be built from: every node type with the JSON schema of its parameters, and the tools a tool.call step can call. No request body.

Field Type Description
node_types[].type string Node type, e.g. http.request.
node_types[].title string Display name.
node_types[].description string What the node does and what its output looks like.
node_types[].category string trigger, action or logic.
node_types[].is_trigger boolean The node starts runs.
node_types[].terminal boolean The node ends a branch (no outgoing edges).
node_types[].handles array of string Named outputs; empty means one default output. Every non-trigger node also has the error output. For a switch, the outputs are its case names plus otherwise.
node_types[].params_schema object JSON schema of params.
tools[].name string Tool name for the tool parameter of tool.call.
tools[].display_name string Display name.
tools[].description string Description.
tools[].category string Category.
tools[].icon string Icon name.
tools[].input_schema object JSON schema of the tool's params.

Validate a workflow

Endpoint POST /api/v3/workflow/validate
Scope ayeto.workflow
Rate limit default
Field Type Required Description
workflow_id UUID Yes The workflow.

Validates the draft as the calling user (including whether the user can use the assistants and booster panels it refers to).

json
{
  "valid": false,
  "issues": [
    {"severity": "error", "message": "Node 'answer' has no output 'true' (outputs: default, error)", "node_id": "answer", "edge": {"source": "answer", "target": "result", "source_handle": "true"}},
    {"severity": "warning", "message": "Node 'draft' is not reachable from any trigger and never runs", "node_id": "draft", "edge": null}
  ]
}

valid is false when there is at least one issue with severity error; errors block runs and publishing, warnings do not.

Run a workflow

Endpoint POST /api/v3/workflow/run
Scope ayeto.workflow
Rate limit default

Runs the draft as the calling user and answers when the run has finished or paused.

Request

Field Type Required Description
workflow_id UUID Yes The workflow.
input object No The run input. For a manual trigger it should match its input_schema; keys listed in required must be present. At most 200,000 characters as JSON.
trigger_node_id string No Trigger to start from. Default: the first trigger.manual, otherwise the first trigger of any type.

Starting from a schedule, webhook or booster record trigger runs the draft by hand, which is useful for testing: pass the input the trigger would produce, e.g. {"body": {...}, "query": {}, "headers": {}} for a webhook trigger.

The user needs run access to the workflow (see scopes and access). Before the run starts, the draft is validated; an invalid draft is rejected with 422. A missing required input key does not reject the request: the run is created and fails at its trigger step (Missing input: <keys>).

The request returns when the run ends, which may take minutes (up to the run time limit). For long runs or live progress, use streaming. When a step waits for an approval, the response comes back with status: "waiting"; follow the run with get run.

Response

200 OK with the run.

Examples

bash
curl -X POST "https://ayeto.ai/api/v3/workflow/run" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workflow_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
    "input": {"customer": "Jana Novak", "question": "When will order 1042 be delivered?"}
  }'
python
import json
import os
import requests

response = requests.post(
    "https://ayeto.ai/api/v3/workflow/run",
    headers={"uni-api-key": os.environ["AYETO_API_KEY"]},
    json={
        "workflow_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
        "input": {"customer": "Jana Novak", "question": "When will order 1042 be delivered?"},
    },
    timeout=1900,
)
response.raise_for_status()
run = response.json()
if run["status"] == "success":
    print(json.loads(run["output_json"]) if run["output_json"] else None)
else:
    print(run["status"], run["error"])
javascript
const response = await fetch("https://ayeto.ai/api/v3/workflow/run", {
  method: "POST",
  headers: {
    "uni-api-key": process.env.AYETO_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    workflow_id: "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
    input: { customer: "Jana Novak", question: "When will order 1042 be delivered?" },
  }),
});
if (!response.ok) throw new Error((await response.json()).detail);
const run = await response.json();
const output = run.output_json ? JSON.parse(run.output_json) : null;
console.log(run.status, output ?? run.error);

Run object

A run has the common fields plus:

Field Type Description
workflow_id UUID The workflow.
organization_id UUID Organization the run went in, or null.
version integer Published version the run executed; null for a run of the draft.
trigger_node_id string Trigger the run started from.
trigger_type string Its node type, e.g. trigger.manual, trigger.webhook.
trigger_depth integer Number of booster record triggered runs in a chain before this one.
automatic boolean Started by a trigger (schedule, webhook, booster record), not by a person.
input_json string The run input as JSON text.
status string See run statuses.
started_at timestamp When the run started executing; null while queued.
active_since timestamp Start of the current execution (after a pause, the resume).
finished_at timestamp When it ended, or null.
steps array of step State of every step that started or was skipped, in execution order.
output_json string The result (value of the last output step reached) as JSON text; empty when there is none.
error string Why the run failed.
credits number Credits the run spent so far.

created.user_id is the user the run went as.

Step object

Field Type Description
node_id string The node.
node_type string Its type.
status string running, waiting, success, failed or skipped.
started_at timestamp Start, null for a skipped step.
finished_at timestamp End.
handle string Output the run continued on: null (default), a named output, or error.
output_json string Output of the step as JSON text, capped at 200,000 characters (2,000 inside a loop body).
output_truncated boolean The output was cut at the cap; output_json is then not valid JSON.
text string Human-readable result or status text.
error string Why the step failed.
file_ids array of UUID Files the step produced.
conversation_id UUID Conversation an assistant.run step created; readable through conversations.
iteration array of integer For a step inside a loop body, the item indexes from the outermost loop in; empty otherwise. A step inside a loop appears once per item.

Example:

json
{
  "id": "c2a1b0d9-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
  "workflow_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
  "organization_id": null,
  "version": null,
  "trigger_node_id": "start",
  "trigger_type": "trigger.manual",
  "trigger_depth": 0,
  "automatic": false,
  "input_json": "{\"customer\": \"Jana Novak\", \"question\": \"When will order 1042 be delivered?\"}",
  "status": "success",
  "started_at": 1759401220011,
  "active_since": 1759401220011,
  "finished_at": 1759401234567,
  "steps": [
    {
      "node_id": "start", "node_type": "trigger.manual", "status": "success",
      "started_at": 1759401220020, "finished_at": 1759401220031, "handle": null,
      "output_json": "{\"customer\": \"Jana Novak\", \"question\": \"When will order 1042 be delivered?\"}",
      "output_truncated": false, "text": "", "error": "", "file_ids": [], "conversation_id": null, "iteration": []
    },
    {
      "node_id": "answer", "node_type": "assistant.run", "status": "success",
      "started_at": 1759401220040, "finished_at": 1759401234400, "handle": null,
      "output_json": "{\"text\": \"Dear Jana, order 1042 ships on Friday ...\", \"data\": null, \"files\": []}",
      "output_truncated": false, "text": "Dear Jana, order 1042 ships on Friday ...", "error": "",
      "file_ids": [], "conversation_id": "8e2f4a6c-1b3d-4e5f-8a7b-9c0d1e2f3a4b", "iteration": []
    },
    {
      "node_id": "result", "node_type": "output", "status": "success",
      "started_at": 1759401234410, "finished_at": 1759401234420, "handle": null,
      "output_json": "{\"answer\": \"Dear Jana, order 1042 ships on Friday ...\"}",
      "output_truncated": false, "text": "", "error": "", "file_ids": [], "conversation_id": null, "iteration": []
    }
  ],
  "output_json": "{\"answer\": \"Dear Jana, order 1042 ships on Friday ...\"}",
  "error": "",
  "credits": 0.42,
  "created": {"timestamp": 1759401220000, "user_id": "0f1e2d3c-4b5a-4968-8776-655443322110"}
}

Stream a run

Endpoint POST /api/v3/workflow/run/stream
Scope ayeto.workflow
Rate limit default

Same request as run a workflow, but the response streams the events of the run as they happen. The run executes on the server to its end even when the client disconnects; read the result afterwards with get run.

Access and validation errors (no access, invalid draft, input too large) are returned as a normal HTTP error before the stream starts.

Wire format

The response (Content-Type: text/event-stream) is a sequence of JSON objects written one after another without separators (not SSE data: lines); see streaming. Each object is a chunk:

json
{"timestamp": 1759401220012, "data": {"type": "run_start", "run_id": "c2a1b0d9-8e7f-4a6b-9c5d-4e3f2a1b0c9d", "node_id": null, "node_type": null, "status": "running", "handle": null, "text": "", "error": "", "output_json": "", "iteration": []}, "error": null}
  • data is a run event (below).
  • data: "" is a keep-alive chunk, sent about every second while nothing happens. Ignore it.
  • A chunk with error set ({"status", "text", "detail"}) reports an error that ended the stream.

Events

Every event has the fields type, run_id, node_id, node_type, status, handle, text, error, output_json and iteration; which of them carry a value depends on the type.

type Meaning Fields with values
run_start The run started. status: "running"
step_start A step started. node_id, node_type, status: "running", iteration
step_progress Progress text of a running step (a tool or an assistant working). node_id, node_type, text, iteration
step_waiting A step waits (an approval). Other branches go on. node_id, node_type, status: "waiting", text, output_json (for an approval {approval_id, expires_at, assignees})
step_end A step ended or was skipped. node_id, node_type, status (success, failed, skipped), handle, text, error, output_json, iteration
run_waiting The run paused; the stream ends without run_end. status: "waiting", text (ids of the waiting steps, comma-separated)
run_end The run ended; the last event. status (success, failed), error, output_json

Events of parallel branches and loop items interleave; use node_id and iteration to tell them apart.

Examples

bash
curl -N -X POST "https://ayeto.ai/api/v3/workflow/run/stream" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"workflow_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "input": {"question": "When will order 1042 be delivered?"}}'

Python, splitting the stream into JSON objects with raw_decode:

python
import json
import os
import requests

decoder = json.JSONDecoder()

def events(response):
    """Yield the run events of a streamed response."""
    buffer = ""
    for piece in response.iter_content(chunk_size=None, decode_unicode=True):
        buffer += piece
        while True:
            buffer = buffer.lstrip()
            if not buffer:
                break
            try:
                chunk, end = decoder.raw_decode(buffer)
            except json.JSONDecodeError:
                break  # incomplete object, wait for more data
            buffer = buffer[end:]
            if chunk.get("error"):
                raise RuntimeError(chunk["error"]["detail"])
            if chunk.get("data"):
                yield chunk["data"]

with requests.post(
    "https://ayeto.ai/api/v3/workflow/run/stream",
    headers={"uni-api-key": os.environ["AYETO_API_KEY"]},
    json={"workflow_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "input": {"question": "When will order 1042 be delivered?"}},
    stream=True,
    timeout=(10, 120),
) as response:
    response.raise_for_status()
    response.encoding = "utf-8"
    for event in events(response):
        if event["type"] == "step_end":
            print(f"{event['node_id']}: {event['status']} {event['error']}")
        elif event["type"] == "run_waiting":
            print("paused, waiting for:", event["text"])
        elif event["type"] == "run_end":
            print("run", event["status"], event["output_json"] or event["error"])

JavaScript, splitting the stream by tracking braces outside strings:

javascript
async function* runEvents(response) {
  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) return;
    buffer += value;
    let depth = 0, inString = false, escaped = false, start = -1, consumed = 0;
    for (let i = 0; i < buffer.length; i++) {
      const c = buffer[i];
      if (inString) {
        if (escaped) escaped = false;
        else if (c === "\\") escaped = true;
        else if (c === '"') inString = false;
      } else if (c === '"') inString = true;
      else if (c === "{") { if (depth++ === 0) start = i; }
      else if (c === "}" && --depth === 0) {
        const chunk = JSON.parse(buffer.slice(start, i + 1));
        consumed = i + 1;
        if (chunk.error) throw new Error(chunk.error.detail);
        if (chunk.data) yield chunk.data;
      }
    }
    buffer = buffer.slice(consumed);
  }
}

const response = await fetch("https://ayeto.ai/api/v3/workflow/run/stream", {
  method: "POST",
  headers: { "uni-api-key": process.env.AYETO_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    workflow_id: "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
    input: { question: "When will order 1042 be delivered?" },
  }),
});
if (!response.ok) throw new Error((await response.json()).detail);
for await (const event of runEvents(response)) {
  if (event.type === "step_start") console.log("running", event.node_id);
  if (event.type === "step_end") console.log(event.node_id, event.status, event.error);
  if (event.type === "run_waiting") console.log("paused, waiting for", event.text);
  if (event.type === "run_end") console.log("done", event.status, event.output_json || event.error);
}

Cancel a run

Endpoint POST /api/v3/workflow/run/cancel
Scope ayeto.workflow
Rate limit default
Field Type Required Description
run_id UUID Yes The run.

Cancels a run that is waiting or queued. Its waiting steps fail and their pending approvals are cancelled. The user the run went as and users with write access to the workflow may cancel it. A run that is running cannot be cancelled (422); it ends on its own or at the run time limit. Returns the cancelled run.

Runs

Endpoints POST /api/v3/workflow/runs/find<br>POST /api/v3/workflow/runs/count<br>POST /api/v3/workflow/runs/get?entity_id=<run id>
Scope ayeto.workflow
Rate limit default
Endpoint POST /api/v3/workflow/runs/delete?entity_id=<run id>
Scope ayeto.workflow.write
Rate limit default

These endpoints see the runs that went as the API key's user: runs the user started, and automatic runs of workflows the user published or activated. Runs of other users are reported as not found.

find returns an array of runs and count an integer; both take the query body. Runs include all their steps and outputs, so always page. get returns one run (empty body). delete deletes a run with the conversations and files its steps created and its approvals, and returns its id; deleting a paused run ends it.

Useful filters:

Goal Filter
Runs of one workflow ["workflow_id", "==", "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a"]
Paused runs ["status", "==", "waiting"]
Failed runs ["status", "==", "failed"]
Runs from the webhook ["trigger_type", "==", "trigger.webhook"]
Runs of the published version only ["version", "!=", null]
Started since a moment ["created.timestamp", ">=", 1759363200000]
bash
curl -X POST "https://ayeto.ai/api/v3/workflow/runs/find" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filters": [["workflow_id", "==", "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a"]], "sort_key": "created.timestamp", "sort_order": 1, "limit_from": 0, "limit_to": 10}'

To follow a paused or queued run, poll get until its status is success, failed or cancelled.

Approvals

The approval endpoints serve the people who decide: any user named by an approval step can use them, without other permissions on the workflow. The API key still needs the ayeto.workflow scope.

Endpoints POST /api/v3/workflow/approval/find<br>POST /api/v3/workflow/approval/get<br>POST /api/v3/workflow/approval/decide
Scope ayeto.workflow
Rate limit default
Endpoint POST /api/v3/workflow/approval/count
Scope ayeto.workflow
Rate limit high

Approval object

Common fields plus:

Field Type Description
workflow_id UUID The workflow.
workflow_name string Its name when the approval was opened.
run_id UUID The paused run.
node_id string The approval step.
organization_id UUID Organization of the workflow, or null.
status string pending, approved, rejected, expired (nobody decided in time) or cancelled (the run ended first).
title string What to decide.
message string Details (Markdown).
form_schema object JSON schema of the form ({"type": "object", "properties": {...}, "required": [...]}) with flat fields of type string, number, integer, boolean, or a file ({"type": "object", "format": "file"}); null when there is no form.
form_values object Prefilled values by field name.
allow_reject boolean The approval can be rejected. When false, the step only collects the form.
assignee_user_ids array of UUID Users who may decide.
expires_at timestamp Deadline.
timeout_continues boolean On expiry the run continues on timeout; otherwise the step fails.
decision_data object The form data of the decision.
comment string Comment of the decision.
decided_by object {id, email, name} of the person who decided, or null.
decided_at timestamp When it was decided or expired.

List approvals

Request:

Field Type Required Description
status string No Only approvals with this status, e.g. pending.
limit_from integer No Index of the first item. Default 0.
limit_to integer No Index after the last item, 1 to 200. Default 50.

Returns {"items": [<approval>, ...], "total": <number>} with the approvals the user may decide (or decided), newest first. total is the number of all matching approvals.

Count pending approvals

No request body. Returns {"pending": 3}, the number of approvals waiting for the user's decision. Suited for polling a badge.

Get an approval

Field Type Required Description
approval_id UUID Yes The approval.

Returns the approval. Users who may decide it and users who can read its workflow may get it.

Decide an approval

Field Type Required Description
approval_id UUID Yes The approval.
decision string Yes approve or reject.
data object No Form data (approve only). Fields you do not send keep their prefilled values; fields not in the form are dropped. At most 20,000 characters as JSON. File fields take the base64 form described in files in run input.
comment string No Comment, at most 2,000 characters.

Only users in assignee_user_ids may decide, and only while the approval is pending and before expires_at. The form data is validated against form_schema. The decision becomes the output of the approval step:

json
{
  "decision": "approved",
  "data": {"amount": 1200, "note": "OK for this quarter"},
  "comment": "Approved",
  "decided_by": {"id": "0f1e2d3c-4b5a-4968-8776-655443322110", "email": "jana@example.com", "name": "Jana Novak"},
  "decided_at": 1759405000000,
  "approval_id": "e4d3c2b1-a0f9-4e8d-9c7b-6a5f4e3d2c1b"
}

The run continues in the background as the user it went as (not as the decider); its status changes from waiting to queued and then running. Returns the decided approval.

bash
curl -X POST "https://ayeto.ai/api/v3/workflow/approval/decide" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "approval_id": "e4d3c2b1-a0f9-4e8d-9c7b-6a5f4e3d2c1b",
    "decision": "approve",
    "data": {"amount": 1200, "note": "OK for this quarter"},
    "comment": "Approved"
  }'

Publishing and triggers

These endpoints need write access to the workflow.

Deployment info

Endpoint POST /api/v3/workflow/deployment
Scope ayeto.workflow.write
Rate limit default
Field Type Required Description
workflow_id UUID Yes The workflow.

Returns the publishing state. It contains the webhook URLs with their secrets, which is why it needs the write scope.

Field Type Description
published_version integer Latest published version, or null.
published_at timestamp When it was published.
active boolean Automatic triggers are on.
run_as UUID User the automatic runs go as.
draft_changed boolean The draft differs from the published version (node positions aside); always true when nothing is published.
triggers array of object Automatic triggers of the published version, see below.
signing_secret string Key that signs requests of http.request steps with sign: true; empty when no step signs.

Trigger fields:

Field Type Description
node_id string The trigger node.
type string trigger.schedule, trigger.webhook or trigger.booster_record.
active boolean The trigger fires.
next_run_at timestamp Next scheduled time (schedule triggers).
last_fired_at timestamp When it last started a run.
url string Webhook URL including its secret (webhook triggers).
panel_id UUID Booster panel (booster record triggers).
collection string Watched collection, * for all (booster record triggers).

A signed http.request sends the header X-Ayeto-Signature: sha256=<hex HMAC-SHA256 of the request body> computed with signing_secret, so the receiving service can verify it.

Publish a workflow

Endpoint POST /api/v3/workflow/publish
Scope ayeto.workflow.write
Rate limit default
Field Type Required Description
workflow_id UUID Yes The workflow.
note string No Note of the version, at most 500 characters.

Validates the draft as the caller, stores it as the next version, activates the workflow and makes the caller the run_as user of automatic runs. Webhook URLs of trigger nodes that existed before stay the same. Returns the deployment info. An invalid draft is rejected with 422.

Activate or deactivate

Endpoint POST /api/v3/workflow/activate
Scope ayeto.workflow.write
Rate limit default
Field Type Required Description
workflow_id UUID Yes The workflow.
active boolean Yes true turns the automatic triggers on, false off.

Activating validates the published version for the caller and makes the caller the run_as user. While inactive, webhook calls return 404 and schedules do not fire. Returns the deployment info. Fails with 422 when nothing is published.

Regenerate a webhook secret

Endpoint POST /api/v3/workflow/trigger/regenerate
Scope ayeto.workflow.write
Rate limit low
Field Type Required Description
workflow_id UUID Yes The workflow.
node_id string Yes The webhook trigger node of the published version.

Gives the webhook a new secret; the old URL stops working at once. Returns the deployment info with the new URL.

Preview a schedule

Endpoint POST /api/v3/workflow/schedule/preview
Scope ayeto.workflow
Rate limit high
Field Type Required Description
cron string Yes Cron expression with five fields: minute, hour, day of month, month, day of week (e.g. 0 8 * * 1-5). At most 200 characters.
timezone string No IANA time zone, e.g. Europe/Prague. Default UTC.

Checks a schedule for a trigger.schedule node and lists its next times:

json
{"valid": true, "error": "", "next_runs": [1759471200000, 1759730400000, 1759816800000, 1759903200000, 1759989600000]}

An invalid schedule returns 200 with valid: false and the reason in error (wrong number of fields, unknown time zone, runs more often than the shortest allowed interval).

Builder assistant

Endpoint POST /api/v3/workflow/assistant
Scope ayeto.workflow.write
Rate limit default
Field Type Required Description
workflow_id UUID Yes The workflow.

Every user with write access has their own builder assistant for a workflow: an assistant that reads, edits, test-runs and explains the workflow in conversation. This endpoint returns the caller's builder (creating it when missing) as an assistant. Talk to it through chat with its id as the assistant; that needs a key with the chat scope. Its edits are recorded as revisions with origin builder.

Revisions

Every change of the definition is a revision. Changes that only move nodes, made by the same user within three minutes, are merged into the latest revision. The newest revisions are kept (100 by default).

List revisions

Endpoint POST /api/v3/workflow/revisions
Scope ayeto.workflow
Rate limit default
Field Type Required Description
workflow_id UUID Yes The workflow.

Returns up to 100 revisions, newest first, without their definitions:

Field Type Description
id UUID The revision.
created_at timestamp When it was made.
user_id UUID Who made it.
origin string created, editor (the app editor or this API), builder (the builder assistant) or restore.
changes object {added_nodes, removed_nodes, changed_nodes, added_edges, removed_edges}: node id lists and edge counts against the previous revision.
restored_from UUID For a restore, the revision that was put back.
nodes integer Number of nodes.
edges integer Number of edges.

Get a revision

Endpoint POST /api/v3/workflow/revision/get
Scope ayeto.workflow
Rate limit default
Field Type Required Description
workflow_id UUID Yes The workflow.
revision_id UUID Yes The revision.

Returns the revision with its full definition: the common fields plus workflow_id, definition, origin, changes and restored_from.

Restore a revision

Endpoint POST /api/v3/workflow/revision/restore
Scope ayeto.workflow.write
Rate limit default

Same request as get a revision. Puts the revision's definition back as the draft; the restore is recorded as a new revision (origin restore), so it can be undone. The published version does not change. Returns the updated workflow.

Export and import

An export is a JSON document with the draft definition, name and description. Runs, revisions, published versions, trigger state (webhook secrets), the signing secret, the organization and sharing are not exported. Assistants that steps refer to are exported as ids only.

Export a workflow

Endpoint POST /api/v3/workflow/export
Scope ayeto.workflow
Rate limit default
Field Type Required Description
workflow_id UUID Yes The workflow.
strip_secrets boolean No Ignored on this API: credentials are always removed.

Through an API key, the values of http.request step headers whose name looks like a credential (contains auth, token, secret, key, password, cookie, signature or session) are always emptied; the header names stay. The emptied paths are listed in secret_keys. The user needs write access, or a read share with the workflow.export permission.

json
{
  "version": "1.0",
  "exported_at": 1759405000000,
  "workflow": {
    "name": "Customer question",
    "description": "Drafts an answer to a customer question",
    "definition": {"nodes": ["..."], "edges": ["..."]},
    "from_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
    "secret_keys": ["notify_crm.headers.Authorization"]
  }
}

Import a workflow

Endpoint POST /api/v3/workflow/import
Scope ayeto.workflow.write
Rate limit default
Field Type Required Description
data object Yes An export document, as returned by export.
new_name string No Name of the new workflow, at most 200 characters. Default: the exported name.
organization_id UUID No Create it in this organization (write access needed). Default: personal.

Creates a new, unpublished workflow of the caller and validates it. Steps that refer to things the caller cannot use (an assistant from another account, emptied credentials) do not block the import; they are reported so they can be fixed.

Field Type Description
workflow object The new workflow.
issues array of object Validation issues for the caller, as in validate.
secret_keys array of string Header values to fill in (<node id>.headers.<name>).

The export format must have the same major version (1), and the definition at most 200 nodes.

Copy a workflow

Endpoint POST /api/v3/workflow/copy
Scope ayeto.workflow.write
Rate limit default
Field Type Required Description
workflow_id UUID Yes The workflow to copy.
new_name string No Name of the copy. Default: the original name.
organization_id UUID No Organization of the copy. Default: personal.

Exports the draft and imports it as a new workflow of the caller in one call; the response is the same as for import. The user needs write access or a read share with the workflow.copy permission. A user with write access to the original keeps the credentials of HTTP steps; a copy made through the workflow.copy permission leaves them out.

Webhooks

Endpoint POST /api/v3/workflow/hook/{trigger_id}/{secret}
Scope none: the secret in the URL authorizes the call
Rate limit webhook per IP, plus a limit per trigger (below)

A trigger.webhook node of a published, active workflow has a public URL. Get it from triggers[].url of the deployment info:

https://ayeto.ai/api/v3/workflow/hook/9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/Xq3v...secret...

Treat the URL as a secret; anyone who has it can start runs that spend the credits of the run_as user. If it leaks, regenerate it.

Request

Send a POST with a JSON body and Content-Type: application/json. A body of another content type is passed on as text (or parsed, when it is JSON). No API key is needed.

The run input (the output of the trigger, available to steps as {{ input }}) is:

Field Type Description
body any The request body; {} when empty.
query object Query string parameters of the URL.
headers object Only these request headers, when present: content-type, user-agent, x-github-event, x-request-id.

Steps read it as {{ input.body.order_id }}, {{ input.query.source }} and so on. The input is limited to 200,000 characters of JSON.

Files in the body

Files can be sent inside the body, at any depth (up to 32 levels):

  • an object with base64 data and a name or filename, and no keys other than name, filename, content_type, mime_type, data, size;
  • a data URL string data:<type>;base64,....

Each file is stored as a file of the run (owned by the run_as user, subject to the upload size limit and storage quota) and replaced in the body by its attachment metadata ({file_id, filename, content_type, file_url, size}). At most 10 files per call.

Response

200 OK as soon as the run is queued; the run executes in the background:

json
{"run_id": "b7c6d5e4-f3a2-4b1c-9d8e-7f6a5b4c3d2e", "status": "queued"}

The run executes the published version as the run_as user, who sees it through runs (trigger_type is trigger.webhook, automatic is true). Its input is not trusted: a step reads the user's files only when the step names them itself, never because the webhook body names them.

Rate limiting

Each trigger accepts at most 60 calls per minute, 2,000 per hour and 20,000 per day, from any number of senders. A per-IP flood limit applies on top. A workflow also refuses new runs while it has 50 runs queued (by default). Both answer 429; a rate limit response carries Retry-After (seconds).

Example

bash
curl -X POST "https://ayeto.ai/api/v3/workflow/hook/9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/$WEBHOOK_SECRET?source=shop" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": 1042,
    "customer": {"name": "Jana Novak", "email": "jana@example.com"},
    "invoice": {"filename": "invoice-1042.pdf", "content_type": "application/pdf", "data": "JVBERi0xLjcKJcfsj6IK..."}
  }'

Errors

Errors have the JSON body {"detail": "..."}. Authentication, rate limit and server errors are described in conventions.

Status Meaning
401 The API key is missing, invalid or lacks the required scope (API key is invalid).
403 The user has no access to the workflow, or lacks the permission or organization membership the operation needs.
404 The workflow, run, revision, approval or webhook does not exist or is not visible to the user.
422 The request is invalid, or the operation is not possible in the current state (detail says why).
423 The workflow or run is being changed by another request; retry shortly.
429 Too many requests (with Retry-After), or too many queued runs.
500 Server error.

Workflow-specific errors:

Status detail Cause
403 permission denied The user's account is not allowed to read or write workflows or runs.
403 You do not have access to this workflow Not the owner and not shared with the user (or shared for reading where writing is needed).
403 This workflow is shared with you without the 'workflow.run' permission Read share without the permission to run (workflow.export, workflow.copy likewise).
403 User is not a member of the organization The workflow belongs to an organization the user is not in.
403 User does not have write permissions in the organization The user cannot work in the workflow's organization.
404 Workflow not found The workflow does not exist (or was removed).
404 entity not found CRUD get, update, delete of a workflow or run that does not exist or is not the user's.
404 Workflow run not found Unknown run, or a run the user may not cancel.
404 Revision not found The revision does not exist or belongs to another workflow.
404 Approval not found Unknown approval, or the user may not see or decide it.
404 The file of the input '<key>' was not found or is not accessible A file field names a file the user does not own.
404 The published workflow has no such webhook trigger Regenerate with a node that is not a webhook trigger of the published version.
404 Webhook not found Unknown trigger id or wrong secret.
404 The workflow of this webhook is not active The workflow is deactivated or not published, or it was removed.
422 The workflow is not valid: <errors> Run, publish or activate of a definition with validation errors.
422 '<id>' is not a trigger of the workflow trigger_node_id is not a trigger.
422 The workflow has no trigger No trigger to start from.
422 The run input is too large (at most 200000 characters of JSON) Run input or webhook input over the limit.
422 The input '<key>' must be one file A file field holds several files or something else.
422 The file is not valid base64 data Inline file with invalid base64.
422 Too many files (at most 10) Too many files in a webhook body.
422 A <status> run can not be cancelled Cancel of a run that is not waiting or queued.
422 Publish the workflow first Activate without a published version.
422 Unsupported workflow export format <version> Import of an export with another major version.
422 The workflow has too many steps (<n>, at most 200) Import of a definition over the node limit.
422 The approval is <status> already Decide on an approval that is no longer pending.
422 The time to decide the approval is over Decide after expires_at.
422 The approval can not be rejected reject when allow_reject is false.
422 Step '<node id>' of the run does not wait The run stopped waiting (cancelled, failed) before the decision.
422 The form data is too large (at most 20000 characters of JSON) Decision data over the limit.
429 Too many requests Rate limit, including the per-trigger webhook limit; see Retry-After.
429 The workflow already has <n> runs waiting Webhook call while the queue of the workflow is full.

A step failure is not an HTTP error: the run comes back with status: "failed" and the reason in error and in the failing step's error.