Multi-agent IEx API for coordinating LLM CLI sessions.
A naming and coordination layer over backend-specific session processes.
Each agent is a supervised process addressed by name. All functions
are designed to be called interactively after import AgentWorkshop.Workshop.
Quick start
# 1. Set backend and config (do this first -- affects all new agents)
configure(backend: MyApp.ClaudeBackend,
backend_config: %{working_dir: "~/projects/myapp"},
context: "Elixir project. Run mix test before committing.")
# 2. Create agents with roles
agent(:impl, "You write clean, tested code.", max_turns: 15)
agent(:reviewer, "You review code. Do not modify files.",
model: "opus", allowed_tools: ["Read", "Bash"])
# 3. Talk to them
ask(:impl, "Implement caching for the user lookup")
pipe(:impl, :reviewer, "Review for correctness")
# 4. Check on things
status() # dashboard table
total_cost() # how much you've spentSync vs async
Two ways to send messages:
ask/2-- blocks until the agent responds, prints the resultcast/2-- returns immediately, agent works in the background
Use await/1 or await_all/0 to collect async results. Use status/0
to see who's busy.
Coordination
pipe/3-- wait for one agent, forward its result to anotherfan/2-- send the same message to multiple agents in parallel
ask/2 and pipe/3 return the agent name on success, so they
compose with Elixir's pipe operator:
ask(:impl, "implement caching")
|> pipe(:reviewer, "review for edge cases")
|> pipe(:tests, "write tests for this")Inspecting agents
status/0-- dashboard of all agentsinfo/1-- detailed map for one agent (model, session_id, cost, ...)result/1-- last response text (or pass:fullfor the full result map)history/1-- print the full conversationcost/0-- itemized costs;cost/1-- single agent;total_cost/0-- sum
Lifecycle
load/0-- load.workshop.exs(auto-loaded on startup if present)load/1-- load a specific setup filereset/1-- clear an agent's conversation (keeps role and config)dismiss/1-- remove an agent entirelyreset_all/0-- stop everything, clear config
Architecture
The OTP Application owns the full lifecycle:
ETS tables, registries, supervisors, and the EventLog. Workshop is the
public IEx facade -- it reads and writes shared state but does not start
or supervise any infrastructure. Call configure/1 to set your backend;
everything else is already running.
Application supervisor (started automatically)
|-- TableManager (owns ETS tables)
|-- :persistent_term (global config -- no process needed)
|-- Registry (PubSub, Scheduler, BoardWorker)
|-- EventLog
|-- DynamicSupervisor (agent sessions)
`-- Task.Supervisor (async cast tasks)
Workshop (this module -- IEx helpers, no supervision duties)
Summary
Functions
Start a named agent.
List all agent names, sorted alphabetically.
Send a message and wait for the response. Prints the result when done.
Wait for an async agent to finish. Prints the result when done.
Wait for all busy agents to finish. Prints each result as it arrives.
Show the work board. Optionally filter by status or type.
Create a board worker — an agent that polls the board for work.
Show budget info. With no argument, shows global. With an agent name, shows per-agent.
Cancel a recurring schedule for an agent.
Cancel a work item, removing it from active consideration.
Send a message asynchronously. Returns :ok immediately.
Claim a work item for an agent.
Clear all recorded events from the event log.
Mark a work item as done. Unblocks any dependents.
Set global defaults inherited by all agents. Call this first.
Show itemized costs for all agents, then print the total.
Show cost for a single agent. Returns the cost as a float.
Start the LiveView dashboard.
Remove an agent entirely. Stops its process and discards all state.
Show recent events.
Run an agent prompt on a recurring interval.
Mark a work item as failed with an optional error message.
Send the same message to multiple agents in parallel (all via cast/2).
Create an agent from a profile.
Get a value from the shared store.
Get a value from the shared store, returning default if the key is missing.
Get uncommitted changes as a diff string.
Get a structured summary of the current git state.
Print the conversation history for an agent.
Get detailed info about an agent as a map.
Load a Workshop setup file (.exs).
Set the log level for Workshop messages.
Start the Workshop MCP server.
Wait for from to finish, then send its last result to to.
Define a reusable agent profile (template).
List available profiles.
Put a value in the shared store.
Reset an agent's conversation. Keeps the role, model, and all config.
Stop all agents and clear global config. The nuclear option.
Reset all budget tracking.
Reset a workflow -- removes its work items and resets to :defined.
Get the last response from an agent.
Run a workflow by expanding its stages into work board items.
List active schedules.
Mark a work item as in progress.
Print a status dashboard showing all agents.
Stop the Workshop and clean up all state. Used for teardown in tests.
Print all entries in the shared store to the console.
Delete a key from the shared store.
List all keys currently in the shared store.
Get the total cost across all agents as a single number.
Stop printing live events.
Start printing live events to the console.
Add a work item to the board.
Create a work item from an agent's last result.
Get details of a specific work item.
Print a summary of all active board workers.
Define a declarative workflow -- a sequence of stages that expand into work board items with dependencies.
Show workflow progress -- each stage with its current status.
List all defined workflows.
Functions
Start a named agent.
The role string is agent-specific context that composes with the global
context from configure/1. If both are set, the effective system prompt
is context <> "\n\n" <> role. Agent-specific options override globals.
Calling agent/3 with an existing name replaces that agent.
Examples
# Full agent with role and options
agent(:impl, "You write clean, well-tested code.", max_turns: 15)
# Read-only reviewer using opus
agent(:reviewer, "You review code. Do not modify files.",
model: "opus", allowed_tools: ["Read", "Bash"])
# Minimal -- inherits everything from configure()
agent(:scratch)
# Options without a role
agent(:fast, model: "haiku", max_turns: 3)Permissions
Workshop defaults to permission_mode: :auto so agents can work
non-interactively. Override per-agent or globally via configure/1:
# Global: all agents use bypass
configure(permission_mode: :bypass_permissions)
# Per-agent: restrict the reviewer to default
agent(:reviewer, "Review only.", permission_mode: :default)Valid modes: :default, :accept_edits, :bypass_permissions,
:dont_ask, :plan, :auto.
Options
Core: :model, :max_turns, :permission_mode, :max_budget_usd, :effort
Permissions: :allowed_tools, :disallowed_tools, :dangerously_skip_permissions
Backend pass-through (Claude CLI): :worktree, :mcp_config, :add_dir,
:settings, :append_system_prompt, :no_session_persistence
Workshop-specific: :workshop_tools, :skill, :max_cost_usd, :timeout
@spec agents() :: [atom()]
List all agent names, sorted alphabetically.
Example
agents() # => [:impl, :reviewer, :tests]
Send a message and wait for the response. Prints the result when done.
This is the primary way to talk to an agent. Each call continues the agent's conversation (the session ID is threaded automatically).
If the agent has a pending async task from cast/2, waits for it first.
Returns the agent name on success, enabling |> chaining with pipe/3:
ask(:impl, "implement caching")
|> pipe(:reviewer, "review for edge cases")Examples
ask(:impl, "What modules are in this project?")
ask(:impl, "Now add tests for the retry module") # continues conversation
ask(:impl, "What files did you change?") # still the same session
Wait for an async agent to finish. Prints the result when done.
If the agent is already idle, prints a note and returns :ok.
Pass a timeout in milliseconds to avoid blocking forever.
Examples
cast(:impl, "Work on this")
# ... later ...
await(:impl) # block until done
await(:impl, 30_000) # give up after 30 seconds
@spec await_all(timeout()) :: :ok
Wait for all busy agents to finish. Prints each result as it arrives.
Example
cast(:impl, "Implement feature")
cast(:tests, "Write tests")
await_all() # blocks until both are done
@spec board(keyword()) :: [AgentWorkshop.Work.t()]
Show the work board. Optionally filter by status or type.
Examples
board() # full board
board(status: :ready) # items ready to pick up
board(type: :code) # only code tasks
Create a board worker — an agent that polls the board for work.
Combines a profile, an agent, and a poll loop. The worker claims ready items matching its work type, executes them, and marks them complete.
Options
:profile- (required) profile name to create the agent from:interval- poll interval in ms (default: 60_000 / 1 min):worktree- whentrue, passesworktree: trueto the agent's query opts, enabling the Claude CLI--worktreeflag for isolated parallel execution
Examples
board_worker(:coder_1, :code, profile: :coder, interval: :timer.minutes(1))
board_worker(:reviewer_1, :review, profile: :reviewer, interval: :timer.minutes(2))
# Post work — workers pick it up automatically
work(:feature, "Implement feature X", type: :code, spec: "...")
Show budget info. With no argument, shows global. With an agent name, shows per-agent.
Examples
budget() # global budget info
budget(:impl) # per-agent budget
@spec cancel(atom()) :: :ok
Cancel a recurring schedule for an agent.
Stops the agent from executing its scheduled prompt. The agent itself remains available for manual interaction. No-op if the agent has no active schedule.
Example
iex> every(:monitor, "Check CI status", interval: :timer.minutes(5))
iex> cancel(:monitor)
:ok
Cancel a work item, removing it from active consideration.
Transitions the item to :cancelled status. Unlike fail_work/2, this
indicates the work was intentionally abandoned rather than attempted
and failed.
Example
iex> work(:cache, "Implement LRU cache", type: :code)
iex> cancel_work(:cache)
:ok
Send a message asynchronously. Returns :ok immediately.
The agent works in the background while you do other things.
Use status/0 to check progress, await/1 to collect the result.
If the agent is already working, the message is queued (up to 5 deep). Queued messages are processed in order after the current task finishes.
Examples
cast(:impl, "Implement the caching layer from issue #12")
cast(:tests, "Add property-based tests for lib/myapp/encoder.ex")
# Queue a follow-up while :impl is still working
cast(:impl, "Also add the cache invalidation")
# Go think about something else...
status() # see who's done (shows queue depth)
await(:impl) # block until current + queued work finishes
await_all() # wait for everyone
Claim a work item for an agent.
Example
claim(:cache, :impl)
@spec clear_events() :: :ok
Clear all recorded events from the event log.
After calling this, events/0 will show no entries until new events occur.
Example
iex> clear_events()
:ok
Mark a work item as done. Unblocks any dependents.
Examples
complete_work(:cache)
complete_work(:cache, "Implemented with GenServer-backed LRU")
@spec configure(keyword()) :: :ok
Set global defaults inherited by all agents. Call this first.
Changes affect new agents only; existing agents keep their config.
Examples
# Minimal
configure(backend: MyApp.ClaudeBackend, backend_config: %{working_dir: "."})
# Full setup
configure(
backend: MyApp.ClaudeBackend,
backend_config: %{working_dir: "~/projects/myapp"},
model: "sonnet",
max_turns: 10,
context: """
Elixir project using Phoenix 1.7.
Run mix test before considering any task complete.
Use conventional commits.
"""
)Permissions
Workshop defaults to permission_mode: :auto so agents can work
non-interactively (the CLI default of :default would hang waiting
for approval that never comes). Override here to change for all agents:
configure(permission_mode: :bypass_permissions)Options
Backend options:
:backend-- module implementingAgentWorkshop.Backend(required on first call):backend_config-- backend-specific configuration (passed tostart_session/2)
Query options: :model, :max_turns, :permission_mode, :max_budget_usd, :effort
Special:
:context-- global system prompt prepended to every agent's role. The effective system prompt for each agent iscontext <> "\n\n" <> role.:persistence--true(use.agent_workshop/in cwd), a path string, orfalseto disable. Saves work board and store to disk on change, reloads on next start.
@spec cost() :: float()
Show itemized costs for all agents, then print the total.
Example
cost()
# :impl: $0.12 (3 turns)
# :reviewer: $0.24 (1 turn)
# Total: $0.36
Show cost for a single agent. Returns the cost as a float.
Example
cost(:impl) # => :impl: $0.12 across 3 turns
Start the LiveView dashboard.
Options
:port- HTTP port (default 4223)
Examples
dashboard() # start on port 4223
dashboard(port: 8080) # custom port
@spec dismiss(atom()) :: :ok
Remove an agent entirely. Stops its process and discards all state.
No-op if the agent doesn't exist. Use reset/1 instead if you want
to keep the agent but clear its conversation.
Show recent events.
Options
:last- number of events to show (default 20)
Examples
events() # last 20
events(last: 50) # last 50
Run an agent prompt on a recurring interval.
Examples
every(:monitor, "Check CI status", interval: :timer.minutes(5))
every(:sweeper, "Clean up stale branches", interval: :timer.hours(1))
Mark a work item as failed with an optional error message.
Transitions the item to :failed status. Unlike cancel_work/1, this
indicates the work was attempted but did not succeed.
Examples
iex> fail_work(:cache, "Tests failed: 3 assertions")
:ok
iex> fail_work(:cache)
:ok
Send the same message to multiple agents in parallel (all via cast/2).
Good for getting multiple perspectives on the same question.
Example
fan("What issues do you see in lib/myapp/retry.ex?", [:impl, :reviewer])
# both agents work concurrently
await_all()
result(:impl) # impl's take
result(:reviewer) # reviewer's take
Create an agent from a profile.
Examples
profile(:coder, "You write code.", max_turns: 15)
from_profile(:coder, :coder_bug_42)
from_profile(:coder, :coder_feature_7, max_turns: 25) # override opts
Get a value from the shared store.
Returns nil if the key does not exist. See get/2 to supply a default.
Examples
iex> put(:spec, "LRU cache with TTL")
iex> get(:spec)
"LRU cache with TTL"
iex> get(:nonexistent)
nil
Get a value from the shared store, returning default if the key is missing.
Examples
iex> get(:missing, "fallback")
"fallback"
iex> put(:count, 5)
iex> get(:count, 0)
5
Get uncommitted changes as a diff string.
Get a structured summary of the current git state.
Returns branch, status, recent commits, and file changes.
Requires the optional git dependency.
Example
git_summary()
# => %{branch: "main", dirty: true, staged: 1, ...}
Print the conversation history for an agent.
Shows each turn with its cost and result text. Use :last to
limit output for long conversations.
Examples
history(:impl) # full conversation
history(:impl, last: 3) # only the last 3 turns
Get detailed info about an agent as a map.
Returns model, session_id, role, status, cost, turns, and config.
Example
info(:impl)
# => %{name: :impl, status: :idle, model: "sonnet", session_id: "abc-123",
# cost: 0.04, turns: 1, role: "You write clean code."}
@spec load(String.t()) :: :ok | {:error, :not_found}
Load a Workshop setup file (.exs).
The file is evaluated with import AgentWorkshop.Workshop in scope,
so it can call configure/1, agent/3, etc. directly.
With no argument, looks for .workshop.exs in the current directory.
Example file (.workshop.exs)
configure(backend: MyApp.ClaudeBackend,
backend_config: %{working_dir: "."},
model: "sonnet",
context: "Elixir project. Run mix test before committing.")
agent(:impl, "You write clean code.", max_turns: 15)
agent(:reviewer, "Review only.", model: "opus",
allowed_tools: ["Read", "Bash"])Examples
load() # loads .workshop.exs
load("setups/review.exs") # loads a specific file
@spec log_level(Logger.level()) :: :ok
Set the log level for Workshop messages.
Useful for quieting debug noise during interactive sessions.
Examples
log_level(:warning) # quiet -- only warnings and errors
log_level(:info) # default
log_level(:debug) # verbose -- see send/receive for every message
Start the Workshop MCP server.
Exposes all Workshop functions as MCP tools over HTTP.
Requires anubis_mcp, bandit, and plug deps.
Can also be triggered via configure(mcp: [port: 4222]).
Examples
mcp_server() # start on default port 4222
mcp_server(port: 8080) # custom port
Wait for from to finish, then send its last result to to.
The message argument frames what to should do with the result.
The forwarded text is appended after your message. Without a message,
a default framing is used.
This is synchronous -- it blocks until both agents are done.
Returns the target agent name on success, so pipes chain:
ask(:impl, "implement caching")
|> pipe(:reviewer, "review for edge cases")
|> pipe(:tests, "write tests for this")Examples
# Implement, then review
ask(:impl, "Implement caching for user lookup")
pipe(:impl, :reviewer, "Review for cache invalidation edge cases")
# Or after an async cast
cast(:impl, "Implement the retry logic")
# ... do other stuff ...
pipe(:impl, :tests) # awaits :impl, sends result to :tests
Define a reusable agent profile (template).
Examples
profile(:coder, "You write clean code.", max_turns: 15)
profile(:reviewer, "Review only.", model: "opus", allowed_tools: ["Read", "Bash"])
List available profiles.
Put a value in the shared store.
Keys can be any term. Use tuples for namespacing.
Examples
put(:spec, "LRU cache with TTL support")
put({:impl, :notes}, "Chose GenServer over Agent")
@spec reset(atom()) :: :ok
Reset an agent's conversation. Keeps the role, model, and all config.
The agent gets a fresh session -- like calling agent/3 again with
the same arguments. Cost tracking is also reset to zero.
Example
ask(:impl, "Write some code")
# ... not happy with the direction ...
reset(:impl)
ask(:impl, "Try a different approach") # fresh conversation
@spec reset_all() :: :ok
Stop all agents and clear global config. The nuclear option.
After this, you'll need to call configure/1 and agent/3 again.
@spec reset_budget() :: :ok
Reset all budget tracking.
Clears both global and per-agent budget limits and spent totals.
After calling this, agents can spend without budget restrictions
until new limits are set via configure(max_cost_usd: ...) or
per-agent agent(:name, "role", max_cost_usd: ...).
Example
iex> reset_budget()
:ok
Reset a workflow -- removes its work items and resets to :defined.
Use this to re-run a workflow or clear a failed run.
Example
reset_workflow(:feature)
run_workflow(:feature)
Get the last response from an agent.
Returns the response text by default. Pass :full to get the
complete result map (includes cost, session_id, duration, etc.).
Examples
result(:impl) # => "Here is the implementation..."
result(:impl, :full) # => %{result: "...", cost_usd: 0.04, ...}
Run a workflow by expanding its stages into work board items.
Board workers will automatically pick up and execute the stages in dependency order.
Example
run_workflow(:feature)
@spec schedules() :: [map()]
List active schedules.
Mark a work item as in progress.
Transitions a work item from :ready (or :claimed) to :in_progress.
Typically called after an agent has claimed the item with claim_work/2.
Example
iex> work(:cache, "Implement LRU cache", type: :code)
iex> claim_work(:cache, :impl)
iex> start_work(:cache)
:ok
@spec status() :: [map()]
Print a status dashboard showing all agents.
Example output
agent | status | task | cost | turns
-----------+---------+--------------------------------------+-------+------
:impl | working | Implement the caching layer from ... | $0.08 | 3
:reviewer | idle | | $0.00 | 0
:tests | idle | | $0.04 | 2
@spec stop() :: :ok
Stop the Workshop and clean up all state. Used for teardown in tests.
Dismisses all agents, clears all ETS tables. The Application supervisor
remains running -- call configure/1 to set up again.
Print all entries in the shared store to the console.
Each entry is displayed as key: value. Prints "Store is empty." when
the store has no entries.
Example
iex> put(:spec, "LRU cache")
iex> put(:status, :draft)
iex> store()
:spec: "LRU cache"
:status: :draft
:ok
Delete a key from the shared store.
No-op if the key does not exist.
Examples
iex> put(:scratch, "temp value")
iex> store_delete(:scratch)
:ok
iex> get(:scratch)
nil
List all keys currently in the shared store.
Returns an empty list when the store has no entries.
Examples
iex> put(:spec, "cache design")
iex> put(:notes, "use GenServer")
iex> store_keys()
[:notes, :spec]
iex> store_keys()
[]
@spec total_cost() :: float()
Get the total cost across all agents as a single number.
@spec unwatch() :: :ok
Stop printing live events.
@spec watch() :: :ok
Start printing live events to the console.
Shows agent creation/dismissal, ask/cast completions with cost, work board changes, errors, and more.
Example
watch()
cast(:impl, "do something")
# [agent] impl created
# [cast] impl complete ($0.04) — Here is the implementation...
Add a work item to the board.
Options
:type- work type (:code,:review,:test,:docs,:deploy,:triage,:custom):spec- detailed specification:priority- 1 (highest) to 5 (lowest), default 3:depends_on- list of work item IDs that must complete first
Examples
work(:cache, "Implement LRU cache",
type: :code, priority: 1,
spec: "LRU cache with configurable max size and TTL per entry")
work(:cache_review, "Review cache implementation",
type: :review, depends_on: [:cache])
Create a work item from an agent's last result.
Uses result(agent_name) as the spec for the new work item.
Useful for turning an agent's analysis, plan, or review into
actionable board items.
Examples
ask(:arch, "Review the architecture of this project")
work_from_result(:arch, :refactor, type: :code, title: "Address architecture findings")
ask(:planner, "Break this feature into tasks")
work_from_result(:planner, :feature_impl, type: :code)
@spec work_item(atom()) :: AgentWorkshop.Work.t() | nil
Get details of a specific work item.
Returns the full work item struct including status, type, priority,
dependencies, claimed agent, and result. Returns nil if the item
does not exist.
Examples
iex> work(:cache, "Implement LRU cache", type: :code, priority: 1)
iex> item = work_item(:cache)
iex> item.title
"Implement LRU cache"
iex> item.status
:ready
iex> work_item(:nonexistent)
nil
@spec workers() :: [map()]
Print a summary of all active board workers.
Board workers are agents that automatically poll the work board for items matching their work type, claim them, execute them, and mark them complete. Shows each worker's type, completed count, and current item (if any). Prints "No board workers." when none are running.
Example
iex> workers()
:coder_1: code, 3 completed
:reviewer_1: review, 1 completed (working on :cache_review)
:ok
Define a declarative workflow -- a sequence of stages that expand into work board items with dependencies.
Each stage is a tuple: {stage_name, agent, title} or
{stage_name, agent, title, opts}.
Stage options
:from- data source: a file path (string), a stage name (atom), or a list of stage names (fan-in):type- work board type (default::custom):priority- priority 1-5 (default: 3)
Example
workflow(:feature, [
{:plan, :planner, "Break this into tasks", from: "specs/feature.md"},
{:implement, :coder, "Implement the plan", from: :plan},
{:test, :tester, "Write tests", from: :implement},
{:review, :reviewer, "Review everything", from: [:implement, :test]}
])
@spec workflow_status(atom()) :: {AgentWorkshop.Workflow.t(), [AgentWorkshop.Work.t() | nil]} | {:error, term()}
Show workflow progress -- each stage with its current status.
Example
workflow_status(:feature)
@spec workflows() :: [AgentWorkshop.Workflow.t()]
List all defined workflows.
Example
workflows()