Workflows
A workflow wires several model calls together: draft then edit, classify then
route, fan out to several agents and combine their answers. In Rig you write
workflows in plain Rust. Every agent call is an async function that
returns a Result, so you compose them with let, match, loops, and
futures::join!. There is no workflow DSL to learn.
Agent or workflow?
Section titled “Agent or workflow?”The main design decision in an LLM application is who controls the flow:
- An agent with tools lets the model decide which tool to call, in what order, and when to stop. Use it for open-ended problems where you can’t list the steps in advance.
- A workflow puts your code in charge: the steps, their order, and the branching are ordinary Rust. Each step may call a model, but no model decides what happens next. Use it when the process is known. It’s cheaper, more predictable, and testable step by step.
Prefer the least agentic design that solves the problem. If a step doesn’t need a model, write a function. If the steps are known, write a workflow. Keep the agent loop for the parts that need the model’s judgment. The two mix freely: a workflow step can be a tool-using agent, and an agent can call a workflow as a tool.
The examples below share one model handle. erase() turns the model into a
cheap, cloneable DynModel that any number of agents can use:
use rig::providers::openai::{self, OpenAI};
let model = OpenAI::from_env()?.completion(openai::GPT_5_5).erase();let writer = AgentBuilder::new(model.clone()).preamble("Draft a one-line tagline.").build();let editor = AgentBuilder::new(model).preamble("Tighten the tagline to under six words.").build();Prompt chaining
Section titled “Prompt chaining”The output of one step becomes the input of the next. Here a writer drafts a
tagline and an editor tightens it (example: agent_prompt_chaining):
let draft = writer.prompt("A tool that compile-checks docs code samples").await?.output();let tagline = editor.prompt(draft.trim()).await?.output();println!("{tagline}");Docs that always compile.Between steps you can add plain code: validate the draft, trim it, or stop early if it fails a check.
Routing
Section titled “Routing”Classify the input with a cheap call, then match to pick the next step
(example: agent_routing). Ask for a typed answer with prompt_typed, so the
model has to return one of your enum’s variants instead of free text you have to
parse:
#[derive(Deserialize, schemars::JsonSchema)]enum Category { Code, Math, Other,}
let router = AgentBuilder::new(client.completion(openai::GPT_5_MINI)) .preamble("Classify the user's request.") .build();
let query = "How do I borrow a value mutably twice?";let category = router.prompt_typed::<Category>(query).await?.output;
let answer = match category { Category::Code => coder.prompt(query).await?, Category::Math => mathematician.prompt(query).await?, Category::Other => generalist.prompt(query).await?,};println!("{}", answer.output());Routing is also how you send easy requests to a small model and hard ones to a large one; see the Model Routing guide.
Parallelization
Section titled “Parallelization”When steps don’t depend on each other, run them at the same time (example:
agent_parallelization). futures::join! waits for all of them and returns
each Result, so one failure doesn’t throw away the others:
let review = "The new update is blazing fast, but the settings menu is confusing.";
let (sentiment, topic) = futures::join!( sentiment_agent.prompt(review).into_future(), topic_agent.prompt(review).into_future(),);
println!("sentiment={}, topic={}", sentiment?.output(), topic?.output());sentiment=NEGATIVE, topic=usabilityagent.prompt(..) returns an AgentRunner; .into_future() (from
std::future::IntoFuture) turns it into the future join! needs. Use
futures::try_join! instead when any failure should cancel the rest. The same
shape works for voting: send one prompt to several agents and take the majority
answer.
Orchestrator-workers
Section titled “Orchestrator-workers”An orchestrator breaks a task into subtasks at run time, workers handle each
one, and a final step combines the results (example: agent_orchestrator).
Unlike parallelization, the subtasks aren’t known in advance:
#[derive(Deserialize, schemars::JsonSchema)]struct Plan { /// One instruction per subtask. subtasks: Vec<String>,}
let orchestrator = AgentBuilder::new(model.clone()) .preamble("Split the task into 2-4 independent writing subtasks.") .build();let worker = AgentBuilder::new(model.clone()) .preamble("Complete the writing subtask you are given.") .build();let editor = AgentBuilder::new(model) .preamble("Merge the drafts into one coherent text.") .build();
let task = "Write a product page for an insulated, plastic-free water bottle.";let plan = orchestrator.prompt_typed::<Plan>(task).await?.output;
// Run every worker concurrently.let drafts = futures::future::try_join_all( plan.subtasks.iter().map(|subtask| worker.prompt(subtask.as_str()).into_future()),).await?;
let combined = drafts.iter().map(|d| d.output()).collect::<Vec<_>>().join("\n\n");let page = editor.prompt(combined).await?.output();println!("{page}");Evaluator-optimizer
Section titled “Evaluator-optimizer”When quality matters more than latency, pair a generator with a critic and loop
until the critic approves (example: agent_evaluator_optimizer). Always bound
the loop:
#[derive(Deserialize, schemars::JsonSchema)]struct Review { approved: bool, /// Concrete fixes, empty when approved. feedback: String,}
let critic = AgentBuilder::new(model) .preamble("Review the text. Approve it only if it is ready to publish.") .build();
let mut draft = writer.prompt("A tool that compile-checks docs code samples").await?.output();
for _ in 0..3 { let review = critic.prompt_typed::<Review>(draft.as_str()).await?.output; if review.approved { break; } let revision = format!("Revise this text:\n{draft}\n\nAddress this feedback:\n{}", review.feedback); draft = writer.prompt(revision).await?.output();}println!("{draft}");The structure of the iteration is fixed in your code; only the content of each step comes from a model.
A loop doesn’t need a critic. An autonomous loop feeds a model’s output back in
until your own condition holds, such as a target value or a passing test
(example: agent_autonomous).
Agents as tools
Section titled “Agents as tools”Turn the flow around and let an agent decide when to call another agent:
Agent::into_tool() wraps an agent as a tool that takes a prompt argument and
returns the sub-agent’s answer. See Agents as tools
for the full example (and the agent_with_agent_tool and multi_agent examples).
Handling failures
Section titled “Handling failures”Every step returns a Result, so error handling is ordinary Rust: ? to abort
the workflow, match to fall back to another step, or retry one step without
redoing the ones before it. Error Handling
covers telling transient provider errors (worth retrying) from permanent ones.
Retry per step, not around the whole workflow, so a retry never re-runs steps
that already had side effects.
