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.co 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.co/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": "api",
"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 is api for runs started this way; see Triggering AI Employees 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, a node completing, or the run reaching a terminal or paused state.
GET https://your-tenant.ema.co/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 chatbot visitors stream through the chat service. For an external web-chatbot 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 — 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, ordocument.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.co/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.co/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 prompt bodies; builder_admin sees model, tokens, tool results, and errors but not prompts; other roles see only node names, status, timing, and sanitized error markers. Both endpoints are rate-limited to 60 requests per minute per tenant, 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 for how schedules fit alongside the other trigger sources.
What's next
- Triggering AI Employees — 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.