> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deck.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a workflow

> Chain tasks with output references, conditions, loops, and failure handling in a single API call.

Run several tasks in sequence without orchestrating each step yourself. You define a [workflow](/concepts/workflows) once, and Deck runs the chain from there.

A workflow is an ordered array of steps. Each step references a [task](/concepts/tasks) by `task_id`, and every run of the workflow turns each step into a full [task run](/concepts/task-runs). Steps execute one at a time, top to bottom. Later steps read earlier outputs with `{{ }}` references, and any step can be skipped with an `if` condition.

## Example

A reservation manager agent checks the current price of a hotel booking. If the price dropped, a second task rebooks at the lower rate.

```text theme={null}
POST /v2/workflows
```

```json theme={null}
{
  "name": "Price drop rebooking",
  "steps": [
    {
      "name": "check_price",
      "type": "task",
      "task_id": "task_a1b2c3...",
      "input": { "confirmation_number": "HLT-849271" }
    },
    {
      "name": "rebook",
      "type": "task",
      "task_id": "task_d4e5f6...",
      "if": "steps.check_price.output.price_dropped == true",
      "input": { "confirmation_number": "{{ steps.check_price.output.confirmation_number }}" }
    }
  ]
}
```

The agent is determined by the task, so a step only needs a `task_id`.

### Definition fields

| Field   | Required | Meaning                                                           |
| ------- | -------- | ----------------------------------------------------------------- |
| `name`  | Yes      | Display name for the workflow                                     |
| `steps` | Yes      | Ordered array of step definitions. See [Step types](#step-types). |

The response is the full workflow object (`wflo_` prefix).

```json theme={null}
{
  "id": "wflo_a1b2c3...",
  "object": "workflow",
  "name": "Price drop rebooking",
  "steps": [ ... ],
  "created_at": "2026-06-18T00:00:00Z",
  "updated_at": "2026-06-18T00:00:00Z",
  "request_id": "req_7c1f9a2b"
}
```

Deck validates the definition on create: every `task_id` must exist in your organization, step names must be unique, and every `{{ }}` reference and `if` condition must point at a step that appears earlier in the array.

<Note>
  Tasks that declare [tokenized input fields](/concepts/tasks#tokenizing-sensitive-fields) can't be used in a workflow yet. Creating a workflow with one is rejected.
</Note>

## Running a workflow

```text theme={null}
POST /v2/workflows/{workflow_id}/run
```

```json theme={null}
{
  "steps": [
    {
      "name": "check_price",
      "credential_id": "cred_hilton..."
    },
    {
      "name": "rebook",
      "credential_id": "cred_hilton..."
    }
  ]
}
```

The run body carries per-step overrides. Each entry names a step from the definition and supplies the run-time values for it. Steps you don't list run exactly as defined.

| Field           | Meaning                                                                                                                         |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `name`          | The step to override, matching a step name in the definition. For a loop, the override applies to the inner step on every item. |
| `credential_id` | Credential for this step's task run. Mutually exclusive with `source_id`.                                                       |
| `source_id`     | Source for this step's task run, for public sources that don't require authentication. Mutually exclusive with `credential_id`. |
| `input`         | Shallow-merged onto the definition's `input`. See [Run-time overrides](#run-time-overrides).                                    |

`session_id` is not accepted on a step. All steps in a run execute in one [session](/concepts/sessions), so an agent that logged in during one step stays logged in for the next. The run's `session_id` field reports which session that is.

Creating a run reserves that one session for the whole workflow, and it counts toward your organization's session limit like any other. If you're at the limit, the request fails with `session_limit_exceeded` rather than queuing a run that can't start.

The response is a workflow run object (`wrun_` prefix) with status `queued`. Execution is asynchronous: poll `GET /v2/workflow-runs/{id}` or subscribe to [events](#events) for progress.

```json theme={null}
{
  "id": "wrun_a1b2c3...",
  "object": "workflow_run",
  "workflow_id": "wflo_a1b2c3...",
  "status": "queued",
  "result": null,
  "runtime_ms": null,
  "session_id": "sess_p7q8r9...",
  "trigger_id": null,
  "steps": [
    {
      "name": "check_price",
      "type": "task",
      "status": "queued",
      "output": null,
      "task_runs": []
    },
    {
      "name": "rebook",
      "type": "task",
      "status": "queued",
      "output": null,
      "task_runs": []
    }
  ],
  "errors": null,
  "created_at": "2026-06-18T00:00:00Z",
  "updated_at": "2026-06-18T00:00:00Z",
  "request_id": "req_4d5e6f7a"
}
```

Send an `Idempotency-Key` header to make run creation safe to retry. See [Idempotency](/api/idempotency).

## Reading a run

A finished run answers three questions.

| Question                                  | Where to look                                              |
| ----------------------------------------- | ---------------------------------------------------------- |
| Which steps ran, failed, or were skipped? | `steps[].status`, in definition order                      |
| Did the run succeed?                      | `status` for the lifecycle state, `result` for the outcome |
| What broke, and where?                    | `errors[]`, each entry naming its `step`                   |

This run comes from a version of the workflow with two more steps: `record_expense` updates the booking's cost in your expense platform, and `save_confirmation` downloads the confirmation. The price held, so `rebook` was skipped. `record_expense` then failed on bad expense platform credentials, which stopped the run before `save_confirmation` started.

```json theme={null}
{
  "id": "wrun_a1b2c3...",
  "object": "workflow_run",
  "status": "failed",
  "result": "failure",
  "steps": [
    {
      "name": "check_price",
      "type": "task",
      "status": "completed",
      "output": { "paid_price": 289.00, "current_price": 289.00, "price_dropped": false },
      "task_runs": [
        { "task_run_id": "trun_5f3a...", "status": "completed", "result": "success", "created_at": "2026-06-18T00:00:01Z", "runtime_ms": 63000, "errors": null }
      ]
    },
    {
      "name": "rebook",
      "type": "task",
      "status": "skipped",
      "output": null,
      "task_runs": []
    },
    {
      "name": "record_expense",
      "type": "task",
      "status": "failed",
      "output": null,
      "task_runs": [
        {
          "task_run_id": "trun_7d2e...",
          "status": "failed",
          "result": "failure",
          "created_at": "2026-06-18T00:01:05Z",
          "runtime_ms": 8000,
          "errors": [
            { "type": "auth", "code": "auth_invalid", "message": "The source rejected the credentials" }
          ]
        }
      ]
    },
    {
      "name": "save_confirmation",
      "type": "task",
      "status": "queued",
      "output": null,
      "task_runs": []
    }
  ],
  "errors": [
    { "type": "auth", "code": "auth_invalid", "message": "The source rejected the credentials", "step": "record_expense" }
  ]
}
```

`skipped` means the step's `if` condition was false. `queued` on a finished run means the step was never reached, because an earlier step failed or the run was canceled. Both have no task runs and `null` output, so read the status to tell them apart.

## Wiring outputs into inputs

Reference an earlier step's output from inside `input` with `{{ steps.<name>.output.<field> }}`. Deck resolves the reference against the upstream step's actual output at run time.

```json theme={null}
{
  "name": "rebook",
  "type": "task",
  "task_id": "task_d4e5f6...",
  "input": {
    "confirmation_number": "{{ steps.check_price.output.confirmation_number }}"
  }
}
```

When a value is a single reference and nothing else, it keeps its JSON type, so an object stays an object and a number stays a number. `input` itself must be an object, so to forward an entire output, nest the reference under a key: `"input": { "booking": "{{ steps.check_price.output }}" }`. A reference embedded in a longer string is substituted as text.

### Finding the paths

The upstream task's `output_schema` lists every field you can reference. Fetch it with `GET /v2/tasks/{task_id}`, or author the workflow in the [Console](https://console.deck.co), which autocompletes against the schema.

### Missing fields

If the source doesn't populate a field on the upstream task, the reference resolves to `null`, and the downstream task run receives `null` for that field. Step input isn't checked against the task's `input_schema` before the run starts, so a required field arriving as `null` doesn't fail the run up front. Write downstream tasks to tolerate a missing value, or gate the step with an `if` on the field.

### Run-time overrides

A step can have an `input` in its definition and still accept an `input` override on the run body. Deck shallow-merges the two: top-level keys from the override win, and keys absent from the override fall back to the definition, with references resolved.

```json theme={null}
// Definition
"input": {
  "confirmation_number": "{{ steps.check_price.output.confirmation_number }}",
  "notes": "Auto-rebooked"
}

// Run-time override
{ "name": "rebook", "input": { "notes": "Manual escalation replay" } }

// Resolved input sent to the task run
{
  "confirmation_number": "<resolved from check_price.output>",
  "notes": "Manual escalation replay"
}
```

Nested objects replace entirely. There's no deep merge, so include every nested key you want to keep.

## Step types

Every step has a `type` and a `name`. Names must be lowercase identifiers matching `[a-z_][a-z0-9_]*`, since they appear inside references and conditions.

### Task steps

A `task` step wraps one task run.

| Field           | Required | Meaning                                                                  |
| --------------- | -------- | ------------------------------------------------------------------------ |
| `name`          | Yes      | Step identifier, unique within the workflow                              |
| `task_id`       | Yes      | The task to run. The agent is derived from the task.                     |
| `credential_id` | No       | Credential for the task run. Mutually exclusive with `source_id`.        |
| `source_id`     | No       | Public source for the task run. Mutually exclusive with `credential_id`. |
| `input`         | No       | Input for the task run. Supports `{{ }}` references.                     |
| `if`            | No       | Condition expression. The step is `skipped` when false.                  |

Pin `credential_id` in the definition when the step always uses the same credential. Leave it out when the credential varies per run and pass it on the run body instead.

### Conditions

Add an `if` condition to any step to control whether it runs. If the expression is false, the step is marked `skipped` and the workflow continues to the next step.

```json theme={null}
{
  "name": "cancel",
  "type": "task",
  "task_id": "task_g7h8i9...",
  "if": "steps.find.output.cancellable"
}
```

There's no branch step type. Express "A or B" as two steps with inverted conditions:

```json theme={null}
{ "name": "cancel", "type": "task", "task_id": "task_g7h8i9...", "if": "steps.find.output.cancellable" },
{ "name": "log_status", "type": "task", "task_id": "task_j1k2l3...", "if": "!steps.find.output.cancellable" }
```

A skipped step produces no task run and its `output` is `null`. Downstream references to it resolve to `null`.

### Loops

A `for_each` step runs an inner task step once per item in an array from an earlier step's output. Iterations run sequentially in the same session, so the agent stays logged in between them.

```json theme={null}
{
  "name": "process_refunds",
  "type": "for_each",
  "items": "steps.find.output.reservations",
  "failure_behavior": "continue",
  "step": {
    "type": "task",
    "task_id": "task_m4n5o6...",
    "input": { "reservation_id": "{{ item.id }}" }
  }
}
```

| Field              | Required | Meaning                                                                                                                                                                                     |
| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`             | Yes      | Step identifier                                                                                                                                                                             |
| `items`            | Yes      | Path to the array to iterate, such as `steps.find.output.reservations`                                                                                                                      |
| `step`             | Yes      | The task step to run per item. It omits `name`; reference the loop as a whole by its outer name.                                                                                            |
| `failure_behavior` | No       | `stop` (default) fails the whole step on the first failed item, which stops the workflow. `continue` runs the remaining items and the workflow carries on; failed items have `null` output. |
| `if`               | No       | Condition expression gating the whole loop                                                                                                                                                  |

Inside the inner step, `item` refers to the current element. The inner step can carry its own `if` to skip individual items; that condition may read `item`, while the outer loop's `if` cannot.

`steps.process_refunds.output` resolves to the ordered array of inner outputs, one per input item. An empty array completes the step immediately with an empty output. If `items` resolves to anything other than an array, the step fails with `invalid_field_value` and the workflow stops. Loops can't nest: the inner step must be a `task`, and `type` may be omitted on the inner step.

## Expression grammar

Conditions and references use a small expression language. It is not JavaScript, and there are no function calls or arithmetic.

| Element                 | Examples                                                    |   |           |
| ----------------------- | ----------------------------------------------------------- | - | --------- |
| Path references         | `steps.<name>.output.<field>`, `item.<field>` inside a loop |   |           |
| Comparison              | `==`, `!=`, `<`, `<=`, `>`, `>=`                            |   |           |
| Logical                 | `&&`, `\|\|`, `!`                                           |   |           |
| Array and string length | `steps.find.output.reservations.length`                     |   |           |
| Grouping                | \`(a                                                        |   | b) && c\` |
| Literals                | strings, numbers, `true`, `false`, `null`                   |   |           |

A condition can be a field reference on its own, with no comparison. It passes when the field is `true`, a non-zero number, a non-empty string or array, or any object. It fails when the field is `false`, zero, an empty string or array, `null`, or missing. Comparing values of different types is `false` rather than an error (`!=` is `true`), so a step never fails because a source omitted an optional field. Comparisons don't chain: write `a == b && b == c`, not `a == b == c`. Deck rejects invalid syntax when you create or update the workflow.

## Fetching a workflow run

```text theme={null}
GET /v2/workflow-runs/{workflow_run_id}
```

Returns the current state of the run with every step's status, output, and task runs.

```json theme={null}
{
  "id": "wrun_a1b2c3...",
  "object": "workflow_run",
  "workflow_id": "wflo_a1b2c3...",
  "status": "completed",
  "result": "success",
  "runtime_ms": 132000,
  "session_id": "sess_p7q8r9...",
  "trigger_id": null,
  "steps": [
    {
      "name": "check_price",
      "type": "task",
      "status": "completed",
      "output": { "paid_price": 289.00, "current_price": 219.00, "price_dropped": true },
      "task_runs": [
        {
          "task_run_id": "trun_5f3a...",
          "status": "completed",
          "result": "success",
          "created_at": "2026-06-18T00:00:01Z",
          "runtime_ms": 63000,
          "errors": null
        }
      ]
    },
    {
      "name": "rebook",
      "type": "task",
      "status": "completed",
      "output": { "confirmation_number": "HLT-849271", "new_price": 219.00 },
      "task_runs": [
        {
          "task_run_id": "trun_9b2c...",
          "status": "completed",
          "result": "success",
          "created_at": "2026-06-18T00:01:05Z",
          "runtime_ms": 68000,
          "errors": null
        }
      ]
    }
  ],
  "errors": null,
  "created_at": "2026-06-18T00:00:00Z",
  "updated_at": "2026-06-18T00:02:13Z",
  "request_id": "req_5e6f7a8b"
}
```

### Run fields

| Field        | Meaning                                                                                                                                                                        |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `status`     | Lifecycle state. See [Statuses](#statuses).                                                                                                                                    |
| `result`     | Outcome once terminal: `success`, `failure`, or `unknown`. `null` while running and for a canceled run.                                                                        |
| `runtime_ms` | Milliseconds from the first step's start until the run reached a terminal status, excluding time spent queued. `null` until terminal.                                          |
| `session_id` | The session every step of this run executes in. Assigned when the run is created through the API, or when a trigger-created run is admitted, so it can be `null` while queued. |
| `trigger_id` | The trigger that created this run, or `null` for runs created through the API                                                                                                  |
| `steps`      | Per-step state in definition order                                                                                                                                             |
| `errors`     | Error objects with a `step` locator when the run failed. `null` otherwise. See [Errors](#errors).                                                                              |

`result` aggregates every task run the workflow produced. It's `success` only when all of them succeeded, `failure` if any ended in failure, and `unknown` when there were no failures but at least one task run finished `unknown`. Skipped steps produce no task runs and don't count, so a run whose every step was skipped is `success`.

### Step state

| Field       | Meaning                                                                                  |
| ----------- | ---------------------------------------------------------------------------------------- |
| `name`      | The step's name from the definition                                                      |
| `type`      | `task` or `for_each`                                                                     |
| `status`    | The underlying task run's status, plus `skipped`. A loop aggregates its task runs.       |
| `output`    | What `{{ steps.<name>.output }}` resolves to. `null` until completion.                   |
| `task_runs` | The task runs this step produced, in order: one for a task step, one per item for a loop |

Each `task_runs` entry is a summary of the underlying task run. For full details, including output, storage, and screenshots, fetch the task run itself with `GET /v2/task-runs/{task_run_id}`.

| Field         | Meaning                                                                                               |
| ------------- | ----------------------------------------------------------------------------------------------------- |
| `task_run_id` | The task run this entry summarizes                                                                    |
| `status`      | The task run's status                                                                                 |
| `result`      | `success`, `failure`, or `unknown`. `null` until terminal.                                            |
| `created_at`  | When the task run was created. Because steps run sequentially, this is when the step or item started. |
| `runtime_ms`  | Execution time in milliseconds. `null` until terminal, and for a run that failed before it started.   |
| `errors`      | The task run's errors when it failed. `null` otherwise.                                               |
| `item`        | Loops only. The input item this run processed.                                                        |

Because steps run one after another, the `created_at` and `runtime_ms` values across entries give you the full timeline of the run from a single fetch. Gaps between entries are time spent waiting on an interaction or moving between steps.

### Optional data

The default response returns the fields above. Use `?include=` to add optional data to each `task_runs` entry:

| Value       | What it adds                                                             |
| ----------- | ------------------------------------------------------------------------ |
| `input`     | The resolved `input` each task run received, with references substituted |
| `storage`   | Files captured during that task run                                      |
| `artifacts` | Screenshots taken during that task run                                   |

Comma-separated values are supported:

```text theme={null}
GET /v2/workflow-runs/{workflow_run_id}?include=input,storage,artifacts
```

This mirrors the `include` parameter on [task runs](/concepts/task-runs#including-additional-data).

## Listing workflow runs

```text theme={null}
GET /v2/workflow-runs?workflow_id=wflo_a1b2c3...&status=failed
```

Runs from every workflow are listed together, newest first. Scope to a single workflow with `workflow_id`, the same way task runs relate to tasks.

| Param             | Meaning                                                                                  |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `workflow_id`     | Scope to a single workflow                                                               |
| `trigger_id`      | Scope to runs created by one trigger                                                     |
| `status`          | Filter by run status. Comma-separated values match any, such as `status=queued,running`. |
| `limit`, `cursor` | Standard [pagination](/api/pagination)                                                   |

Filters combine with AND, per the [list endpoint conventions](/api/filtering-list-endpoints). List entries carry every run field and each step's `status` and `output`, but omit `task_runs`. Fetch a run individually for those.

## Inspecting a workflow run's task runs

Every task run a workflow produces carries a `workflow_run_id` and the owning `step` name. Both are always present and `null` for standalone runs. To list a workflow run's task runs directly instead of reading them out of each step:

```text theme={null}
GET /v2/task-runs?workflow_run_id=wrun_a1b2c3...
```

Add `step=<name>` to narrow to one step, and combine with the existing task run filters. Use `workflow_id` instead to span every run of a workflow.

## Interactions

If a step runs into MFA or other verification on the source, the workflow enters `paused` and the step's task run enters `interaction_required`. Find the task run in the step's `task_runs` array and submit the response to its interaction endpoint:

```text theme={null}
POST /v2/task-runs/{task_run_id}/interaction
```

The request body and semantics match [task run interactions](/guides/interactions). Once the task run accepts the input, the step resumes and the workflow continues. There's no workflow-specific interaction endpoint.

Inside a loop, each item is its own task run. When the current item needs input, the workflow pauses and later items wait until it resolves.

## Canceling a workflow run

```text theme={null}
POST /v2/workflow-runs/{workflow_run_id}/cancel
```

The run transitions to `canceling` while the current step stops, then to `canceled` once it finishes. The in-flight task run is canceled at the same time; no separate call to the task run cancel endpoint is needed. A run can be canceled from `queued`, `running`, or `paused`. Canceling a run that already finished is rejected with `invalid_field_value`.

## Timeouts

Each step is bounded by its task's [run timeout](/concepts/task-runs#timeouts). A run that stays in flight for 24 hours fails with a `timeout` error on the step it was on, whatever the steps' own limits.

## Failure handling

A workflow run stops on the first step failure. The run ends `failed`, steps that already completed keep their `output`, later steps stay `queued`, and the error is recorded on the run.

A workflow can't continue past a failed step or retry it. Deck retries transient infrastructure failures inside the task run before it reports a failure.

To keep going when some items in a list fail, use the loop's `failure_behavior`.

### Continuing past failed items

With `failure_behavior: "continue"` on a loop, a failed item doesn't stop the remaining items or the steps after the loop. The step finishes `completed` with `null` in the failed item's output slot, and the run carries no top-level `errors`. The failed item's own `errors` in `task_runs` is the record of what went wrong, and the run's `result` is still `failure` because a task run failed.

```json theme={null}
{
  "name": "cancel_each",
  "type": "for_each",
  "status": "completed",
  "output": [ { "canceled": true }, null ],
  "task_runs": [
    {
      "task_run_id": "trun_a1...",
      "status": "completed",
      "result": "success",
      "created_at": "2026-06-18T00:00:01Z",
      "runtime_ms": 41000,
      "errors": null,
      "item": { "reservation_id": "R-001" }
    },
    {
      "task_run_id": "trun_a2...",
      "status": "failed",
      "result": "failure",
      "created_at": "2026-06-18T00:00:43Z",
      "runtime_ms": 12000,
      "errors": [
        { "type": "auth", "code": "auth_invalid", "message": "The source rejected the credentials" }
      ],
      "item": { "reservation_id": "R-002" }
    }
  ]
}
```

## Errors

When a run ends `failed`, its top-level `errors` array holds standard Deck [error objects](/api/errors) with one extra field: `step`, naming the step that caused the failure. Workflows don't introduce new error types, and `field` is present only when the error concerns a specific field, as on task runs. The same error, without `step`, appears on the failing step's `task_runs` entry.

```json theme={null}
{
  "id": "wrun_a1b2c3...",
  "object": "workflow_run",
  "status": "failed",
  "result": "failure",
  "steps": [
    {
      "name": "check_price",
      "type": "task",
      "status": "failed",
      "output": null,
      "task_runs": [
        {
          "task_run_id": "trun_5f3a...",
          "status": "failed",
          "result": "failure",
          "created_at": "2026-06-18T00:00:01Z",
          "runtime_ms": 63000,
          "errors": [
            { "type": "auth", "code": "auth_invalid", "message": "The source rejected the credentials" }
          ]
        }
      ]
    },
    {
      "name": "rebook",
      "type": "task",
      "status": "queued",
      "output": null,
      "task_runs": []
    }
  ],
  "errors": [
    {
      "type": "auth",
      "code": "auth_invalid",
      "message": "The source rejected the credentials",
      "step": "check_price"
    }
  ]
}
```

The workflow-level entry is a summary. The task run's own `errors` at `GET /v2/task-runs/{id}` is the authoritative record, along with its storage and screenshots.

## Statuses

### Workflow run statuses

| Status      | Meaning                                                                                  |
| ----------- | ---------------------------------------------------------------------------------------- |
| `queued`    | Waiting to start                                                                         |
| `running`   | Executing a step                                                                         |
| `paused`    | The current step is waiting on external input. The step's own status carries the reason. |
| `completed` | All steps finished                                                                       |
| `failed`    | A step failed and the workflow stopped                                                   |
| `canceling` | Cancellation requested, the current step is stopping                                     |
| `canceled`  | You canceled the workflow before it completed                                            |

Any state where the run is waiting on someone shows as `paused`. Check the step for the specific reason. `status` is where the run is in its lifecycle and `result` is how it turned out, the same way task runs separate the two.

### Step statuses

A step's `status` is its task run's status: `queued`, `running`, `interaction_required`, `review_required`, `completed`, `failed`, `canceling`, or `canceled`. See [task run statuses](/concepts/task-runs#task-run-statuses). One value is workflow-only:

| Status    | Meaning                                                         |
| --------- | --------------------------------------------------------------- |
| `skipped` | The step's `if` condition was false, so no task run was created |

A loop's `status` summarizes its items: `running` while any item is in flight, `failed` when an item fails under `failure_behavior: stop`, and `completed` once every item finishes.

## Managing workflows

| Method   | Path                          | Notes                                                                                   |
| -------- | ----------------------------- | --------------------------------------------------------------------------------------- |
| `GET`    | `/v2/workflows/{workflow_id}` | Returns the full workflow object                                                        |
| `GET`    | `/v2/workflows`               | Paginated list of workflows                                                             |
| `PATCH`  | `/v2/workflows/{workflow_id}` | Accepts any subset of `name` and `steps`. Passing `steps` replaces the array wholesale. |
| `DELETE` | `/v2/workflows/{workflow_id}` | Returns `{ "id": "wflo_...", "object": "workflow", "deleted": true }`                   |

Edits apply to future runs only. Runs already in flight continue against the definition they started with. Deleting a workflow doesn't affect in-flight runs, but the definition can no longer be run.

`POST /v2/workflows`, `PATCH`, `DELETE`, and `POST /v2/workflows/{id}/run` all accept an `Idempotency-Key` header.

## Running on a schedule

A [trigger](/guides/triggers#targeting-a-workflow) can target a workflow instead of a task. Each fire creates one workflow run per credential in scope, and that credential is used by every step the trigger doesn't pin. Steps pinned in the trigger's `steps` config keep their own `credential_id` or `source_id`, which is how a scheduled workflow can log in with the fanned-out credential and then act on a second source. The trigger's `steps` config has the same shape as the run body above, but it's the same for every credential and every fire, so the first step's input can't vary per user. Later steps can, by referencing earlier outputs.

A trigger-created run carries the `trigger_id`, and `GET /v2/workflow-runs?trigger_id=trg_...` lists one trigger's runs. The trigger's `skip_if` reads the run's `result`, so skip conditions behave the same whether the trigger targets a task or a workflow. Otherwise a triggered run is identical to one you create directly.

## Events

Workflows emit into the existing [events](/events/events) system on three channels.

Lifecycle events track the definition:

| Event              | Fired when                      |
| ------------------ | ------------------------------- |
| `workflow.created` | A workflow is created           |
| `workflow.updated` | A workflow is updated via PATCH |
| `workflow.deleted` | A workflow is deleted           |

Workflow run events fire once per status transition, so a subscriber sees every state the run passed through:

| Event                    | Fired when                                    |
| ------------------------ | --------------------------------------------- |
| `workflow_run.queued`    | Run is waiting to start                       |
| `workflow_run.running`   | A step begins executing                       |
| `workflow_run.paused`    | The current step is waiting on external input |
| `workflow_run.completed` | All steps finished                            |
| `workflow_run.failed`    | A step failed and the workflow stopped        |
| `workflow_run.canceling` | Cancellation requested                        |
| `workflow_run.canceled`  | Run is canceled                               |

The payload carries the run's `workflow_run_id`, `workflow_id`, `status`, `session_id`, `trigger_id`, and `result`. `result` is only set on the terminal events. See the [events reference](/events/events-reference#workflow-run-events) for the full payload.

Task run events fire as normal. Every task step emits the standard [`task_run.*` events](/events/events-reference#task-run-events), and events aren't suppressed inside a workflow. Their payloads add `workflow_run_id` and the owning `step` name, plus `trigger_id` when the run was trigger-created, so existing handlers can attribute workflow-driven runs to the right step. A loop emits one stream per item.
