Skip to main content
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.
Prerequisites:
  • Authentication configured (see Authentication guide)
1

Create a workflow

Create an empty workflow container.Endpoint: POST /api/workflows
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"]
2

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, and returns the agent’s output through a response block.Endpoint: PUT /api/workflows/{id}/graph
# 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())
Unknown block types are rejected with 400 Unknown block type. See the Workflows concept page for what each block does.
3

Execute the workflow

Run the workflow with input data. Returns execution results.Endpoint: POST /api/workflows/{id}/execute
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)
4

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.
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 will return 401).
# 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"]
The deploy endpoint returns {"ok": true, "state": "deployed"} and arm returns {"ok": true, "armed_until": "<iso-timestamp>"} — neither response contains the webhook credentials.
5

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.
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) — 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 .
Endpoint: POST /api/webhooks/{webhook_id}
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.
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())

What’s Next

Workflows (Copilot)

Build workflows with natural language.

Workflows

Understand block types and graph execution.

Workflows API Reference

Full endpoint documentation.