---
question: How do I follow a workflow run's steps, and stop it?
kind: task
via_web: available
via_cli: na
via_python: available
via_api: available
via_config: available
ref_web: System › Jobs
ref_python: WorkflowRun
ref_api: GET /api/workflows/{module_id}/runs/{run_id}
ref_config: nats.jobs.retention
prereq: You need ABI 3.x, a NATS project and a started workflow run. The System app and `/api/workflows` take a platform super admin, or the engine's `ABI_API_KEY` for the REST API.
next:
  - /v2/capabilities/build/workflows/run-a-workflow
  - /v2/capabilities/build/workflows/write-a-durable-workflow
  - /v2/capabilities/build/pipelines/monitor-runs
---

> **You are reading the v2.x docs (engine modules). New projects use v3.** [Read this page in v3](/capabilities/build/workflows/monitor-and-cancel-a-run) · [Migrate from 2.x to 3.0](/capabilities/upgrade/migrate-to-3)

# Monitor and cancel a run

Every durable run, of a `@workflow` or of a published class (`workflow.<slug>`, `pipeline.<slug>`), is a job run with a record: its status, attempt, input, output, logs, trace, and the steps it stored. A cancel is cooperative: the host sets `ctx.cancelled`, the step running at that moment ends, and the run cancels the child runs it waits for.

Durable runs are ABI 3.x with NATS. On ABI 2.x, and on 3.x without NATS, a direct call of `run` leaves no record; a Dagster run shows in the [Dagster](/v2/capabilities/build/applications/dagster) UI.

A run's status is one of:

| Status | Meaning |
|---|---|
| `QUEUED` | Started, not taken by a host yet. |
| `RUNNING` | A replica of the module runs it. |
| `RETRYING` | An attempt failed; the next one starts after a backoff. |
| `SUCCEEDED` | It returned its output. |
| `SKIPPED` | It called `ctx.skip()`: nothing to do, no output. |
| `FAILED` | It failed with no attempt left, or with an error another attempt cannot fix (an invalid input). |
| `TIMED_OUT` | It ran past its `timeout`. |
| `CANCELLED` | Someone cancelled it, or its parent ended for good. |

## Web UI

### Open the workflow

Open **System** from the Nexus sidebar, then the **Jobs** tab. Pick **Workflows** in the kind filter to list only workflows, then click one. Its page shows the **Schedule** (with the input each trigger sends), **Health**, **Now**, **Settings**, the **Input schema** and **Output schema**, and its **Runs**.

![System app on the Jobs tab, filtered](/abi/Screenshot_System_Jobs.png)

*System › Jobs: each row is a job or a workflow, with its schedule, recent runs and last run.*

### Read a run

Click a run. Its panel has the timeline and the **Steps**, **Logs**, **Input**, **Result** (**Output** when the run did not succeed) and **Trace** tabs.

**Steps** lists each step in start order: its key, status, attempts, timing and error. A `call` or `map_call` step links to its child run: click it to open the child's own panel.

### Cancel it

While the run is going, click **Cancel**. The cancel is written to the audit log. The run ends `CANCELLED` at its next check of `ctx.cancelled`, or when its current step returns. **Re-run** starts a new run with the same input.

The System app is for platform super admins only.

## CLI

Not applicable. No `abi` command lists or cancels runs. Use the Web UI, or call the REST API from a script.

## Python

### Keep the run

`start(...)` returns a `WorkflowRun`:

```python
report = self.get_workflow("daily_report")
run = await report.start({"day": "2026-10-31"}, idempotency_key="report-2026-10-31")
print(run.run_id, run.duplicate)  # duplicate: the same key within two minutes
```

`get_workflow` is on the module (its own workflows) and on `self.engine.modules[id]` (another module): see [Run a workflow](/v2/capabilities/build/workflows/run-a-workflow?via=python).

### Read its status and steps

```python
record = await run.status()        # {"status": "QUEUED"} until a host takes it
steps = await run.steps()          # key, kind, status, attempt, timing, error, child
record = await run.wait(timeout=600)
output = await run.result()
```

- `status()` returns the run record.
- `steps()` returns the step summaries in start order, without their results. A `call` step carries `child`: the child's `module_id`, `workflow` and `run_id`.
- `wait(timeout=)` returns the record once the run is over.
- `result()` returns the output, or `None` for a skipped run. It raises `WorkflowRunFailed` for a run that ended any other way.

### Cancel it

```python
await run.cancel()
```

Inside the workflow, check `ctx.cancelled.is_set()` in long loops, and give I/O calls a timeout, so a cancel or a timeout takes effect soon.

## REST API

The Nexus workflow routes. Auth: `Authorization: Bearer $ABI_API_KEY` (the engine's key, which acts as the operator), or the session token of a platform super admin. These routes are not in the generated API reference.

### Read a run

The run id is what the start returned (`daily_report:12`):

```bash
curl -s "http://localhost:9879/api/workflows/reports/runs/daily_report:12" \
  -H "Authorization: Bearer $ABI_API_KEY"
```

The answer has `status`, `attempt`, `max_attempts`, `fired_at`, `started_at`, `finished_at`, `duration_ms`, `error`, `trace_id`, the whole `result`, and `steps`: each with `key`, `kind`, `status`, `attempt`, timing, `error`, `child` (`module_id`, `workflow`, `run_id`, `key`) and `result_bytes`. `steps_truncated` is set when the run has more steps than one page. A run no host has a record of (pruned, mistyped, not taken yet), a plain job's run, or a malformed id answers 404.

### Cancel it

```bash
curl -s -X POST "http://localhost:9879/api/workflows/reports/runs/daily_report:12/cancel" \
  -H "Authorization: Bearer $ABI_API_KEY"
```

`202 {"ok": true}` when the cancel was sent, 409 when the run is not running, 404 for a plain job's run. A cancel is written to the audit log first (with `via: api_key` for the key); if the audit log cannot be written, the answer is 503 and nothing is cancelled.

### List the workflows

`GET /api/workflows` lists every workflow with its module, input and output schemas, limits and triggers. To list runs across jobs and workflows, use the System routes on [Monitor runs](/v2/capabilities/build/pipelines/monitor-runs?via=api).

## Config

### Keep run records longer

```yaml title="config.yaml"
nats:
  jobs:
    retention:
      max_age_days: 7
      max_runs_per_job: 1000
      skipped_max_age_minutes: 60
```

These are the defaults: finished runs are kept 7 days, at most 1,000 per workflow, skipped ones 60 minutes. A pruned run's steps are deleted with it. Running and queued runs are never pruned. The other keys are on [Monitor runs](/v2/capabilities/build/pipelines/monitor-runs?via=config).

## Parents and children

- A parent that ends for good (cancelled, timed out, or failed with no attempt left) cancels the children it still waits for.
- A parent that the host retries leaves its children running: the next attempt waits for the same children.
- Cancelling a child alone fails the parent's step with `ChildRunFailed`, and the parent retries or fails like for any error.
