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 pendingrunning → 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 a DetailedRunResponse — the run plus a per-model token_usage breakdown and output/named_outputs.
  • Steps: GET /api/v1/workflow/runs/{run_id}/steps returns 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_typeui, api, schedule, webhook, feedback, or document.
    • statuspending, running, paused, completed, failed, cancelled, or skipped.
    • since — an RFC 3339 lower bound (inclusive) on created_at.
  • List recent runs across all workflows: GET /api/v1/workflow/runs — the dashboard view, paginated with limit and offset. 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}/trace returns 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/enable and .../disable. Enabling requires a published version (otherwise 409).
  • 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_type each 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.

Last updated: Jul 3, 2026