Skip to content

feat: Add agent workflows as code (workflow builder) - #922

Open
PaulAsjes wants to merge 5 commits into
mainfrom
feat/workflow-builder
Open

PaulAsjes wants to merge 5 commits into
mainfrom
feat/workflow-builder

Conversation

@PaulAsjes

Copy link
Copy Markdown
Collaborator

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 with agents.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
  • Where it lives: elevenlabs.agent_workflows, a hand-maintained package added to .fernignore. No generated code is touched. from elevenlabs import Workflow needs an additional_init_exports entry in elevenlabs-dx's generators.yml, because the generated __init__.py can't be edited by hand. That entry is left as a follow-up.
  • API: the spec's vocabulary in snake_case: Workflow, converse / tool / transfer / end / go_to, when / on_success / on_failure / on_result, node().update, remove, insert_between (returns the inserted node or None), build(on_warning=...), to_json / to_json_string, from_agent / from_json. Value types are Condition, Destination, and Expr (all_of / any_of / if_else, plus Expr.llm(description, type=..., enum=...), which emits value_schema). Errors come back as WorkflowValidationError.issues. Options are keyword arguments. A plain string where a condition is expected is an LLM condition, and an Expr is an expression condition. Validation only happens in build() / to_json().
  • Internal state is wire-format dicts. Python's generated models already use the wire names (except aliases such as trueExpression, which jsonable_encoder handles). So to_json() sorts the dicts, and build() turns them into AgentWorkflowRequestModel with construct_type. No camel/snake mapping is needed. Nested options accept generated models or wire dicts.
  • Whole floats are written as integers in to_json(), because PositionInput turns 300 into 300.0 and JS can't tell them apart.
  • Unknown node types are detected from the generated AgentWorkflowRequestModelNodesValue union, not from a hard-coded list. They round-trip through JSON unchanged. Python's request serializer does send them, with Pydantic UserWarnings, unlike JS, where they're rejected. build() still reports unknown_node_type and says so, and the README shows how to send to_json() through additional_body_parameters. Both paths are tested against a mocked agents.update.
  • Loaded keys: from_json keeps keys it doesn't recognize instead of stripping them. The Python generated WorkflowEdgeModelInput has no reroutes field (the JS types do), so stripping would lose the editor's bend points.
  • Includes the recent JS review fixes: reroutes kept, agent-to-agent destination change clears node_id / branch_id, auto-layout skips occupied cells, reachability follows backward conditions in reverse, on_result puts success first, and build() warns on unknown node types.

Testing

  • pytest tests/agent_workflows: 44 passed (8 shared fixtures plus unit tests that mirror workflow.test.ts, and Python-specific checks of the agents.update request 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.
  • Fixture output was diffed against node's JSON.stringify(expected, null, 2) for all 8 fixtures: identical.
  • ruff check and mypy are clean on the new files. Repo-wide mypy . reports one existing error, in conversational_ai/conversation.py (websockets.WebSocketClientProtocol, from my local websockets version), which is unrelated to this change.
  • Not tested against the live API.

🤖 Generated with Claude Code

PaulAsjes and others added 2 commits November 4, 2026 17:14
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>
@PaulAsjes
PaulAsjes requested a balanced review from Copilot November 4, 2026 16:23
@PaulAsjes PaulAsjes changed the title Add agent workflows as code (workflow builder) feat: Add agent workflows as code (workflow builder) Nov 4, 2026
@PaulAsjes
PaulAsjes marked this pull request as ready for review November 4, 2026 16:30
@cursor

cursor Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Incorrect workflow graphs could change live agent routing when deployed via agents.update, though validation runs client-side before send; unknown node types may need to_json() bypass to avoid Pydantic serialization quirks.

Overview
Adds a new hand-maintained elevenlabs.agent_workflows package so Eleven Agents workflows can be defined with chained Python (converse, tool, transfer, end, when, go_to, etc.) instead of editing the graph by hand. Workflow.build() validates the graph (aggregating WorkflowValidationError issues), assigns edge ids/order, auto-layouts nodes, and returns AgentWorkflowRequestModel for conversational_ai.agents.create / update.

Also supports load/edit/serialize: Workflow.from_agent, from_json, to_json / to_json_string (canonical API JSON aligned with other SDKs), plus node().update, remove, and insert_between. Helpers cover Condition, Destination, and Expr for expression-based routing.

README documents the workflow-as-code API; .fernignore excludes the new package from codegen. Tests add shared golden fixtures (byte-identical JSON vs the JS reference) and unit coverage for edge merging, validation, and mocked agents.update payloads.

Reviewed by Cursor Bugbot for commit 9dedb1f. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread src/elevenlabs/agent_workflows/_wire.py Resolved
PaulAsjes and others added 2 commits November 4, 2026 17:38
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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants