---
title: "How do I run an AI workflow from my app and get the output files back?"
description: "To run an AI workflow from your app, send its inputs to POST /api/v1/workflows/{id}/run with your API key, poll the run, then download each output file."
updated: 2026-10-24
---

To run an AI workflow from your own app, send its input values to `POST /api/v1/workflows/{id}/run` with your API key, poll the run until it finishes, then download the file URLs in the result. Wireflow loads the workflow you saved in the editor, puts your values into its inputs and runs it, so your app never sends the node graph.

You need three things:

- **A workflow you own**, saved in the Wireflow editor. `/run` works only on workflows you own, and `GET /api/v1/workflows` lists yours with their ids.
- **An API key** with the `workflows:execute` scope, or **Full access**. Create it in **Settings > API Keys**. It starts with `sk-` and is shown only once. The examples below read it from the `WIREFLOW_API_KEY` environment variable.
- **Credits.** The key owner's credits pay for each AI model node that runs. API access is on every plan, including Free, but runs still use credits.

## How do I start a workflow over HTTP?

Send a `POST` to `https://www.wireflow.ai/api/v1/workflows/{id}/run` with your key as a Bearer token and the input values in `inputs`. The call returns `202 Accepted` straight away with an `executionId`, and the run carries on in the background.

```bash
curl -X POST https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/run \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{"inputs": {"node-prompt": "a ceramic mug on a wooden table, soft morning light"}}'
```

```javascript
const API = 'https://www.wireflow.ai/api/v1';
const headers = {
  Authorization: `Bearer ${process.env.WIREFLOW_API_KEY}`,
  'Content-Type': 'application/json',
};

const res = await fetch(`${API}/workflows/YOUR_WORKFLOW_ID/run`, {
  method: 'POST',
  headers: { ...headers, 'Idempotency-Key': 'order-1042' },
  body: JSON.stringify({
    inputs: { 'node-prompt': 'a ceramic mug on a wooden table, soft morning light' },
  }),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const { data } = await res.json();
console.log(data.executionId, data.poll);
```

```python
import os
import requests

API = "https://www.wireflow.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['WIREFLOW_API_KEY']}"}

res = requests.post(
    f"{API}/workflows/YOUR_WORKFLOW_ID/run",
    headers={**HEADERS, "Idempotency-Key": "order-1042"},
    json={"inputs": {"node-prompt": "a ceramic mug on a wooden table, soft morning light"}},
)
res.raise_for_status()
data = res.json()["data"]
print(data["executionId"], data["poll"])
```

Response `202 Accepted`:

```json
{
  "data": {
    "executionId": "exec_789",
    "status": "running",
    "poll": "/api/v1/workflows/executions/exec_789/poll"
  }
}
```

`poll` is a path, so put `https://www.wireflow.ai` in front of it. The `Location` header points to the same URL.

The `Idempotency-Key` header is optional. If a call times out or returns a `5xx`, the run may still have started. Retry with the same key and `/run` replays the original run, where a retry without it bills a second run.

## How do I send inputs, like text or an image URL, to a workflow run?

Put each value in `inputs`, keyed by the id of the input node it fills. To see those ids and the type each one expects, `GET` the same `/run` URL:

```bash
curl https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/run \
  -H "Authorization: Bearer $WIREFLOW_API_KEY"
```

Response `200 OK` (trimmed, with example values):

```json
{
  "data": {
    "workflowId": "cm1abc123",
    "workflowName": "Product shot to clip",
    "inputs": [
      { "nodeId": "node-prompt", "label": "Image prompt", "type": "text", "required": true },
      { "nodeId": "node-photo", "label": "Product photo", "type": "image", "required": true }
    ],
    "endpoint": "/api/v1/workflows/cm1abc123/run",
    "method": "POST"
  }
}
```

- **Text** goes in as a plain string.
- **Images, audio and video** go in as `https` URLs. Each input's `type` says what it feeds: `text`, `image`, `audio`, `video`, `url`, `number`, `boolean` or `select`. The full table is in [Run Workflows](/docs/api/run).
- **Inputs you leave out** keep the value saved in the workflow. Send no `inputs` at all to run with the saved defaults.

To use a file from your own computer, upload it first and send the `url` you get back. Uploads need a key with the `workflows:write` scope. Files up to 4MB can go inline, and bigger ones use the three-step upload in [Media Uploads](/docs/api/media), up to 25MB, or 100MB for video.

```bash
curl -X POST https://www.wireflow.ai/api/v1/media/upload \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -F "file=@mug.png"
```

The reply includes the file's `url` on `cdn.wireflow.ai`, ready to send as an image input:

```json
{
  "inputs": {
    "node-prompt": "the same mug on a beach at sunset",
    "node-photo": "https://cdn.wireflow.ai/uploads/api/42/a1b2....png"
  }
}
```

## How do I get the generated files from a run?

Poll `GET /api/v1/workflows/executions/{executionId}/poll` until `status` is `COMPLETED`, then read the file URLs in each node's `output` under `nodeResults.nodeRuns`. Keep polling while the status is `RUNNING` or `PENDING`, and stop on `COMPLETED`, `FAILED`, `CANCELLED` or `TIMEOUT`.

A finished image run looks like this:

```json
{
  "status": "COMPLETED",
  "nodeResults": {
    "nodeRuns": [
      {
        "nodeId": "node-1",
        "status": "COMPLETED",
        "output": {
          "images": [{ "url": "https://cdn.wireflow.ai/..." }]
        }
      }
    ]
  }
}
```

The code below waits for the run, then saves every file it finds. It collects each `url` field in a node's `output`, wherever it sits, and downloads each file. The JavaScript and Python versions continue from the start examples above.

```bash
POLL="https://www.wireflow.ai/api/v1/workflows/executions/exec_789/poll"
for i in $(seq 1 120); do
  CODE=$(curl -s -D headers.txt -o run.json -w '%{http_code}' "$POLL" \
    -H "Authorization: Bearer $WIREFLOW_API_KEY")
  if [ "$CODE" = "429" ]; then
    WAIT=$(grep -i '^retry-after:' headers.txt | tr -dc '0-9')
    sleep "${WAIT:-5}"; continue
  fi
  if [ "$CODE" != "200" ]; then echo "HTTP $CODE: $(cat run.json)"; exit 1; fi
  STATUS=$(jq -r '.status' run.json)
  echo "$STATUS"
  case "$STATUS" in
    COMPLETED|FAILED|CANCELLED|TIMEOUT) break ;;
  esac
  sleep 5
done
[ "$STATUS" = "COMPLETED" ] || { echo "Run ended: $STATUS"; exit 1; }
jq -r '.nodeResults.nodeRuns[].output | .. | .url? // empty' run.json |
  while read -r URL; do curl -s -O "$URL"; done
```

```javascript
import { writeFile } from 'node:fs/promises';

const FINAL = ['COMPLETED', 'FAILED', 'CANCELLED', 'TIMEOUT'];
const auth = { Authorization: `Bearer ${process.env.WIREFLOW_API_KEY}` };

async function waitForRun(pollPath) {
  let delay = 1000;
  while (true) {
    const res = await fetch(`https://www.wireflow.ai${pollPath}`, { headers: auth });
    if (res.status === 429) {
      await new Promise((r) => setTimeout(r, Number(res.headers.get('Retry-After') || 5) * 1000));
      continue;
    }
    if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
    const run = await res.json();
    if (FINAL.includes(run.status)) return run;
    await new Promise((r) => setTimeout(r, delay));
    delay = Math.min(delay * 1.5, 10000);
  }
}

function collectUrls(value, found = []) {
  if (Array.isArray(value)) value.forEach((item) => collectUrls(item, found));
  else if (value && typeof value === 'object') {
    for (const [key, item] of Object.entries(value)) {
      if (key === 'url' && typeof item === 'string') found.push(item);
      else collectUrls(item, found);
    }
  }
  return found;
}

const run = await waitForRun(data.poll);
if (run.status !== 'COMPLETED') throw new Error(run.error || run.status);

for (const node of run.nodeResults.nodeRuns) {
  for (const url of collectUrls(node.output)) {
    const file = await fetch(url);
    const name = `${node.nodeId}-${url.split('/').pop()}`;
    await writeFile(name, Buffer.from(await file.arrayBuffer()));
    console.log('saved', name);
  }
}
```

```python
import time

FINAL = {"COMPLETED", "FAILED", "CANCELLED", "TIMEOUT"}

def wait_for_run(poll_path):
    delay = 1
    while True:
        res = requests.get("https://www.wireflow.ai" + poll_path, headers=HEADERS)
        if res.status_code == 429:
            time.sleep(int(res.headers.get("Retry-After", "5")))
            continue
        res.raise_for_status()
        run = res.json()
        if run["status"] in FINAL:
            return run
        time.sleep(delay)
        delay = min(delay * 1.5, 10)

def collect_urls(value):
    if isinstance(value, list):
        for item in value:
            yield from collect_urls(item)
    elif isinstance(value, dict):
        for key, item in value.items():
            if key == "url" and isinstance(item, str):
                yield item
            else:
                yield from collect_urls(item)

run = wait_for_run(data["poll"])
if run["status"] != "COMPLETED":
    raise RuntimeError(run.get("error") or run["status"])

for node in run["nodeResults"]["nodeRuns"]:
    for url in collect_urls(node.get("output")):
        name = f"{node['nodeId']}-{url.rsplit('/', 1)[-1]}"
        with open(name, "wb") as f:
            f.write(requests.get(url).content)
        print("saved", name)
```

The shell loop checks up to 120 times, 5 seconds apart. Video generation can take 5+ minutes, so raise the count for long clips.

Rather not poll? Add a `webhook` block to the `/run` body and Wireflow POSTs the result to your server when the run ends. [How do I know when a run is finished](/docs/check-when-a-run-is-finished) compares the two and covers statuses, signatures and retries.

## How do I run an image model, an upscaler and a video model as one workflow from code?

Wire the three models together in one saved workflow and start it with a single `POST /run`. One call runs them all, and the finished run holds the image, the upscaled image and the video.

An example build, with prices from each node's page and the pricing page:

| Node | Node type | Wire its output into | Price |
| --- | --- | --- | --- |
| Text Input, labelled "Image prompt" | `input:text` | Nano Banana 2, **Prompt** | Free |
| [Nano Banana 2](/docs/nodes/generate--nano_banana_2) | `generate:nano_banana_2` | Topaz Upscale, **Image** | Per image |
| [Topaz Upscale](/docs/nodes/process--topaz_upscale_image) | `process:topaz_upscale_image` | Wan 2.5, **Image URL** | Per image |
| Text Input, labelled "Motion prompt" | `input:text` | Wan 2.5, **Prompt** | Free |
| [Wan 2.5](/docs/nodes/video--wan_25_preview_text_to_video) | `video:wan_25_preview_text_to_video` | Nothing: its **Generated Video** is the result | Per run |

Wan 2.5 switches between text to video and image to video based on what you wire into it. New accounts get a free video, and more videos need a paid plan. Resolution, length and quality change each price, and the [pricing page](https://www.wireflow.ai/pricing) is the source for plan prices.

Build it on the canvas, or ask your agent to draft it over MCP with `create_workflow_from_prompt`, which saves a private draft and runs nothing. Open the draft and check the wires before the first paid run.

The run call then sends both prompts:

```bash
curl -X POST https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/run \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "inputs": {
      "node-image-prompt": "a ceramic mug on a wooden table, soft morning light",
      "node-motion-prompt": "slow push-in, steam rising from the mug"
    }
  }'
```

Use the node ids that `GET /run` returns for your workflow. When the run is `COMPLETED`, each model node has its own entry in `nodeResults.nodeRuns`, so the download code above saves the still, the upscaled still and the clip.

The run charges every model node that runs, at its own price. If the balance cannot cover it, the API refuses the run with `402` and a `breakdown` of credits per node. For the same idea on the blog, see [how to chain multiple AI models in one API call](https://www.wireflow.ai/blog/how-to-chain-multiple-ai-models-in-one-api-call).

## How do I run a workflow over many rows through the API?

Call `/run` once per row, keep each row's `executionId`, and give each call its own `Idempotency-Key` so a retry never bills a row twice. If the API answers `429 Too Many Requests`, wait the number of seconds in the `Retry-After` header, then send that row again.

```python
import csv
import os
import time
import requests

API = "https://www.wireflow.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['WIREFLOW_API_KEY']}"}

runs = {}
with open("products.csv", newline="") as f:
    for row in csv.DictReader(f):
        while True:
            res = requests.post(
                f"{API}/workflows/YOUR_WORKFLOW_ID/run",
                headers={**HEADERS, "Idempotency-Key": f"catalog-{row['sku']}"},
                json={"inputs": {"node-prompt": row["prompt"]}},
            )
            if res.status_code != 429:
                break
            time.sleep(int(res.headers.get("Retry-After", "5")))
        res.raise_for_status()
        runs[row["sku"]] = res.json()["data"]["executionId"]

print(runs)
```

Then poll each `executionId` as above. Or add a `webhook` block with `metadata`, such as `{"sku": "A-100"}`, to each call: the delivery echoes `metadata` back, so you can match each result to its row. In Claude Code, the [ai-workflow-runner skill](/docs/skills/ai-workflow-runner) runs your own workflow over a CSV, with a cost estimate first.

Last updated: 2026-10-24

---

Documentation index: fetch https://www.wireflow.ai/llms.txt for the full list of Wireflow docs.
