---
question: How do I start a workflow from Nexus, Python or HTTP?
kind: task
via_web: available
via_cli: available
via_python: available
via_api: available
via_config: na
ref_web: System › Jobs › Run now
ref_cli: abi run script
ref_python: get_workflow(name).run()
ref_api: POST /api/workflows/{module_id}/{name}/runs
prereq: You need a workflow in a module that runs. The Web UI, `/api/workflows` and `get_workflow` start durable runs, which need ABI 3.x and a NATS project (the default of `abi new project` on 3.x). The CLI and the kernel route work on ABI 2.x too.
next:
  - /v2/capabilities/build/workflows/monitor-and-cancel-a-run
  - /v2/capabilities/build/workflows/schedule-a-workflow
  - /v2/capabilities/build/workflows/expose-as-a-tool
---

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

# Run a workflow

A workflow runs in one of two ways:

- **A direct call.** Your code, a script, an agent tool or the kernel route calls a class's `run(parameters)` in process. It returns at once, and nothing records it.
- **A durable run** (ABI 3.x, NATS project). A caller starts the workflow by name: a `@workflow`, or a class through its published name `workflow.<slug>`. The trigger is stored in JetStream, one replica of the module runs it, and the run is recorded with its input, steps and output in System › Jobs. A durable start answers with a run, and the caller waits for it as long as it chooses.

## Web UI

ABI 3.x, in a NATS project. ABI 2.x has no System › Jobs: use the CLI, Python or the kernel route.

### Open the workflow

Open **System** from the Nexus sidebar, then the **Jobs** tab. Pick **Workflows** in the kind filter, then click the workflow. A class shows under its published name, such as `workflow.customers_workflow`.

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

*System › Jobs: jobs and workflows, with their schedules and recent runs.*

### Fill in the input

Click **Run now**. The dialog shows the module id and a form built from the workflow's input schema, required fields first; **JSON** switches to an editor for the same input. The input is checked against the schema before anything is sent: a field that does not fit is named in the form, and nothing starts.

### Start it

Click **Run** (or press ⌘↵). The start is written to the audit log. The run shows in the workflow's **Runs**, `QUEUED` until a replica takes it. Click it to follow its steps: see [Monitor and cancel a run](/v2/capabilities/build/workflows/monitor-and-cancel-a-run?via=web).

The System app is for platform super admins only.

## CLI

### Write a script

```python title="scripts/run_customers.py"
from my_module.workflows.CustomersWorkflow import (
    CustomersWorkflow,
    CustomersWorkflowConfiguration,
    CustomersWorkflowParameters,
)

workflow = CustomersWorkflow(CustomersWorkflowConfiguration())
print(workflow.run(CustomersWorkflowParameters(entity_id="42")))
```

### Run it with the engine

From the project root:

```bash
abi run script scripts/run_customers.py
```

`abi run script` loads the engine first, so the workflow can read its module's services. Arguments after the path are passed to the script. On ABI 3.x in a NATS project, the broker has to be up (`abi dev up`). This is a direct call: it is not recorded as a run. The command is [`abi run script`](/user-guide/reference/cli#abi-run-script).

No `abi` command starts a durable run. From a terminal, use `curl` as in the [REST API steps](/v2/capabilities/build/workflows/run-a-workflow?via=api).

## Python

### Call `run` directly

Build the class and call `run` with its parameters model:

```python
workflow = CustomersWorkflow(CustomersWorkflowConfiguration())
result = workflow.run(CustomersWorkflowParameters(entity_id="42"))
```

Inside the engine (an agent, another workflow, a job handler), the module's services are already there. Outside it, load the engine first, as the CLI steps do.

### Start a durable run

ABI 3.x, in a NATS project. `get_workflow(name)` returns a proxy with two methods, both coroutines:

```python
report = self.get_workflow("daily_report")          # one of this module's workflows

output = await report.run({"day": "2026-10-31"})    # start, wait, return the output
run = await report.start({"day": "2026-10-31"}, idempotency_key="report-2026-10-31")
```

- `run(input, timeout=)` returns the output, or raises `WorkflowRunFailed` when the run did not succeed. `timeout` bounds the wait (`asyncio.TimeoutError`); the run goes on.
- `start(input, idempotency_key=)` returns a `WorkflowRun` at once: `status()`, `wait()`, `result()`, `steps()`, `cancel()`. A repeated key within two minutes is the same run.
- The input is a dict or the input model. A class takes its parameters: `self.get_workflow("workflow.customers_workflow").run({"entity_id": "42"})`.
- In sync code, run the coroutine: `asyncio.run(report.run({...}))`.

### Start another module's workflow

| Call | Reaches |
|---|---|
| `self.engine.modules["<module id>"].get_workflow(name)` | A module listed in `ModuleDependencies(modules=[...])`, typed by its declared input and output. |
| `self.engine.get_workflow("<module id>", name)` | Any module by id, on the engine's connection. The module must be a declared dependency. It checks nothing before the start: a run of a workflow nobody hosts stays `QUEUED`. |

A module's id is its name under `modules:` in `config.yaml`. Code outside a module starts a run with the [REST API](/v2/capabilities/build/workflows/run-a-workflow?via=api). Inside a durable workflow, start a child run with `ctx.call` instead: see [Write a durable workflow](/v2/capabilities/build/workflows/write-a-durable-workflow?via=python#call-other-workflows).

## REST API

Two routes run a workflow. They differ in what they reach and what they record.

### Call the kernel route

ABI 2.x and 3.x. A workflow class with a live, typed `run` has `POST /workflows/<slug>` on the engine API. The body is the parameters model; the answer is the result. It is a direct call, not a recorded run. Auth: the engine's `ABI_API_KEY` as a Bearer token.

```bash
curl -X POST "http://localhost:9879/workflows/customers_workflow" \
  -H "Authorization: Bearer $ABI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"entity_id": "42"}'
```

See [Expose as an API](/v2/capabilities/build/workflows/expose-as-an-api).

### Start a durable run

ABI 3.x, in a NATS project. `POST /api/workflows/{module_id}/{name}/runs` on Nexus starts any durable workflow, of any module. The body is the input (`{}` when empty). Auth: `Authorization: Bearer $ABI_API_KEY`, or the session token of a platform super admin.

```bash
curl -s -X POST "http://localhost:9879/api/workflows/reports/daily_report/runs?wait=30" \
  -H "Authorization: Bearer $ABI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: report-2026-10-31" \
  -d '{"day": "2026-10-31"}'
```

- Without `wait`, the answer is `202` with the run, `QUEUED`. With `?wait=<seconds>` (0 to 120) and a run that ends in time, it is `200` with its `status`, `result` and `steps`. A run that failed also answers 200: check `status`.
- `Idempotency-Key` (at most 200 printable ASCII characters) makes a repeated start the same run, for two minutes.
- An input that does not match the schema answers 422 with `{source, reason, errors: [{path, message}]}`, and nothing starts. A plain job or an unknown name answers 404.
- Every start is written to the audit log first. These routes are not in the generated API reference.

With `abi dev up`, use the `api` port that `abi dev ports` prints. To read or cancel the run, see [Monitor and cancel a run](/v2/capabilities/build/workflows/monitor-and-cancel-a-run?via=api).

## Config

Not applicable. `config.yaml` enables the module but does not run a workflow. A schedule is a trigger in the module's code: see [Schedule a workflow](/v2/capabilities/build/workflows/schedule-a-workflow).
