# Running workflows (/guides/workflows)



A workflow is a saved graph of steps. Each step is an ordinary generation. One call runs them all, in order, and each step's output feeds the next.

* **Build workflows in the app, run them from code.** No API or MCP tool creates or edits a graph.
* A template is a ready-made workflow. Run it as is, or fill its inputs.

## 1. List templates [#1-list-templates]

<Tabs items="['REST','MCP','CLI']">
  <Tab value="REST">
    ```bash title="Terminal"
    curl -s https://app.riffads.com/api/v1/workflows/templates \
      -H "Authorization: Bearer $RIFFADS_API_KEY"
    ```
  </Tab>

  <Tab value="MCP">
    ```text
    list_templates {}
    ```
  </Tab>

  <Tab value="CLI">
    <SurfaceStatus id="cli" />

    ```bash title="Terminal"
    riffads workflows templates
    ```
  </Tab>
</Tabs>

```json title="Response (one input shown)"
{
  "ok": true,
  "templates": [
    {
      "template_key": "ugc_face_cam",
      "name": "Creator to camera",
      "step_count": 7,
      "inputs": [
        { "node": "f_brand", "label": "Brand context", "field": "text",
          "field_label": "value", "kind": "text", "max_length": 8000, "filled": true }
      ]
    }
  ]
}
```

Each `inputs` entry is a value you may set. `kind` is `text`, `setting`, `actor` or `voice`. Node ids can't be guessed, so read them from this list.

<Callout type="warn" title="Replace the example text">
  Template text inputs hold example copy about an invented product. Set them, or you get an ad for something you don't sell.
</Callout>

## 2. Start a run [#2-start-a-run]

<Tabs items="['REST','MCP','CLI']">
  <Tab value="REST">
    ```bash title="Terminal"
    curl -s -X POST https://app.riffads.com/api/v1/workflows/templates/ugc_face_cam/invoke \
      -H "Authorization: Bearer $RIFFADS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "max_credits": 1500,
        "inputs": [
          { "node": "f_brand", "field": "text", "value": "Lumen Skin, direct to consumer skincare." }
        ]
      }'
    ```

    A saved workflow: `POST /workflows/{workflow_id}/invoke`, same body.
  </Tab>

  <Tab value="MCP">
    ```text
    run_workflow { template_key: "ugc_face_cam", max_credits: 1500,
                   inputs: [{ node: "f_brand", field: "text", value: "Lumen Skin, ..." }] }
    ```

    Send `template_key` or `workflow_id`, never both.
  </Tab>

  <Tab value="CLI">
    <SurfaceStatus id="cli" />

    ```bash title="Terminal"
    riffads workflows invoke --template ugc_face_cam -m 1500 \
      -i f_brand.text='Lumen Skin, direct to consumer skincare.' --wait
    ```
  </Tab>
</Tabs>

The answer is `201` with a `workflow_run_id` (`wfr_`). Nothing is finished yet.

| Body field    | Rule                                                                       |
| ------------- | -------------------------------------------------------------------------- |
| `max_credits` | Required. Spend cap for the whole run, every step                          |
| `inputs`      | Optional. Up to 60 `{ node, field, value }`                                |
| `value`       | String, number or boolean. `actor` takes an `act_` id, `voice` a `voc_` id |

* Inputs change this run only. The saved workflow stays as it is.
* One bad input refuses the whole call. Nothing starts, nothing is charged.
* You can't pass files through `inputs`, and you can't set `count` or a wired field.
* The key needs the **Workflows** scope, or you get `403 insufficient_scope`.
* The first run of a template creates a copy in your workspace named with `(agent)`. Later runs reuse it.

<Callout type="warn" title="One run at a time per workflow">
  Starting it again while it runs: `409 submission_in_flight`. A blind retry after the first run ends starts, and charges, a second run.
</Callout>

## 3. Watch the run [#3-watch-the-run]

There is no wait endpoint for runs. Read `GET /workflow-runs/{id}` (MCP: `get_workflow_run`, CLI: `riffads workflows run <id> --wait`) about every 30 seconds until `run.still_running` is `false`.

| Run status          | Means                                            |
| ------------------- | ------------------------------------------------ |
| `queued`, `running` | Still going                                      |
| `completed`         | Every step finished                              |
| `partial`           | Some steps finished, some failed or were skipped |
| `failed`            | Nothing finished                                 |
| `canceled`          | Stopped before any step finished                 |

* Files are at `run.nodes[].generations[].outputs[].url`. Same 600 second signed links as a single generation. [Getting results](/guides/results).
* A failed step has `error.code`: `provider_error`, `moderation`, `timeout`, `canceled`, `insufficient_credits`, `invalid_input` or `unknown`.
* A `skipped` step never ran because an input failed.

## Stop or retry [#stop-or-retry]

Only in the app. There is no cancel or retry over REST, MCP or CLI. On `partial` or `failed`, read each step's error, fix the input, and start a new run.

A talking actor step makes its own voice first. No need to run [the two steps](/guides/talking-actor) yourself.
