Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 149 additions & 24 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ clap = { version = "4", features = ["derive", "string", "env"] }
clap_complete = "4"
clap_mangen = "0.2"
hmac = "0.12"
# Draws `agents workflow diagram` in the terminal: Mermaid in, box-drawing text
# out, in pure Rust. The default `cli` feature is mmdflux's own binary.
mmdflux = { version = "2.6.1", default-features = false }
# The terminal's width, so diagrams can shorten long IDs to fit it.
terminal_size = "0.4"
dotenvy = "0.15"
futures-util = "0.3"
httpdate = "1"
Expand Down
83 changes: 82 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ elevenlabs agents widget embed <agent_id>

# Run the tests attached to an agent (polls to completion; exits non-zero on failure)
elevenlabs agents test <agent_id>

# Draw an agent's workflow in the terminal, or print it as Mermaid (local files only, no API key)
elevenlabs agents workflow diagram <config-path|agent_id|name> [--format ascii|mermaid|mermaid-md]
```

### Two surfaces in one namespace
Expand All @@ -217,7 +220,7 @@ elevenlabs agents test <agent_id>

| | Workflow commands | API commands |
|---|---|---|
| **What** | `init`, `add`, `status`, `push`, `pull`, `test`, `templates`, `widget embed` | `create`, `get`, `list`, `update`, `delete`, `duplicate`, `run_tests`, and subgroups like `branches`, `tools`, `tests`, `conversations` |
| **What** | `init`, `add`, `status`, `push`, `pull`, `test`, `templates`, `widget embed`, `workflow diagram` | `create`, `get`, `list`, `update`, `delete`, `duplicate`, `run_tests`, and subgroups like `branches`, `tools`, `tests`, `conversations` |
| **Operates on** | Your local project files, syncing them with ElevenLabs | The API directly — one command, one request |
| **Arguments** | Positional, e.g. `agents test <agent_id>` | Flags, e.g. `agents get --agent-id <id>` |
| **Output** | Progress text | The API response (`--format json\|table\|yaml\|csv`) |
Expand All @@ -229,6 +232,84 @@ The API commands own the primitive names, so the workflow only adds verbs the AP

> **Migrating from v0 (`@elevenlabs/cli`):** v0's `agents list` showed local config and `agents delete <id>` took a positional and cleaned up locally. In v1 both names belong to the API surface — use `agents status` for the local view, and `--agent-id` for delete. v0's `agents widget <id>` is now `agents widget embed <id>`, since `agents widget` is an API subgroup.

### Visualizing workflows

Workflows are stored as `workflow.nodes` / `workflow.edges` in the agent config, which is lossless but hard to read, and harder to review as a diff. `agents workflow diagram` turns one into a flowchart:

```bash
# Draw it in the terminal: a config file, or an id or name from agents.json
elevenlabs agents workflow diagram Support
elevenlabs agents workflow diagram agent_configs/support.json
elevenlabs agents workflow diagram --all

# Print it as Mermaid source, to save, or to paste into docs or a PR comment
elevenlabs agents workflow diagram Support --format mermaid > docs/support-flow.mmd
elevenlabs agents workflow diagram Support --format mermaid-md # wrapped in a ```mermaid fence
elevenlabs agents workflow diagram --all --format mermaid-md # every agent and branch, one section each
```

It reads local files only: no API key, no network, so it also runs in CI on forks.

The default format, `ascii`, draws boxes and arrows (in colour when stdout is a terminal and `NO_COLOR` is unset). To keep a workflow narrow enough to read, each edge condition is drawn as a numbered key on its edge and written out in full in a legend underneath:

```
╭───────╮
│ Start │
╰───────╯
│
│
│
│
╭───╮
│ 1 │
╰───╯
│
│
▼
┌───────────────────┐
║ Tool ║
║ tool_lookup_order ║
└───────────────────┘
│ │
└───┐ ┌──┘
│ │
│ │
╭───╮ ╭───╮
│ 2 │ │ 3 │
╰───╯ ╰───╯
│ │
│ │
▼ │
╭───────────╮ ▼
│ Say │ ╭───────────────╮
│ Apologize │ │ End (success) │
╰───────────╯ ╰───────────────╯

1 The caller has given an order number
2 failure
3 success
```

A few things keep a busy workflow readable, so lines don't cross or run together:

- A step back (a backward condition) ends in a stub, such as `6 ↩`. The legend says where it goes: `6 Changes topic (back to 'Ask for the order number')`.
- An ending that several paths lead to (an end, or an agent or phone transfer) is drawn once for each path.
- When a node's branches would still cross, its box is widened or the branches are reordered, whichever draws without crossings.
- When the drawing is wider than the terminal (or `COLUMNS`), long IDs are shortened, as in `tool_01j…sfb67`. If it still doesn't fit, a note says so; `--format mermaid` has the full IDs.

`--format mermaid` prints [Mermaid](https://mermaid.js.org/), which GitHub renders in Markdown files, issues and PR comments. Each node type has its own shape and colour (start/end, say, tool, update state, subagent, and agent or phone transfers); types the CLI doesn't know yet are drawn as plain dashed boxes. Edge labels summarise the condition: its label if it has one, otherwise the LLM condition text (truncated), `success`/`failure` for tool results, or `expression`. A backward condition is drawn as a dotted arrow in the other direction. Nodes and edges are emitted sorted by ID, so the output changes only when the workflow does and diffs cleanly.

```mermaid
flowchart TD
n_apologize("Say · Apologize")
n_done(["End (success)"])
n_lookup[["Tool · tool_lookup_order"]]
n_start_node(["Start"])
n_lookup -->|"failure"| n_apologize
n_lookup -->|"success"| n_done
n_start_node -->|"The caller has given an order number"| n_lookup
```

### Tools

Manage the webhook and client tools your agents reference. Tools are tracked in `tools.json` with configs under `tool_configs/`.
Expand Down
52 changes: 30 additions & 22 deletions cli/elevenlabs/workflow/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub fn register(app: CliApp) -> CliApp {

/// Load `agents.json`, erroring with v0's "run init first" hint when it
/// is missing.
fn require_agents() -> Result<project::AgentsConfig, CliError> {
pub(super) fn require_agents() -> Result<project::AgentsConfig, CliError> {
if !Path::new(project::AGENTS_FILE).exists() {
return Err(CliError::Validation(
"agents.json not found. Run 'elevenlabs agents init' first.".to_string(),
Expand All @@ -78,7 +78,7 @@ fn require_agents() -> Result<project::AgentsConfig, CliError> {

/// Read an agent's display name from its config file. Ports v0's
/// `getAgentName` (Unknown when unreadable, Unnamed when nameless).
fn agent_display_name(config_path: &str) -> String {
pub(super) fn agent_display_name(config_path: &str) -> String {
match project::read_value_in_project(config_path) {
Ok(value) => value
.get("name")
Expand All @@ -89,6 +89,28 @@ fn agent_display_name(config_path: &str) -> String {
}
}

/// Find an agent in `agents.json` by ID, falling back to the display name in
/// its config file.
///
/// ID first so existing scripts keep resolving identically. `init`'s
/// next-steps tell users to pass the name they just created, and an agent ID
/// is not something anyone has to hand at that point.
pub(super) fn find_agent<'a>(
config: &'a project::AgentsConfig,
key: &str,
) -> Option<&'a project::AgentDefinition> {
config
.agents
.iter()
.find(|a| a.id.as_deref() == Some(key))
.or_else(|| {
config
.agents
.iter()
.find(|a| agent_display_name(&a.config) == key)
})
}

// ── init ────────────────────────────────────────────────────────────

#[derive(clap::Args)]
Expand Down Expand Up @@ -453,26 +475,12 @@ struct TestArgs {
fn handle_test(args: TestArgs, ctx: &AppContext) -> Result<(), CliError> {
let _scope = api::command_scope("agents.test");
let config = require_agents()?;
// ID first so existing scripts keep resolving identically, then fall back
// to the display name in the config file. `init`'s next-steps tell users
// to pass the name they just created, and an agent ID is not something
// anyone has to hand at that point.
let agent = config
.agents
.iter()
.find(|a| a.id.as_deref() == Some(args.agent.as_str()))
.or_else(|| {
config
.agents
.iter()
.find(|a| agent_display_name(&a.config) == args.agent)
})
.ok_or_else(|| {
CliError::Validation(format!(
"No agent with ID or name '{}' in agents.json",
args.agent
))
})?;
let agent = find_agent(&config, &args.agent).ok_or_else(|| {
CliError::Validation(format!(
"No agent with ID or name '{}' in agents.json",
args.agent
))
})?;

let config_path = Path::new(&agent.config);
if !config_path.exists() {
Expand Down
Loading
Loading