---
title: Workflows
description: Create, list, retrieve, update, and delete AI workflows via the Wireflow API.
updated: 2026-10-03
---

Workflows are the core resource in Wireflow. Each workflow contains a graph of AI model nodes connected by edges.

## List Workflows

```
GET /api/v1/workflows
```

Returns all workflows owned by the authenticated user, ordered by most recently updated.

**Query Parameters**

| Parameter  | Type     | Description                                    |
| ---------- | -------- | ---------------------------------------------- |
| `isActive` | `string` | Filter by active status: `"true"` or `"false"` |
| `mediaId`  | `string` | Filter by project/media ID                     |
| `teamId`   | `string` | Filter by team ID                              |

**Request**

```bash
curl https://www.wireflow.ai/api/v1/workflows \
  -H "Authorization: Bearer sk-your-api-key"
```

**Response** `200 OK`

```json
[
  {
    "id": "cm1abc123",
    "name": "Product Image Generator",
    "description": "Generates product photos from text descriptions",
    "webhookId": "whk_xyz789",
    "webhookUrl": null,
    "isActive": true,
    "isPublished": false,
    "executionCount": 42,
    "lastExecutedAt": "2025-03-15T10:30:00.000Z",
    "tags": ["image", "product"],
    "createdAt": "2025-01-10T08:00:00.000Z",
    "updatedAt": "2025-03-15T10:30:00.000Z"
  }
]
```

## Create a Workflow

```
POST /api/v1/workflows
```

Creates a new workflow. The `nodes` and `edges` arrays define the workflow graph.

Build nodes from the [node catalog](/docs/api/nodes) (`GET /api/v1/nodes`). It declares every node's ports, config keys, enum options, defaults, and pricing. The write path lints the graph against it: unknown node types are rejected, unknown config keys and bad enum values come back as warnings in the response `meta`.

**Request Body**

| Field         | Type       | Required | Description                                                                                  |
| ------------- | ---------- | -------- | -------------------------------------------------------------------------------------------- |
| `name`        | `string`   | Yes      | Workflow name                                                                                |
| `description` | `string`   | No       | Workflow description                                                                         |
| `nodes`       | `Node[]`   | Yes      | Array of workflow nodes                                                                      |
| `edges`       | `Edge[]`   | Yes      | Array of connections between nodes                                                           |
| `tags`        | `string[]` | No       | Tags for organizing workflows                                                                |
| `isActive`    | `boolean`  | No       | Whether the workflow is active (default: `true`)                                             |
| `webhookId`   | `string`   | No       | Stored as-is. Inbound trigger URLs are not available yet, see [Webhooks](/docs/api/webhooks) |
| `mediaId`     | `string`   | No       | Project/media ID to scope the workflow to                                                    |

`mediaId` must name a media you own, belong to the team of, or hold edit access to. Anything else, including an id that does not exist, is refused with `403 forbidden` and nothing is saved.

**Request**

```bash
curl -X POST https://www.wireflow.ai/api/v1/workflows \
  -H "Authorization: Bearer sk-your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Workflow",
    "description": "Generates images from prompts",
    "nodes": [
      {
        "id": "node-1",
        "type": "basedNode",
        "position": { "x": 0, "y": 0 },
        "data": {
          "label": "Text to Image",
          "nodeType": "model:fal:text-to-image",
          "category": "model",
          "prompt": "A sunset over mountains"
        }
      }
    ],
    "edges": [],
    "tags": ["image"]
  }'
```

**Response** `201 Created`

```json
{
  "id": "cm1abc123",
  "name": "My Workflow",
  "description": "Generates images from prompts",
  "nodes": [...],
  "edges": [],
  "webhookId": null,
  "isActive": true,
  "tags": ["image"],
  "userId": 1,
  "createdAt": "2025-03-20T12:00:00.000Z",
  "updatedAt": "2025-03-20T12:00:00.000Z"
}
```

## Get a Workflow

```
GET /api/v1/workflows/{id}
```

Retrieves a single workflow by ID. Run history is not included: use `GET /api/v1/workflows/{id}/executions`.

**Request**

```bash
curl https://www.wireflow.ai/api/v1/workflows/cm1abc123 \
  -H "Authorization: Bearer sk-your-api-key"
```

**Response** `200 OK`

```json
{
  "id": "cm1abc123",
  "name": "My Workflow",
  "description": "Generates images from prompts",
  "nodes": [...],
  "edges": [...],
  "webhookId": null,
  "isActive": true,
  "linkPermission": "PRIVATE",
  "isPublished": false,
  "publishedSlug": null,
  "publishedAt": null,
  "publishSettings": null,
  "tags": ["image"],
  "createdAt": "2025-03-20T12:00:00.000Z",
  "updatedAt": "2025-03-20T12:00:00.000Z",
  "isOwner": true,
  "canEdit": true,
  "canDuplicate": true,
  "userRole": "OWNER"
}
```

**Readers who are not the owner or an editor.** A VIEW link (every published template) lets an anonymous visitor read the workflow, nodes included. What they do not get is the creator's integration and billing settings: `webhookId`, `webhookUrl` and `lastEditorHeartbeatAt` are omitted, `secrets` is `null`, and `publishSettings` is reduced. Every key that names a secret, token, key or password is removed at any depth, along with `webhookUrl`, `dailyCreditBudget` and `executionEngine`. Owners and editors get the full response above.

## Update a Workflow

```
PUT /api/v1/workflows/{id}
```

Updates an existing workflow. All fields are optional; only include the fields you want to change.

When sending `nodes`/`edges`, build them against the [node catalog](/docs/api/nodes). Responses from `GET /api/v1/workflows/{id}` round-trip cleanly: canvas runtime fields (`width`, `height`, `positionAbsolute`, `selected`, `dragging`) are stripped on read and on write, so you never persist stale view state.

The GET body can be sent back whole. `linkPermission`, `isPublished`, `publishedSlug`, `publishedAt` and `publishSettings` are not writable here, but a value equal to the stored one is accepted and ignored. Changing one returns `400 field_not_writable`, naming every changed field and the endpoint that owns it: `POST /api/v1/workflows/{id}/visibility` for `linkPermission`, `POST /api/v1/workflows/{id}/publish` for the rest. If they differ because the workflow changed after your GET (the body's `updatedAt` or `baseUpdatedAt` is older than the stored one), you get `409 stale_write` instead: reload and reapply.

Two more fields need care on a whole-body round trip:

- `secrets` is shown only to the workflow's creator. Everyone else gets `null`, and a `null` sent back by anyone but the creator is ignored, so it never clears the creator's secrets. A creator's `null` does clear them, so do the round trip with a key that has `workflows:read`: without it GET serves you as an anonymous visitor and `secrets` comes back `null`.
- `poster` sent back pins the stored thumbnail. Omit it if you want the graph's newest image to refresh it.

**Request Body**

| Field         | Type             | Description                                                                                                    |
| ------------- | ---------------- | -------------------------------------------------------------------------------------------------------------- |
| `name`        | `string`         | Workflow name                                                                                                  |
| `description` | `string \| null` | Workflow description                                                                                           |
| `nodes`       | `Node[]`         | Updated nodes array                                                                                            |
| `edges`       | `Edge[]`         | Updated edges array                                                                                            |
| `tags`        | `string[]`       | Tags                                                                                                           |
| `isActive`    | `boolean`        | Active status                                                                                                  |
| `webhookId`   | `string`         | Stored as-is, see [Webhooks](/docs/api/webhooks)                                                               |
| `webhookUrl`  | `string`         | Stored as-is. Nothing is sent to it: for a completion callback, pass `webhook` to [`POST /run`](/docs/api/run) |
| `folderId`    | `string \| null` | Move to a folder                                                                                               |

`folderId` moves the workflow into a folder, or out of its folder with `null`. Only the workflow owner can move it, and the folder must be yours or belong to a team you are in (403 otherwise, 404 if it does not exist). Folders belong to one workspace, so a folder in another workspace moves the workflow there: its team becomes the folder's team (personal for your personal folders), as long as you are a current member of that team (400 `folder_workspace_mismatch` otherwise). Anyone who could reach the workflow only through its old team loses access. Direct shares and the link permission are unchanged, you stay the owner, runs bill the new team's pool, and each of the workflow's automations is re-pointed to the account its runs will charge (a teammate's automation never moves to a team they are not in). `GET /api/v1/folders` lists the folders of your active workspace only. `POST /api/v1/workflows` with an `id` that changes `folderId` follows the same rule, except it never changes the workflow's workspace: a folder in another workspace is a 400 `folder_workspace_mismatch` there, so use this PUT. Like the guarded fields above, a `folderId` that differs from the stored one because the workflow changed after your GET (the body's `updatedAt` or `baseUpdatedAt` is older) gets `409 stale_write`, so a stale body never undoes someone else's move: reload and reapply.

A workflow created through this API is personal, while `GET` and `POST /api/v1/folders` use your active workspace. So when your active workspace is a team, every folder the API can list or create is in that team, and moving an API-created workflow into one with `PUT /api/v1/workflows/:id { folderId }` moves the workflow into the team. To keep API-created workflows personal while a team is active, create the folder in your personal workspace instead: `POST /api/v1/folders` takes an optional `teamId`, which must be a team you are currently a member of (your personal team counts; any other team is a 404, whether or not it exists). Without `teamId` the folder goes in your active workspace, as before.

The rest of a GET body is accepted and ignored: `updatedAt`, the per-reader fields (`isOwner`, `canEdit`, `canDuplicate`, `userRole`, `media`, `meta`), and any other stored field sent back with the value GET returned (or in a body older than the workflow, since counters like `views` move on their own). Any other key, including a read-only field you changed (`teamId`, say), is ignored and named in `meta.warnings`, never dropped silently. The warning lists every accepted key. Those keys will become a `400 unknown_parameter`.

**Request**

```bash
curl -X PUT https://www.wireflow.ai/api/v1/workflows/cm1abc123 \
  -H "Authorization: Bearer sk-your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Updated Workflow Name",
    "isActive": false
  }'
```

**Response** `200 OK`

Returns the full updated workflow object.

## Delete a Workflow

```
DELETE /api/v1/workflows/{id}
```

Soft-deletes a workflow. The workflow is deactivated and marked as deleted but can be recovered by support.

**Request**

```bash
curl -X DELETE https://www.wireflow.ai/api/v1/workflows/cm1abc123 \
  -H "Authorization: Bearer sk-your-api-key"
```

**Response** `200 OK`

```json
{
  "success": true
}
```

## Data Types

### Node Object

Nodes represent AI models, inputs, and utilities on the workflow canvas. Every node uses the React Flow `Node` structure.

| Field      | Type     | Required | Description                                     |
| ---------- | -------- | -------- | ----------------------------------------------- |
| `id`       | `string` | Yes      | Unique node identifier (e.g. `"node-1"`)        |
| `type`     | `string` | Yes      | Always `"basedNode"` for standard nodes         |
| `position` | `object` | Yes      | Canvas layout coordinates: `{ "x": 0, "y": 0 }` |
| `data`     | `object` | Yes      | Node configuration (see below)                  |

**`data` fields:**

| Field      | Type     | Required | Description                                                                        |
| ---------- | -------- | -------- | ---------------------------------------------------------------------------------- |
| `label`    | `string` | Yes      | Display name shown on the node                                                     |
| `category` | `string` | Yes      | Node category: `"model"`, `"input"`, `"utility"`, `"output"`, `"logic"`            |
| `nodeType` | `string` | Yes      | Identifies the model or utility (see common values below)                          |
| `config`   | `object` | No       | Node settings (prompt, seed, guidance_scale, etc.). **This is the canonical bag.** |
| `params`   | `object` | No       | Legacy read location. Avoid — see the warning below.                               |
| `inputs`   | `Port[]` | No       | Input port definitions: `[{ "id": "image", "label": "Image", "type": "IMAGE" }]`   |
| `outputs`  | `Port[]` | No       | Output port definitions: `[{ "id": "image", "label": "Image", "type": "IMAGE" }]`  |

> **Write settings to `config`, not `params`.** At run time the resolver merges
> `{...config, ...params, ...inputs}`, so `params` WINS over `config` — but
> normalization and rehydration only ever touch `config`. A value written to
> `params` therefore overrides the normalized one and is never itself
> normalized, which is how a node ends up running something the canvas does not
> show. `params` is a legacy read location kept for old graphs.

The `position` field controls where the node appears on the visual canvas. It has no effect on execution — the API uses it to persist layout when saving workflows.

### Layout ships with the edit

`position` has no effect on execution, but a board is a picture a human opens,
so a graph left as a pile of stacked cards discredits the work that produced it.
Do not guess coordinates: card height is browser state and no caller outside the
tab can get it right.

- **Read real geometry.** `GET /api/v1/workflows/{id}` returns every node with a
  read-only `dims: { width, height, source }`. Branch on `source`, never on the
  numbers: `measured` is a real rendered box, `declared` is a size the user set
  by dragging a resize handle, `estimated` is our per-type model.
- **`layoutPending` is the routine outcome — branch on it first.** After a
  structural edit `meta.warnings` normally carries a `layoutPending:` line, and
  it means NOTHING WAS MOVED: the graph is exactly as you wrote it. Two
  independent reasons produce it, and both are common. One, an editor tab may
  be holding the board — repositioning a board an open tab holds makes the
  editor's conflict self-heal read a harmless 409 as a foreign edit and wedges
  that user behind a banner they cannot clear, and the server cannot reliably
  rule the tab out, so it assumes the worst. Two, server-side arrangement is
  flag-gated and OFF by default until a durable editor-presence signal exists.
  Plan for this path.
- **When arrangement is enabled and the board is provably quiet**, a write that
  adds, removes or rewires nodes is instead ARRANGED by the write itself —
  topological columns, producers left of consumers, group frames fitted to
  their members with one uniform gap — and the response echoes the new
  positions with its own `meta.warnings` line. A write that changes only VALUES
  (a prompt, a result, a rename) is not a topology change and is never
  rearranged. Pass `autoLayout: false` to keep your own coordinates.
- **Your grouping is never changed, on either path.** It is a semantic choice
  only you can make, and a grouping the wiring disagrees with comes back as an
  advisory, never a silent regroup.
- **Adds are guarded for you either way.** A node this PUT ADDS that arrives
  with no position, stacked on another, or burying one is repositioned before
  it is persisted, and every move is named in the response's `meta.warnings`
  with the node id and its new coordinates. Only nodes the write added are ever
  moved, because an existing position belongs to the user.
- **Removals are still yours on the pending path.** A delete leaves a hole and a
  rewire crosses edges, and neither shows up anywhere else in the response body.
- **Arrange on demand, any time.** `POST /api/v1/workflows/{id}/layout` is the
  same engine as the in-app Auto Layout button and stays available whether or
  not the write arranged for you — it is what you call after a `layoutPending`.
  It is group-aware, and it refuses any board over
  250 nodes or 750 edges with a 422. Pass a `scope` array of node ids to lay
  out just the part you changed.
- **What "geometry only" actually covers.** The layout authors node positions, a
  group box's size, and a group box's derived `childNodeIds`, and it DELETES the
  legacy ReactFlow `parentId`/`parentNode`/`extent` keys from any node it lays
  out (it applies absolute positions, which ReactFlow would otherwise read as
  parent-relative; only weavy-imported boards carry them). Your `config`,
  `prompt`, `type`, `result` and the edge list are never touched, so you do not
  need to re-verify the graph afterwards.

An edit is not done until the board is tidy.

**Common `nodeType` values:**

| nodeType                                       | Description               |
| ---------------------------------------------- | ------------------------- |
| `generate:flux_2`                              | Flux 2 text-to-image      |
| `generate:flux_2_pro`                          | Flux 2 Pro text-to-image  |
| `generate:nano_banana_pro`                     | Nano Banana Pro image gen |
| `generate:imagen3`                             | Google Imagen 3           |
| `generate:imagen4`                             | Google Imagen 4           |
| `generate:bytedance_seedream_v4_text_to_image` | Seedream v4 text-to-image |
| `edit:flux_2_edit`                             | Flux 2 image editing      |
| `edit:nano_banana_pro_edit`                    | Nano Banana Pro editing   |
| `edit:bytedance_seedream_v4_edit`              | Seedream v4 editing       |
| `video:kling_video_2_5_i2v`                    | Kling 2.5 image-to-video  |
| `talking:veed_fabric`                          | VEED Fabric talking head  |
| `input:text`                                   | Text input                |
| `input:image`                                  | Image/media upload        |
| `output:preview`                               | Preview output            |
| `utility:prompt_concat`                        | Prompt concatenator       |

### Edge Object

Edges define connections between node ports.

| Field          | Type     | Required | Description                                            |
| -------------- | -------- | -------- | ------------------------------------------------------ |
| `id`           | `string` | Yes      | Unique edge identifier (e.g. `"edge-1"`)               |
| `source`       | `string` | Yes      | Source node ID                                         |
| `target`       | `string` | Yes      | Target node ID                                         |
| `sourceHandle` | `string` | No       | Output port ID on the source node (e.g. `"out-image"`) |
| `targetHandle` | `string` | No       | Input port ID on the target node (e.g. `"in-image"`)   |

**Validation limits:** Max 1500 edges and a 4 MB request body per workflow (over 100 nodes is a lint warning, not a refusal). Over the edge cap, continue the next section on a second board. Node data nesting is limited to 10 levels deep.

## Permissions

Workflow access is determined by:

1. **Workflow creator** — full access
2. **Media/project owner** — full access if the workflow belongs to their project
3. **Team member** — access if the workflow belongs to a team project
4. **Explicit permission** — granted via sharing (EDIT or OWNER)

Delete requires ownership — team members need the OWNER role on the team to delete workflows.

---

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