Repository navigation
Conversation
Port of the JS workflow builder (elevenlabs/elevenlabs-js#480) to Python, in elevenlabs.agent_workflows. Workflow, converse/tool/transfer/end, when/on_success/ on_failure/on_result/go_to, update/remove/insert_between, build() returning AgentWorkflowRequestModel, and to_json/from_json in the API wire format. The shared fixtures are copied from the JS repo and produce byte-identical JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirrors elevenlabs/elevenlabs-js@b72e1e64: a forward-only BFS from start, then each back edge with a visited target and unvisited source continues the search from that source at target depth + 1. Reachability uses the same search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR SummaryMedium Risk Overview Also supports load/edit/serialize: README documents the workflow-as-code API; Reviewed by Cursor Bugbot for commit 9dedb1f. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
🟡 Changes recommended
Explicit nulls are removed from user payloads, and backward transitions bypass validation, allowing invalid workflow conditions and node transitions.
2 open issues
What changed in this PR
Adds a hand-maintained Python workflow builder for defining, editing, and serializing agent workflows through chained calls, integrated with the existing agent create/update APIs.
Changes:
- Adds workflow nodes, conditional routing, expressions, transfers, validation, and automatic layout.
- Supports loading existing workflows and exporting deterministic API-format JSON.
- Adds usage documentation, shared JavaScript fixtures, and Python-specific serialization tests.
| File | Description |
|---|---|
tests/agent_workflows/test_workflow.py |
Tests builder behavior, editing, loading, and request serialization. |
tests/agent_workflows/test_fixtures.py |
Checks shared fixture output and validation errors. |
tests/agent_workflows/fixtures/08-on-result.json |
Covers success-first result routing. |
tests/agent_workflows/fixtures/07-errors.json |
Covers aggregated validation errors. |
tests/agent_workflows/fixtures/06-round-trip.json |
Covers unknown node types and preserved edge metadata. |
tests/agent_workflows/fixtures/05-edit-loaded.json |
Covers editing loaded workflows. |
tests/agent_workflows/fixtures/04-expressions-and-loops.json |
Covers expressions, loops, and backward conditions. |
tests/agent_workflows/fixtures/03-transfers-and-ends.json |
Covers transfer destinations and shared end nodes. |
tests/agent_workflows/fixtures/02-tool-routing.json |
Covers tool outcomes and schema overrides. |
tests/agent_workflows/fixtures/01-docs-example.json |
Defines the basic documentation example. |
tests/agent_workflows/fixture_runner.py |
Translates shared fixture operations into builder calls. |
tests/agent_workflows/__init__.py |
Establishes the workflow test package. |
src/elevenlabs/agent_workflows/workflow.py |
Implements workflow loading, editing, building, and export. |
src/elevenlabs/agent_workflows/validate.py |
Adds graph validation and reachability analysis. |
src/elevenlabs/agent_workflows/node.py |
Implements chained node creation, branching, and updates. |
src/elevenlabs/agent_workflows/node_types.py |
Detects node types supported by generated models. |
src/elevenlabs/agent_workflows/layout.py |
Positions new nodes while preserving existing positions. |
src/elevenlabs/agent_workflows/graph.py |
Stores graph state and applies node options. |
src/elevenlabs/agent_workflows/expr.py |
Builds API-format expressions. |
src/elevenlabs/agent_workflows/errors.py |
Defines structured issues and validation exceptions. |
src/elevenlabs/agent_workflows/destination.py |
Provides phone, SIP, and agent destinations. |
src/elevenlabs/agent_workflows/condition.py |
Provides transition conditions. |
src/elevenlabs/agent_workflows/_wire.py |
Handles wire conversion, canonicalization, and copying. |
src/elevenlabs/agent_workflows/__init__.py |
Exports the public builder API. |
README.md |
Documents building, editing, and storing workflows. |
.fernignore |
Protects the hand-maintained package from regeneration. |
Review effort: Balanced
to_wire() dropped every None, so a constant schema override such as
{"customer_id": None} lost its key. jsonable_encoder already leaves out unset
model fields, and apply_options filters omitted options, so the extra pass
only removed real data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirrors elevenlabs/elevenlabs-js@61c67a93. The backend's request model requires these, so build() output was rejected: - The start node's id is "start_node", not "start". - Only one edge may connect two nodes, in either direction. A go_to back to a node that already leads here is sent as that edge's backward condition; any other second edge is a new duplicate_edge error. - edge_order lists an edge with a backward condition under its target too, as the API does. Loaded nodes keep their loaded edge_order. Fixtures re-copied from the JS repo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0d2bada. Configure here.
Mirrors elevenlabs/elevenlabs-js@07b9ab79: a second go_to from a node to itself was merged into the first as its backward condition. reverse_edge_merges now skips self-loops, and every edge from a node to itself is a self_loop error, since the API rejects them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


Summary
Python port of the "agents as code" workflow builder from elevenlabs/elevenlabs-js#480. Developers can write an agent workflow as chained calls (
wf.converse(...).when("...").tool(...)), send it withagents.create/agents.update, and store it in git as API-format JSON. For every shared fixture, the JSON is byte-identical to what the JS SDK writes.Details
elevenlabs.agent_workflows, a hand-maintained package added to.fernignore. No generated code is touched.from elevenlabs import Workflowneeds anadditional_init_exportsentry in elevenlabs-dx'sgenerators.yml, because the generated__init__.pycan't be edited by hand. That entry is left as a follow-up.Workflow,converse/tool/transfer/end/go_to,when/on_success/on_failure/on_result,node().update,remove,insert_between(returns the inserted node orNone),build(on_warning=...),to_json/to_json_string,from_agent/from_json. Value types areCondition,Destination, andExpr(all_of/any_of/if_else, plusExpr.llm(description, type=..., enum=...), which emitsvalue_schema). Errors come back asWorkflowValidationError.issues. Options are keyword arguments. A plain string where a condition is expected is an LLM condition, and anExpris an expression condition. Validation only happens inbuild()/to_json().trueExpression, whichjsonable_encoderhandles). Soto_json()sorts the dicts, andbuild()turns them intoAgentWorkflowRequestModelwithconstruct_type. No camel/snake mapping is needed. Nested options accept generated models or wire dicts.to_json(), becausePositionInputturns300into300.0and JS can't tell them apart.AgentWorkflowRequestModelNodesValueunion, not from a hard-coded list. They round-trip through JSON unchanged. Python's request serializer does send them, with PydanticUserWarnings, unlike JS, where they're rejected.build()still reportsunknown_node_typeand says so, and the README shows how to sendto_json()throughadditional_body_parameters. Both paths are tested against a mockedagents.update.from_jsonkeeps keys it doesn't recognize instead of stripping them. The Python generatedWorkflowEdgeModelInputhas noreroutesfield (the JS types do), so stripping would lose the editor's bend points.node_id/branch_id, auto-layout skips occupied cells, reachability follows backward conditions in reverse,on_resultputs success first, andbuild()warns on unknown node types.Testing
pytest tests/agent_workflows: 44 passed (8 shared fixtures plus unit tests that mirrorworkflow.test.ts, and Python-specific checks of theagents.updaterequest body). Ran on Python 3.13 / Pydantic 2.13, and on Python 3.8 with both Pydantic 2.10.6 (the lockfile version) and Pydantic 1.x.JSON.stringify(expected, null, 2)for all 8 fixtures: identical.ruff checkandmypyare clean on the new files. Repo-widemypy .reports one existing error, inconversational_ai/conversation.py(websockets.WebSocketClientProtocol, from my local websockets version), which is unrelated to this change.🤖 Generated with Claude Code