How do I run an AI workflow from my app and get the output files back?

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.

Last updated View as Markdown

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.

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"}}'
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);
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:

{
  "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:

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):

{
  "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.
  • 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, up to 25MB, or 100MB for video.

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:

{
  "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:

{
  "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.

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
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);
  }
}
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 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 generate:nano_banana_2 Topaz Upscale, Image Per image
Topaz Upscale 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 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 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:

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.

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.

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 runs your own workflow over a CSV, with a cost estimate first.

For AI agents: the full documentation index is at /llms.txt, and most docs pages are available as Markdown by adding .md to the URL.

© 2026 Wireflow. All rights reserved.