Workflows as code
Overview
A workflow is stored as a graph of nodes and edges. Writing that graph by hand means you have to keep edge IDs, edge_order lists, and condition objects consistent yourself. The Workflow builder in the Python and JavaScript SDKs lets you describe the same graph with chained method calls. It then produces the workflow value that agents.create() and agents.update() already accept.
Use it to:
- Build a workflow in code and review it like any other code change.
- Load an agent’s workflow, change one node, and push it back without disturbing the rest of the graph.
- Store workflows as JSON in version control and validate them in CI.
The builder runs entirely in the SDK. It calls no new API endpoints, and anything it builds can be opened and edited in the dashboard’s visual editor.
Requirements
- Python: the
elevenlabspackage. The builder is imported fromelevenlabs.agent_workflows. - JavaScript: the
@elevenlabs/elevenlabs-jspackage. The builder is exported from the package root.
Build a workflow
The Workflow object stands for the start node. Each verb adds a node and returns it, so you can chain the next step from it:
The routing methods decide which edge leads to the next node:
Verbs called directly on the workflow branch from the start node with an unconditional edge. This example routes callers to an order lookup, retries identification if the lookup fails, and transfers VIP or frustrated callers:
build() validates the whole graph and returns the request model. It also:
- Gives the start node the ID
start_node, which the API requires. - Generates edge IDs in the form
source__target. - Fills in each node’s
edge_orderin the order you added the edges. - Positions any node that has no position, so the graph is laid out left to right in the visual editor.
Write a workflow as one chain
up() returns the node the current one was chained from, so you can describe a whole workflow in one expression instead of keeping each node in a variable. Inside a single chain, use go_to() / goTo() with a node ID, because there is no variable to pass:
For a node handle from wf.node(id), up() returns the node the first edge into it comes from. For the start node, it returns the start node.
Node options
Optional settings go in keyword arguments in Python and in a trailing options object in JavaScript. Their names match the node’s fields in the API:
Every verb also accepts position if you want to place the node yourself. Workflow(prevent_subagent_loops=True) in Python, or new Workflow({ preventSubagentLoops: true }) in JavaScript, stops the conversation from re-entering a subagent it has already visited.
Transfer destinations
Conditions
when() takes a plain string, a Condition, or an Expr:
- A string is an LLM condition: the LLM decides whether it holds.
Condition.llm(text, label=...)is the same, with a label shown on the edge in the visual editor. UseCondition.expr(expr, label=...)to label an expression.- An
Expris evaluated against dynamic variables, without calling the LLM.
Build expressions from Expr.variable(name) and Expr.literal(value). Compare them with eq, neq, gt, gte, lt and lte, and do arithmetic with add, sub, mul and div. Combine them with all_of / allOf, any_of / anyOf and if_else / ifElse. Plain values are wrapped in Expr.literal() for you. Expr.llm(description) asks the LLM to extract a value from the conversation; pass type and enum to get a string, a number, or one of a fixed set instead of a boolean.
Loops and backward edges
The API allows one edge between any two nodes, in either direction. To let the conversation return the way it came, give the edge a backward condition with back:
go_to() / goTo() handles the common case for you. When the target node already has an edge leading here, the new edge becomes that edge’s backward condition. In the first example, failed.go_to(identify) is sent as the backward condition of the identify → lookup_order edge. Any other second edge between the same two nodes, and any edge from a node to itself, fails validation.
Edit an existing agent
Workflow.from_agent() / Workflow.fromAgent() loads the workflow of an agent returned by agents.get(). Node IDs, edge IDs, editor positions and edge order are kept, so pushing it back only changes what you changed.
insert_between() / insertBetween() passes your function a branch that carries the replaced edge’s condition. The function returns the last node it added, which is then connected to the original target. The method returns that node, or None / undefined if there is no edge between the two nodes.
Store workflows in version control
to_json_string() / toJSONString() writes the workflow in the API’s wire format: snake_case keys, sorted at every level, with 2-space indentation. It’s the same shape the REST API and the CLI use, and both SDKs write byte-for-byte identical files for the same workflow, so diffs stay small. Workflow.from_json() / Workflow.fromJSON() reads it back, and also accepts a full agent config with a workflow key, such as a file in the CLI’s agent_configs/ directory.
A typical setup:
Seed the file
Load the agent with from_agent() / fromAgent() and write it out, or pull it with elevenlabs agents pull.
Edit
Change the JSON directly, or load it with from_json() / fromJSON() and edit it with the
builder in a script.
Visualize a workflow
The CLI draws a workflow file as a flowchart, so you can check what your code built without opening the dashboard. It accepts a bare workflow file written by to_json_string() / toJSONString(), or an agent config with a workflow key:
By default it draws boxes and arrows in the terminal, with each edge condition numbered on its edge and written out in a legend underneath. Use --format mermaid to print Mermaid source instead, or --format mermaid-md to wrap it in a fenced code block that GitHub renders, for example in a pull request comment:
The command reads local files only, so it needs no API key and can run in CI.
Validation
Chained calls never fail on their own. build(), to_json() / toJSON() and to_json_string() / toJSONString() check the whole graph and raise or throw a single WorkflowValidationError whose issues list every problem, each with a code, a message and the node_id / nodeId it concerns.
Errors include:
- Two edges between the same pair of nodes, or an edge from a node to itself.
- An edge into the start node, or out of an end node.
- An edge from a tool node that isn’t a success or failure edge, or a success or failure edge from any other kind of node.
- Two nodes with the same ID, or an edge to a node that doesn’t exist.
Nodes that can’t be reached from the start node are reported as warnings, through on_warning / onWarning, and don’t stop the build.
Node types newer than your SDK
A workflow loaded from the API or from a file can contain node types that your SDK version doesn’t know yet. They round-trip through from_json() / fromJSON() and to_json_string() / toJSONString() unchanged, and build() reports each one as a warning. The typed request model can’t carry them, so upgrade the SDK, or send the wire format directly: