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

# Build Workflows Programmatically

> Create automated workflows by defining blocks and edges via the API. Blocks are processing steps (LLM calls, agent runs, conditions). Edges connect them into a directed graph.

Workflows are deterministic automation pipelines. Unlike agents (which decide what to do), workflows follow a fixed graph of blocks and edges. This guide creates a simple summarizer workflow and deploys it as a webhook.

<Note>
  **Prerequisites:**

  * Authentication configured (see Authentication guide)
</Note>

<Steps>
  <Step title="Create a workflow">
    Create an empty workflow container.

    **Endpoint:** `POST /api/workflows`

    <CodeGroup>
      ```python Python theme={null}
      response = requests.post(
          f"{BASE_URL}/api/workflows",
          headers=headers,
          json={"name": "Document Summarizer", "description": "Summarizes uploaded documents"},
      )
      workflow = response.json()
      wf_id = workflow["id"]
      ```

      ```typescript TypeScript theme={null}
      const response = await fetch(`${BASE_URL}/api/workflows`, {
        method: "POST",
        headers,
        body: JSON.stringify({ name: "Document Summarizer", description: "Summarizes uploaded documents" }),
      });
      const workflow = await response.json();
      ```

      ```bash cURL theme={null}
      curl -X POST '{BASE_URL}/api/workflows' \
        -H "apikey: {API_KEY}" \
        -H "Authorization: Bearer {API_KEY}" \
        -H "Content-Type: application/json" \
        -d '{"name": "Document Summarizer"}'
      ```
    </CodeGroup>
  </Step>

  <Step title="Define the graph">
    Save blocks (processing steps) and edges (connections between them) as a complete graph. The block registry recognizes these canonical types: `starter`, `agent`, `code`, `condition`, `general_api`, `platform_api`, `response`, `split`, `webhook`, `orchestration` (`function` and `api_call` are accepted as back-compat aliases for `code` and `general_api`). The example below wires a `starter` that injects workflow variables, runs an `agent` block to produce a summary, then returns the agent's output through a `response` block.

    **Endpoint:** `PUT /api/workflows/{id}/graph`

    <CodeGroup>
      ```python Python theme={null}
      # Assumes an agent already exists. Create one with POST /api/agents
      # if you don't have one yet — see guides/build-agent.
      agent_id = "..."  # existing agent UUID

      response = requests.put(
          f"{BASE_URL}/api/workflows/{wf_id}/graph",
          headers=headers,
          json={
              "blocks": [
                  {"id": "start", "type": "starter", "config": {}, "position": {"x": 0, "y": 0}},
                  {"id": "summarize", "type": "agent", "config": {
                      "agent_id": agent_id,
                      "message": "Summarize the following document:\n\n{{variables.text}}",
                  }, "position": {"x": 300, "y": 0}},
                  {"id": "out", "type": "response", "config": {}, "position": {"x": 600, "y": 0}},
              ],
              "edges": [
                  {"source": "start", "target": "summarize"},
                  {"source": "summarize", "target": "out"},
              ],
          },
      )
      print(response.json())
      ```

      ```typescript TypeScript theme={null}
      // Assumes an agent already exists. Create one with POST /api/agents
      // if you don't have one yet — see guides/build-agent.
      const agentId = "..."; // existing agent UUID

      await fetch(`${BASE_URL}/api/workflows/${wfId}/graph`, {
        method: "PUT",
        headers,
        body: JSON.stringify({
          blocks: [
            { id: "start", type: "starter", config: {}, position: { x: 0, y: 0 } },
            { id: "summarize", type: "agent", config: {
              agent_id: agentId,
              message: "Summarize the following document:\n\n{{variables.text}}",
            }, position: { x: 300, y: 0 } },
            { id: "out", type: "response", config: {}, position: { x: 600, y: 0 } },
          ],
          edges: [
            { source: "start", target: "summarize" },
            { source: "summarize", target: "out" },
          ],
        }),
      });
      ```

      ```bash cURL theme={null}
      curl -X PUT '{BASE_URL}/api/workflows/{wf_id}/graph' \
        -H "apikey: {API_KEY}" \
        -H "Authorization: Bearer {API_KEY}" \
        -H "Content-Type: application/json" \
        -d '{"blocks": [...], "edges": [...]}'
      ```
    </CodeGroup>

    <Note>
      Unknown block types are rejected with `400 Unknown block type`. See the [Workflows concept page](/concepts/workflows-concept) for what each block does.
    </Note>
  </Step>

  <Step title="Execute the workflow">
    Run the workflow with input data. Returns execution results.

    **Endpoint:** `POST /api/workflows/{id}/execute`

    <CodeGroup>
      ```python Python theme={null}
      response = requests.post(
          f"{BASE_URL}/api/workflows/{wf_id}/execute",
          headers=headers,
          json={"variables": {"text": "Your document content here..."}},
      )
      result = response.json()
      print(result)
      ```

      ```typescript TypeScript theme={null}
      const response = await fetch(`${BASE_URL}/api/workflows/${wfId}/execute`, {
        method: "POST",
        headers,
        body: JSON.stringify({ variables: { text: "Your document content here..." } }),
      });
      const result = await response.json();
      console.log(result);
      ```

      ```bash cURL theme={null}
      curl -X POST '{BASE_URL}/api/workflows/{wf_id}/execute' \
        -H "apikey: {API_KEY}" \
        -H "Authorization: Bearer {API_KEY}" \
        -H "Content-Type: application/json" \
        -d '{"variables": {"text": "Your document content here..."}}'
      ```
    </CodeGroup>
  </Step>

  <Step title="Deploy with webhook">
    Two activation modes:

    * **Deploy** (`POST /api/workflows/{id}/deploy`) sets `state = "deployed"`. The webhook accepts unlimited calls until you `undeploy`.
    * **Arm** (`POST /api/workflows/{id}/arm`) leaves `state = "internal"` but opens a 10-minute window during which the webhook accepts exactly one call. After it fires (or expires), you must arm again.

    Use deploy for production integrations; use arm for one-shot tests.

    The `webhook_id` and `webhook_secret` are properties of the **webhook block** itself, stored in `block.config`. The studio editor mints them client-side when you drag in a webhook block. To retrieve them programmatically, fetch the workflow and pull them from the block's config.

    <Note>
      If you create the webhook block via API instead of the UI (`PUT /api/workflows/{id}/graph`), you must mint both `webhook_id` and `webhook_secret` yourself and include them in the block's `config`. The server does not generate them. `webhook_id` MUST be a valid UUID; the trigger endpoint validates it and returns 400 otherwise. `webhook_secret` can be any non-empty string (the studio editor uses `crypto.randomUUID()` for both). A webhook block with no secret is silently un-triggerable (the trigger endpoint returns 401).
    </Note>

    <CodeGroup>
      ```python Python theme={null}
      # Deploy (or arm) the workflow
      requests.post(f"{BASE_URL}/api/workflows/{wf_id}/deploy", headers=headers)
      # OR: requests.post(f"{BASE_URL}/api/workflows/{wf_id}/arm", headers=headers)

      # Look up webhook credentials from the saved graph (assumes one webhook block)
      wf = requests.get(f"{BASE_URL}/api/workflows/{wf_id}", headers=headers).json()
      webhook_block = next(b for b in wf["blocks"] if b["type"] == "webhook")
      webhook_id = webhook_block["config"]["webhook_id"]
      webhook_secret = webhook_block["config"]["webhook_secret"]
      ```

      ```typescript TypeScript theme={null}
      // Deploy (or arm) the workflow
      await fetch(`${BASE_URL}/api/workflows/${wfId}/deploy`, { method: "POST", headers });

      // Look up webhook credentials from the saved graph (assumes one webhook block)
      const wf = await fetch(`${BASE_URL}/api/workflows/${wfId}`, { headers }).then(r => r.json());
      const webhookBlock = wf.blocks.find((b: { type: string }) => b.type === "webhook");
      const webhookId = webhookBlock.config.webhook_id;
      const webhookSecret = webhookBlock.config.webhook_secret;
      ```

      ```bash cURL theme={null}
      # Deploy (or arm)
      curl -X POST '{BASE_URL}/api/workflows/{wf_id}/deploy' \
        -H "apikey: {API_KEY}" \
        -H "Authorization: Bearer {API_KEY}"

      # Read graph and pull webhook_id / webhook_secret from the block whose type=="webhook"
      curl '{BASE_URL}/api/workflows/{wf_id}' \
        -H "apikey: {API_KEY}" \
        -H "Authorization: Bearer {API_KEY}"
      ```
    </CodeGroup>

    <Note>
      The deploy endpoint returns `{"ok": true, "state": "deployed"}` and arm returns `{"ok": true, "armed_until": "<iso-timestamp>"}`. Neither response contains the webhook credentials.
    </Note>
  </Step>

  <Step title="Trigger externally">
    Call the webhook endpoint from any external system. No platform API key is needed; auth is per-webhook via the secret, sent either as a Bearer header (preferred) or a `?token=` query param. The request body becomes the workflow's input variables directly.

    <Note>
      Use one auth mechanism or the other, not both. The server checks the header with `auth_header.lower().startswith("bearer ")` (note the trailing space). If your `Authorization` header is exactly `Bearer ` (trailing space, no token), which is what `Bearer ${secret || ""}` produces when `secret` is falsy, the server reads an empty token and 401s without consulting `?token=`. The `?token=` fallback only fires when the header doesn't start with `Bearer `.
    </Note>

    **Endpoint:** `POST /api/webhooks/{webhook_id}`

    <Note>
      If the workflow is `deployed`, the webhook accepts unlimited calls. If only armed, the webhook accepts exactly one call within the 10-minute window; re-arm to fire it again.
    </Note>

    <CodeGroup>
      ```python Python theme={null}
      response = requests.post(
          f"{BASE_URL}/api/webhooks/{webhook_id}",
          headers={"Authorization": f"Bearer {webhook_secret}", "Content-Type": "application/json"},
          json={"text": "Document to summarize..."},
      )
      print(response.json())
      ```

      ```typescript TypeScript theme={null}
      const response = await fetch(`${BASE_URL}/api/webhooks/${webhookId}`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${webhookSecret}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ text: "Document to summarize..." }),
      });
      console.log(await response.json());
      ```

      ```bash cURL theme={null}
      curl -X POST '{BASE_URL}/api/webhooks/{webhook_id}' \
        -H "Authorization: Bearer {webhook_secret}" \
        -H "Content-Type: application/json" \
        -d '{"text": "Document to summarize..."}'
      ```
    </CodeGroup>
  </Step>
</Steps>

## What's Next

<CardGroup cols={2}>
  <Card title="Workflows (Copilot)" href="/guides/workflows-copilot">
    Build workflows with natural language.
  </Card>

  <Card title="Workflows" href="/concepts/workflows-concept">
    Understand block types and graph execution.
  </Card>

  <Card title="Workflows API Reference" href="/api-reference/workflows">
    Full endpoint documentation.
  </Card>
</CardGroup>
