Workflow API
A workflow is the DAG of typed nodes that powers an AI Employee. The Workflow API is where you execute that DAG: start a run, follow it in real time, list and inspect past runs, respond to a human-in-the-loop (HITL) pause, and pull a step-by-step trace. The workflow service is mounted at /api/v1/workflow, so every path below is that prefix plus the path shown.
All requests require a JWT or tenant API key. Examples use https://your-tenant.ema.ai as the host.
Where CRUD lives. Creating, editing, publishing, and versioning a workflow are covered on the AI Employee API page, because a workflow is the executable core of an AI Employee. This page is about running workflows and working with the runs they produce.
Start a run
POST /api/v1/workflow/workflows/{id}/run starts an asynchronous run of the workflow's published version. The body carries the run's inputs.
POST https://your-tenant.ema.ai/api/v1/workflow/workflows/3f7a.../run
Authorization: Bearer eyJhbGciOiJSUzI1Ni...
Content-Type: application/json
{
"input_params": {
"ticket_subject": "Cannot reset my password",
"ticket_body": "I keep getting an error on the reset page."
}
}
input_params is an object whose keys match the workflow's start-node input schema. Optionally include a session_id to group the run under a chat session. The response is 202 Accepted with the run:
{
"id": "9b2c...",
"workflow_id": "3f7a...",
"workflow_name": "Support Triage",
"version": 4,
"status": "pending",
"trigger_type": "ui",
"is_dry_run": false,
"created_at": "2026-06-01T12:00:00Z"
}
The run proceeds in the background. status moves through pending → running → a terminal state (completed, failed, cancelled) or paused (waiting on a human). trigger_type records the credential that started the run — api when the call carries a tenant API key, and ui for a JWT-authenticated call like the one above; see Triggering AI Employees via API for the other sources. Follow the run with the SSE stream or poll run details.
Dry run
POST /api/v1/workflow/workflows/{id}/dry-run takes the same body and returns the same RunResponse, but runs without side effects. Tools are not actually invoked and nothing is persisted as a real run. Use it to validate a workflow end-to-end before going live.
Stream a run in real time
GET /api/v1/workflow/runs/{run_id}/stream opens a Server-Sent Events (SSE) stream of the run's progress. Each event is a data: line carrying a JSON object describing what happened: a step starting, a token of streamed output, an agent completing, or the run reaching a terminal or paused state.
GET https://your-tenant.ema.ai/api/v1/workflow/runs/9b2c.../stream
Authorization: Bearer eyJhbGciOiJSUzI1Ni...
Accept: text/event-stream
data: {"type":"step_started","run_id":"9b2c...","step_id":"...","node_id":"classify"}
data: {"type":"step_completed","run_id":"9b2c...","step_id":"...","node_id":"classify"}
data: {"type":"run_completed","run_id":"9b2c...","status":"completed"}
Consume events until the run reaches run_completed, run_failed, or run_paused. The server cancels its publisher subscription cleanly when your client disconnects.
External AI Employee visitors stream through the chat service. For an external web AI Employee visitor, the equivalent SSE stream is proxied at GET /api/v1/chat/runs/{run_id}/stream so the visitor's token never reaches the workflow service directly. See the Chat API.
Durable async tool snapshots
For long-running async Tool calls, GET /api/v1/workflow/runs/{run_id}/external-tool-snapshots returns every external tool call attached to the run, both in-progress and terminal. The chat UI calls this on mount to hydrate tool messages that started before the client connected to the stream.
Inspect a run
- Run details:
GET /api/v1/workflow/runs/{run_id}returns aDetailedRunResponse— the run plus a per-modeltoken_usagebreakdown andoutput/named_outputs. - Steps:
GET /api/v1/workflow/runs/{run_id}/stepsreturns the ordered list of steps the run executed. - List a workflow's runs:
GET /api/v1/workflow/workflows/{id}/runs— paginated, with optional filters:trigger_type—ui,api,schedule,webhook,feedback,document,app_public, oremail.status—pending,running,paused,completed,failed,cancelled, orskipped.since— an RFC 3339 lower bound (inclusive) oncreated_at.
- List recent runs across all workflows:
GET /api/v1/workflow/runs— the dashboard view, paginated withlimitandoffset. Chat runs are grouped by session.
GET https://your-tenant.ema.ai/api/v1/workflow/workflows/3f7a.../runs?status=failed&trigger_type=schedule
Authorization: Bearer eyJhbGciOiJSUzI1Ni...
Cancel a run
POST /api/v1/workflow/runs/{id}/cancel aborts a pending, running, or paused run and cancels every pending HITL request on it. This is an admin-only operation. The response reports how many HITL requests were cancelled:
{ "id": "9b2c...", "status": "cancelled", "hitl_cancelled_count": 1 }
Already-terminal runs (completed, failed, cancelled) return 409 Conflict.
Human-in-the-loop (HITL)
When a workflow node needs a person — to approve a step, choose an option, or fill a form — the run pauses and a HITL request is created. The run resumes once someone responds.
- List pending requests:
GET /api/v1/workflow/hitl/pending(paginated). - Get one:
GET /api/v1/workflow/hitl/{request_id}. - Respond:
POST /api/v1/workflow/hitl/{request_id}/respond. - Claim:
POST /api/v1/workflow/hitl/{request_id}/claim— assign the request to yourself before responding. - Cancel:
POST /api/v1/workflow/hitl/{request_id}/cancel.
The respond body wraps a response whose shape depends on the request type. For an ask_human request the response is a discriminated choice or freetext variant:
POST https://your-tenant.ema.ai/api/v1/workflow/hitl/7d1e.../respond
Authorization: Bearer eyJhbGciOiJSUzI1Ni...
Content-Type: application/json
{
"response": {
"type": "choice",
"value": "approve"
}
}
For freetext, send { "type": "freetext", "value": "..." }. Legacy shapes are also accepted: { "approved": true, "comment": "..." } for an approval, and { "form_data": { ... } } for a form. On success the run resumes and the endpoint returns the updated HITL request. A malformed response — unknown type, missing value, or a value that matches no offered choice — returns 400.
Traces
For deep debugging, the workflow service can export an OpenTelemetry (OTLP) worklog reconstructed from a run's persisted state.
- One run:
GET /api/v1/workflow/runs/{run_id}/tracereturns a JSON envelope{ "traces": ..., "logs": ... }covering spans for each step, LLM call, and Tool call, with HITL pause/resume boundaries as span events. - A workflow over a time range:
GET /api/v1/workflow/workflows/{id}/runs/trace?start_time=...&end_time=...returns merged worklogs for every run started in the window, newest-first and cursor-paginated (page_token/next_page_token).
Detail is filtered by role: env_admin / system_admin see full detail including instruction bodies; builder_admin sees model, tokens, tool results, and errors but not instructions; other roles see only node names, status, timing, and sanitized error markers. Both endpoints are rate-limited to 60 requests per minute per workspace, and payloads are capped at 10 MiB (the single-run endpoint returns 413 if the trace exceeds the cap even after truncation).
Schedules
A workflow can run on a recurring schedule. The schedule endpoints live under the same prefix:
- Create or update:
PUT /api/v1/workflow/workflows/{id}/schedule. - Enable / disable:
POST /api/v1/workflow/workflows/{id}/schedule/enableand.../disable. Enabling requires a published version (otherwise409). - Delete:
DELETE /api/v1/workflow/workflows/{id}/schedule.
Scheduled runs appear in run history with trigger_type=schedule. See Triggering AI Employees via API for how schedules fit alongside the other trigger sources.
API schemas
DAGNode types
A DAGNode in the workflow definition carries a type field that determines which config block is present. The valid values are:
| Type | Config field | Description |
|---|---|---|
agent | agent_config | An AI agent step (intent classification, search & respond, extraction, rule validation, or custom). |
call_aie | call_aie_config | Invokes another published AI Employee as a child run. |
send_email | send_email_config | Sends an email via a configured integration. |
notify_user | notify_user_config | Sends, updates, or closes a nudge card (Announce or Collect contract). |
live_agent_transfer | live_agent_transfer_config | Transfers the conversation to a human agent queue. |
schedule | schedule_config | Starts or stops a workflow-managed cron schedule on a dashboard AI Employee. start creates an enabled schedule; stop ends one. The two operations carry different field sets. |
DAGValidationWarning codes
Publish and save responses include a warnings array of DagValidationWarning objects. Each warning has a code, severity, message, and node_id. Advisory warnings (severity: warning) do not block publish; blocking warnings (severity: error) do.
The full set of codes is returned by the OpenAPI spec. The table below covers codes a builder is most likely to act on:
| Code | Severity | Meaning and remedy |
|---|---|---|
NUDGE_PARK_NO_DOWNSTREAM_REREAD | warning | A park-mode nudge node has no agent, call_aie, or tool-bearing node downstream. The run will resume on pre-park values. Move the volatile read below the nudge node, or point it at another AI Employee. |
NUDGE_COLLECT_NEVER_ENDS | warning | A notify_user node in trigger-mode Collect (it names a trigger_workflow_id) carries no expires_after_seconds, no reminders cadence, and has no close node reachable downstream. The nudge will stay live with tappable cards until a person acts. Fix by setting an expiry or adding a close node below it. Park mode is exempt — a park with no positive expires_after_seconds is refused at publish. |
What's next
- Triggering AI Employees via API — every way a run can start and what
trigger_typeeach produces. - Chat API — drive a chat workflow turn-by-turn instead of one run at a time.
- AI Employee API — build, publish, and version the workflow you're running.