/workflows in the app) draws it; the backend validates it, compiles it to a Google ADK
Workflow and runs it, streaming one event per node.
No user code runs inside a workflow: routing rules and {{...}} references are evaluated
by the engine itself.
In the Studio
- New workflow — start from scratch (a manual trigger) or from the Tool → Router → Agents template, and give it a name.
- Drag nodes from the palette, or click one to add it. Link them; on a routing node, pick the route of each edge — an edge without a route is drawn red.
- Select a node to configure it. Every text field offers the upstream nodes as chips:
one click inserts the right
{{...}}reference. - Test opens the run panel: enter a payload (JSON, text or form), Run, and follow each node live — status, duration, route taken, output. Replay steps through the last run on the canvas.
- Publish once the validation badge reads Graph valid. This arms the trigger.
Next-step suggestions
Select a node and the canvas lists, under Next step, up to three node types that usually come after it, the most likely first. Click one: the node is added, linked, and its main field already reads the node it hangs off —{{trigger1}} in an Agent’s
input, an Output’s value or a Notification’s body. Nothing is published.
- While the workflow has no Output, Output comes first whenever it is one of the options.
- On a routing node (router, classifier, condition, try), the suggestion fills the first declared branch that has no edge yet, one branch at a time.
- Nothing is suggested after an Output, or after a node that is already linked.
Suggest with AI
When the deployment turns it on, Suggest with AI under the list asks the shared model for up to two complete nodes, configured from the workflow’s name, description and nodes. The model is only called when you click. Its proposals, marked ✨, come first, and each one replaces the rule suggestion of the same type. Hover one to read why it is proposed. The label and the reason are written in the language of the workflow’s name. Every proposal is checked before you see it, and dropped rather than repaired:- it must pass the checks a node of a saved graph passes;
- it may only use one of your agents, and only reference nodes upstream of it;
- the branch it is wired to is computed by the server, never chosen by the model.
agent, classifier, extract, rag, router, condition,
convert, set, notification (channel app only) and output. Without any agent
on your account, the four types that need one are not offered at all. It never proposes
a trigger, a tool, an HTTP request, an approval or a loop, try, merge or subworkflow node.
What reaches the model is the structure of the graph: node types, ids and labels,
branch names, field names (the trigger’s sample payload, Extract and Set nodes), a Tool
node’s tool and an HTTP node’s method, plus your agents’ ids, names and descriptions.
Sample values, templates, URLs, headers and recipients are never sent.
Self-hosting. The button only shows when both the shared model is configured
(
DEFAULT_LLM_MODEL and DEFAULT_LLM_API_KEY) and the feature is on:
Import and export a JSON file
Export in the Studio downloads the workflow as a JSON file named after it:{version, nodes, edges} — the body the
Definitions API stores. A file without nodes and edges is refused.
An imported workflow always starts as a draft. The file carries no trigger URL or HMAC
secret: publishing the copy issues new ones. Agent, tool and workflow ids are kept as
they are: in a graph from another account, point them at your own — an agent that is not
yours stops the run with agent_not_found.
Lifecycle of a workflow
Every save creates a revision (
edit, publish, unpublish, restore). Restoring a
revision always puts the workflow back to draft. Automatic triggers only ever run the
published version, on behalf of the workflow owner.
Nodes
Rule operators for
router, condition and a loop’s until: eq, ne, gt, gte,
lt, lte (numeric), contains (case-insensitive text), in (value in a list),
exists. A rule whose field is empty tests the node’s input.
Structure rules
- Node
id: a letter, then up to 63 letters, digits,_or-. Ids are unique. - At most one trigger per workflow. Without one, the workflow is manual.
- No cycles: repeat work with a
loop, not a back edge. - Only
router,classifier,conditionandtryput arouteon their outgoing edges, and only a route they declare.conditionandtryallow one edge per route. - An
outputnode ends the flow: it cannot have outgoing edges. - A
looportrybody is its own graph with exactly one trigger. Atrybody cannot contain anothertryor aloop. - A subworkflow cannot call its own workflow; chains stop at depth 3.
Final output of a run
The run output is the value of theoutput node reached, or, without one, the output of
the last node reached. A run may end on several nodes — parallel branches from a fan-out,
or two output nodes — and the output is then an object keyed by node id, like a merge.
Only the nodes actually reached appear: the branch a router, classifier, condition
or try did not take contributes nothing.
An output node that is reached decides alone: the other branches’ last nodes are then
left out of the result.
A route that is chosen but has no outgoing edge ends the run normally; the output is then
empty.
References between nodes
A string in a node’sconfig can read the output of an upstream node with
{{node_id}} or {{node_id.path.to.value}}. Path segments are object keys or list
indexes ({{items.0.name}}).
- Alone in the field, the reference keeps the value’s type:
"{{trigger.amount}}"gives the number1500, an object stays an object. - Inside text, it is converted to text: objects and lists become JSON, a missing value becomes an empty string.
- The referenced node must be an ancestor of the node that reads it, otherwise the graph is refused (“is not upstream, its output does not exist yet”).
{{node_id}} always works. A path is checked against what the engine guarantees:
A path that crosses a plain value at run time (for example
{{agent1.amount}} when the
agent answered with text) stops the run with template_ref_invalid instead of silently
producing nothing.
Inside a loop body, the body’s trigger receives {item, index, previous} — previous is
the previous iteration’s output, or the loop input on the first turn. An until
condition reads {{iteration.output...}} and {{iteration.index}} only.
Example
A webhook receives an order; orders above 1,000 are reviewed by an agent.AGENT_ID is the id of one of your agents.
With the payload {"customer": "ACME", "amount": 1500}, order.amount stays the number
1500, the condition takes the true route and the agent receives
Review the order from ACME for 1500 EUR.
Triggers
The trigger node’sconfig.kind decides how a published workflow starts. The payload is
what {{trigger...}} reads.
Email and file triggers use the account connected under
Integrations; without it the trigger stays inactive
(
integration_missing). The first scan of a watched folder only records the files
already there; later scans start one run per new or modified file.
A schedule tick is skipped while the previous scheduled run is still running.
An agent_tool workflow becomes a tool named workflow:<tool_name> that agents can call.
The call waits for the run (120 s at most) and returns {run_id, output}, or an error.
Workflows that call each other through agent_tool or workflow_done stop at a chain of
5, and never loop back to a workflow already in the chain.
Webhook and form URLs
Publishing a webhook or form workflow issues a secret URL, shown in the Studio and byGET /api/workflows/{workflow_id}/triggers:
- webhook:
POST /api/hooks/workflows/{token}— answers202 {"run_id": "..."}; - form:
/forms/{token}in the app, submitted toPOST /api/hooks/forms/{token}.
POST /api/workflows/{workflow_id}/triggers/rotate issues a new token (the old URL stops
working). With hmac: true, the HMAC secret is shown only when it is generated or
rotated; each call must then carry X-Apowerb-Signature: sha256=<hex>, the HMAC-SHA256 of
the raw body with that secret.
404, a bad signature 401, a body
over 256 KiB 413, and more than 60 calls a minute per workflow 429. A public form
accepts 10 submissions a minute per visitor; fields that the form does not declare are
dropped.
Running and following a run
POST /api/workflows/defs/{workflow_id}/run with {"payload": ...} runs the saved
version and streams Server-Sent Events:
Nodes run one at a time, in dependency order: branches after a fan-out are correct but
serialized. Each node output is capped at 1 MiB (
node_output_too_large); HTTP responses
too. Stop a run with POST /api/workflows/{wid}/cancel, wid being the run id carried by
run_started: the current node finishes, no further node starts.
Runs are kept with a status — running, success, error, cancelled — and listed
newest first by GET /api/workflows/runs; each entry carries its trigger, the cause of its
failure and input_available, which tells whether its input is still on disk and the run can
therefore be replayed. GET /api/workflows/runs/{run_id} returns one run. Per-node outputs
are only in the event stream, not stored.
POST /api/workflows/runs/{run_id}/replay starts a new run from the stored input; a
successful run needs ?force=true, since its side effects already happened.
Error codes
Errors carry a stablecode and params (never the raw exception). The most common:
Security notes
- The
httpnode refuses private and internal addresses and setsHostitself. A4xxor5xxanswer does not fail the node: test{{node.status}}downstream.Authorization,Proxy-Authorization,CookieandX-API-Keyheaders cannot be written in a graph, since anyone who reads or exports it would see them; authenticated HTTP calls are not available yet. - Notifications are limited to 30 sends an hour per owner, all channels together.
appgoes to the workflow owner,teamsto the owner’s Teams webhook. - A subworkflow or
workflow_donesource must belong to the same owner; other owners’ workflows answer as not found.
Definitions API
Runs and triggers
All endpoints require authentication and only see the caller’s workflows.
