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 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_type — ui, api, schedule, webhook, feedback, document, app_public, or email.
    • status — pending, 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.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}/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 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/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 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:

TypeConfig fieldDescription
agentagent_configAn AI agent step (intent classification, search & respond, extraction, rule validation, or custom).
call_aiecall_aie_configInvokes another published AI Employee as a child run.
send_emailsend_email_configSends an email via a configured integration.
notify_usernotify_user_configSends, updates, or closes a nudge card (Announce or Collect contract).
live_agent_transferlive_agent_transfer_configTransfers the conversation to a human agent queue.
scheduleschedule_configStarts 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:

CodeSeverityMeaning and remedy
NUDGE_PARK_NO_DOWNSTREAM_REREADwarningA 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_ENDSwarningA 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

Last updated: Oct 12, 2026