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

# Programmatic Workflows

> Operate Sixtyfour workflows entirely outside the Workflow Editor — define and run with the Workflow Definition JSON.

The Workflow Editor can provide the JSON definition behind every workflow. That same JSON is what the Workflows API consumes for `create_workflow`, `update_workflow`, and `run`. Pair it with the Workflow ID and you can operate a workflow entirely from your own code — no UI required.

<Tip>To build workflows in the UI, see [Building Workflows](/guides/building-workflows).</Tip>

## The Workflow Definition JSON

The JSON shown in the editor's **Workflow API Reference** modal is the canonical, server-side representation of a workflow.

A workflow definition is a directed graph of blocks and edges:

```json theme={null}
{
  "workflow_name": "Enrich inbound leads",
  "workflow_description": "Webhook → enrich people → outgoing webhook",
  "workflow_definition": {
    "blocks": [
      {
        "block_id": "1f2c3a4d-...-source",
        "sequence_number": 1,
        "block_type": "io",
        "block_name": "webhook",
        "specs": {
          "input_schema": { "email": "string", "company": "string" },
          "dataframe_type": "LEAD"
        }
      },
      {
        "block_id": "2a3b4c5d-...-enrich",
        "sequence_number": 2,
        "block_type": "enrichment",
        "block_name": "lead_enrichment",
        "specs": {
          "return_fields": { "title": "string", "linkedin_url": "string" }
        }
      },
      {
        "block_id": "3a4b5c6d-...-deliver",
        "sequence_number": 3,
        "block_type": "output",
        "block_name": "outgoing_webhook",
        "specs": {
          "url": "https://example.com/hooks/sixtyfour",
          "timeout_seconds": 10
        }
      }
    ],
    "edges": [
      { "from_block_id": "1f2c3a4d-...-source", "to_block_id": "2a3b4c5d-...-enrich" },
      { "from_block_id": "2a3b4c5d-...-enrich", "to_block_id": "3a4b5c6d-...-deliver" }
    ]
  }
}
```

### Field reference

| Field                        | Type              | Description                                                                         |
| ---------------------------- | ----------------- | ----------------------------------------------------------------------------------- |
| `workflow_name`              | string            | Human-readable name shown in the editor and `GET /workflows`.                       |
| `workflow_description`       | string            | Free-form description.                                                              |
| `workflow_definition.blocks` | array             | All processing steps in the graph. Order is irrelevant — execution follows `edges`. |
| `workflow_definition.edges`  | array             | Directed connections (`from_block_id` → `to_block_id`). Defines data flow.          |
| `id`                         | string (optional) | Pre-generated UUID for the workflow. Omit to let the server assign one.             |

Each block has the following shape:

| Field             | Type              | Description                                                                                                                                                             |
| ----------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `block_id`        | string (optional) | UUID. The server normalizes missing or non-UUID IDs and rewrites edges to match.                                                                                        |
| `sequence_number` | integer           | UI ordering hint. Does not affect runtime execution.                                                                                                                    |
| `block_type`      | string            | Free-form category (`io`, `transform`, `enrichment`, `signal`, `output`). The runtime ignores this for dispatch.                                                        |
| `block_name`      | string            | The executor identifier. This is what the runtime uses to resolve a block. See [Workflow Blocks Reference](/api-reference/workflows/workflow-blocks) for the full list. |
| `specs`           | object            | Block-specific configuration. Schema varies per `block_name`.                                                                                                           |

<Warning>`block_name` selects the executor — renaming it will break execution with `Unknown block type: ...`. `block_type` is a free-form label; renaming it is harmless. Treat `block_name` as immutable when hand-editing JSON.</Warning>

## Get the JSON for an existing workflow

Two ways to get the JSON:

* From the editor: open a workflow in [app.sixtyfour.ai](https://app.sixtyfour.ai), click the menu next to **Run**, choose **View Workflow Definition**, and copy the JSON. The Workflow ID appears at the top of the modal.
* From the API: `GET /workflows/{workflow_id}` returns the same definition under `workflow_definition`.

```bash cURL theme={null}
curl -X GET "https://api.sixtyfour.ai/workflows/WORKFLOW_ID" \
  -H "x-api-key: YOUR_API_KEY"
```

The response includes `id`, `name`, `description`, `status`, and the full `workflow_definition` graph. The `workflow_definition` field can be passed directly back into `create_workflow` or `update_workflow` without modification.

## Programmatic flows

The flows below all use the same JSON body shape (`CreateWorkflowRequest`).

### Run an existing workflow

Pair a stored Workflow ID with `POST /workflows/run` to start a job from any client. The workflow lives server-side; your code triggers it and consumes results.

<CodeGroup>
  ```python Python theme={null}
  import os, requests

  BASE = "https://api.sixtyfour.ai"
  HEADERS = {"x-api-key": os.environ["SIXTYFOUR_API_KEY"], "Content-Type": "application/json"}

  resp = requests.post(
      f"{BASE}/workflows/run",
      params={"workflow_id": "WORKFLOW_ID"},
      headers=HEADERS,
      json={"webhook_payload": [{"email": "ada@example.com", "company": "Acme"}]},
  )
  resp.raise_for_status()
  print("job_id:", resp.json()["job_id"])
  ```

  ```javascript JavaScript theme={null}
  const BASE = "https://api.sixtyfour.ai";
  const headers = {
    "x-api-key": process.env.SIXTYFOUR_API_KEY,
    "Content-Type": "application/json",
  };

  const resp = await fetch(`${BASE}/workflows/run?workflow_id=WORKFLOW_ID`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      webhook_payload: [{ email: "ada@example.com", company: "Acme" }],
    }),
  });
  const { job_id } = await resp.json();
  console.log("job_id:", job_id);
  ```
</CodeGroup>

Two optional body fields parameterize each run:

| Field             | Purpose                                                                                                        |
| ----------------- | -------------------------------------------------------------------------------------------------------------- |
| `webhook_payload` | Input rows for workflows whose source block is `webhook`. List or single object.                               |
| `specs_override`  | Overrides the first block's `specs` for this run only. Use when one workflow serves many parameterized inputs. |

The full request/response shape and polling pattern live in [Workflow Execution](/api-reference/workflows/workflows-execution).

<Tip>For workflows whose source block is `read_csv`, upload the CSV to `/storage/csv/upload` and pass the returned `handle_id` as `specs_override.resource_handle_id`. See [Workflow Execution](/api-reference/workflows/workflows-execution).</Tip>

### Create a workflow from JSON

POST the definition to `create_workflow` to register a new workflow server-side. The response includes the assigned `id`, which you then pass to `run`.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.sixtyfour.ai/workflows/create_workflow" \
    -H "x-api-key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d @workflow.json
  ```

  ```python Python theme={null}
  import os, json, requests

  BASE = "https://api.sixtyfour.ai"
  HEADERS = {"x-api-key": os.environ["SIXTYFOUR_API_KEY"], "Content-Type": "application/json"}

  with open("workflow.json") as f:
      body = json.load(f)

  resp = requests.post(f"{BASE}/workflows/create_workflow", headers=HEADERS, json=body)
  resp.raise_for_status()
  workflow_id = resp.json()["id"]
  print("created:", workflow_id)
  ```

  ```javascript JavaScript theme={null}
  import fs from "node:fs/promises";

  const BASE = "https://api.sixtyfour.ai";
  const headers = {
    "x-api-key": process.env.SIXTYFOUR_API_KEY,
    "Content-Type": "application/json",
  };

  const body = JSON.parse(await fs.readFile("workflow.json", "utf8"));
  const resp = await fetch(`${BASE}/workflows/create_workflow`, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
  });
  const { id } = await resp.json();
  console.log("created:", id);
  ```
</CodeGroup>

## Choose your own Workflow ID

You can pass an optional `id` (a valid UUID) in the request body to choose your own Workflow ID instead of letting the server assign one. The endpoint returns `409` if that ID is already taken.

## Deploy + Run from CI

A typical deploy script saves the workflow definition with `update_workflow`, then triggers a run. `update_workflow` creates the workflow if `workflow_id` doesn't exist and updates it otherwise, so the same script is safe to run on every deploy.

```python Python theme={null}
import os, json, requests

BASE = "https://api.sixtyfour.ai"
HEADERS = {"x-api-key": os.environ["SIXTYFOUR_API_KEY"], "Content-Type": "application/json"}
WORKFLOW_ID = os.environ["WORKFLOW_ID"]

with open("workflow.json") as f:
    body = json.load(f)

u = requests.post(
    f"{BASE}/workflows/update_workflow",
    params={"workflow_id": WORKFLOW_ID},
    headers=HEADERS,
    json=body,
)
u.raise_for_status()

r = requests.post(
    f"{BASE}/workflows/run",
    params={"workflow_id": WORKFLOW_ID},
    headers=HEADERS,
    json={"webhook_payload": [{"email": "ada@example.com", "company": "Acme"}]},
)
r.raise_for_status()
print("running:", r.json()["job_id"])
```

`update_workflow` runs the same schema and edge checks as `create_workflow` and rejects invalid graphs with `400` before saving — so the save step doubles as a validation gate. To deliver completed run results to your own URL, add an `outgoing_webhook` block to the workflow — see [Outgoing Webhooks](/api-reference/webhooks/outgoing). Polling, result download, and cancellation are documented in [Workflow Execution](/api-reference/workflows/workflows-execution).

## Authentication

All programmatic endpoints accept either an API key or an OAuth client credential. `webhook_payload` and `specs_override` require machine authentication (API key or OAuth) — they are rejected for JWT (browser session) requests.

The credential also determines ownership: the JSON definition has no `org_id` field, so a workflow created with a given API key belongs to that key's organization. To deploy the same JSON into a different organization, POST it using that organization's API key.

| Auth method              | When to use                                                            | Header                               |
| ------------------------ | ---------------------------------------------------------------------- | ------------------------------------ |
| API key                  | Server-to-server scripts, CI, single-tenant deploys                    | `x-api-key: YOUR_API_KEY`            |
| OAuth client credentials | Multi-tenant integrations, third-party apps acting on behalf of a user | `Authorization: Bearer ACCESS_TOKEN` |

See [Get API Key](/get-api-key) for API keys and [OAuth Clients](/guides/oauth-clients) for OAuth setup.
