> Source: https://builder.ema.ai/v2/api-reference/workflow-api
> Title: Workflow API

# 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](/builder/v2/api-reference/authentication). Examples use `https://your-tenant.ema.co` as the host.

> [TIP]
> **Where CRUD lives.** Creating, editing, publishing, and versioning a workflow are covered on the [AI Employee API](/builder/v2/api-reference/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.

```http
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:

```json
{
  "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](/builder/v2/api-reference/triggers) for the other sources. Follow the run with the [SSE stream](#stream-a-run-in-real-time) or poll [run details](#inspect-a-run).

### 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.

```http
GET https://your-tenant.ema.co/api/v1/workflow/runs/9b2c.../stream
Authorization: Bearer eyJhbGciOiJSUzI1Ni...
Accept: text/event-stream
```

```text
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.

> [INFO]
> **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](/builder/v2/api-reference/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_type` — `ui`, `api`, `schedule`, `webhook`, `feedback`, or `document`.
    -   `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.

```http
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:

```json
{ "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:

```http
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](/builder/v2/api-reference/triggers) for how schedules fit alongside the other trigger sources.

## What's next

-   [Triggering AI Employees](/builder/v2/api-reference/triggers) — every way a run can start and what `trigger_type` each produces.
-   [Chat API](/builder/v2/api-reference/chat-api) — drive a chat workflow turn-by-turn instead of one run at a time.
-   [AI Employee API](/builder/v2/api-reference/ai-employee-api) — build, publish, and version the workflow you're running.
