Skip to content

Workflow Agents Examples

These examples demonstrate the advanced workflow agent patterns. For API details, see Advanced Orchestration. For the basic patterns (Sequential, Parallel, Loop), see Workflow Agents.

Two reviewers debate code quality, then a lead synthesizes actionable feedback.

use heartbit::{AgentRunner, DebateAgent};
let security_reviewer = AgentRunner::builder(provider.clone())
.name("security_reviewer")
.system_prompt(
"You are a security-focused code reviewer. \
Identify vulnerabilities, injection risks, and auth issues. \
Reference specific lines. When you agree with all points, say CONSENSUS."
)
.build()?;
let perf_reviewer = AgentRunner::builder(provider.clone())
.name("perf_reviewer")
.system_prompt(
"You are a performance-focused code reviewer. \
Identify N+1 queries, unnecessary allocations, and bottlenecks. \
Reference specific lines. When you agree with all points, say CONSENSUS."
)
.build()?;
let lead = AgentRunner::builder(provider.clone())
.name("lead")
.system_prompt(
"You are the tech lead. Synthesize the reviewers' findings into \
a prioritized list of actionable items. Deduplicate overlapping concerns."
)
.build()?;
let review = DebateAgent::builder()
.debater(security_reviewer)
.debater(perf_reviewer)
.judge(lead)
.max_rounds(2)
.should_stop(|text| text.contains("CONSENSUS"))
.build()?;
let output = review.execute(&code_to_review).await?;
println!("Review:\n{}", output.result);

Three classifiers vote on sentiment. Majority wins.

use heartbit::{AgentRunner, VotingAgent};
let build_voter = |name: &str, style: &str| -> Result<AgentRunner<_>, _> {
AgentRunner::builder(provider.clone())
.name(name)
.system_prompt(format!(
"Classify the sentiment as exactly one of: POSITIVE, NEGATIVE, NEUTRAL. \
Approach: {style}. Reply with only the label."
))
.build()
};
let voting = VotingAgent::builder()
.voter(build_voter("intuitive", "Go with your gut feeling")?)
.voter(build_voter("analytical", "Count positive vs negative words")?)
.voter(build_voter("contextual", "Consider tone, sarcasm, and context")?)
.vote_extractor(|output| {
let text = output.result.trim().to_uppercase();
if text.contains("POSITIVE") { "POSITIVE".into() }
else if text.contains("NEGATIVE") { "NEGATIVE".into() }
else { "NEUTRAL".into() }
})
.build()?;
let result = voting.execute("The update broke everything but support was helpful").await?;
println!("Sentiment: {} (votes: {:?})", result.winner, result.tally);

Two proposers draft content from different angles, a synthesizer merges them.

use heartbit::{AgentRunner, MixtureOfAgentsAgent};
let technical = AgentRunner::builder(provider.clone())
.name("technical")
.system_prompt(
"Write a technically detailed blog section about the given topic. \
Include code snippets and architecture details."
)
.build()?;
let storyteller = AgentRunner::builder(provider.clone())
.name("storyteller")
.system_prompt(
"Write an engaging narrative blog section about the given topic. \
Use analogies and real-world examples to make concepts accessible."
)
.build()?;
let editor = AgentRunner::builder(provider.clone())
.name("editor")
.system_prompt(
"You receive multiple draft sections. Combine them into a single \
cohesive blog post that balances technical depth with readability. \
Preserve code snippets from the technical draft."
)
.build()?;
let moa = MixtureOfAgentsAgent::builder()
.proposer(technical)
.proposer(storyteller)
.synthesizer(editor)
.layers(1)
.build()?;
let output = moa.execute("Building multi-agent systems in Rust").await?;
println!("{}", output.result);

A pipeline that fetches data, analyzes it, and conditionally branches to an alert path.

use heartbit::{AgentRunner, DagAgent};
let ingest = AgentRunner::builder(provider.clone())
.name("ingest")
.system_prompt("Parse the raw input into structured JSON.")
.build()?;
let validate = AgentRunner::builder(provider.clone())
.name("validate")
.system_prompt(
"Validate the data. If any field is missing or invalid, \
prefix your response with INVALID: and explain. \
Otherwise prefix with VALID: and return the clean data."
)
.build()?;
let enrich = AgentRunner::builder(provider.clone())
.name("enrich")
.system_prompt("Add derived fields and computed metrics to the data.")
.build()?;
let report = AgentRunner::builder(provider.clone())
.name("report")
.system_prompt("Format the enriched data as a markdown report.")
.build()?;
let error_handler = AgentRunner::builder(provider.clone())
.name("error_handler")
.system_prompt("Explain what went wrong with the data and suggest fixes.")
.build()?;
let dag = DagAgent::builder()
.node("ingest", ingest)
.node("validate", validate)
.node("enrich", enrich)
.node("report", report)
.node("error_handler", error_handler)
.edge("ingest", "validate")
.conditional_edge("validate", "enrich", |output| output.starts_with("VALID:"))
.conditional_edge("validate", "error_handler", |output| output.starts_with("INVALID:"))
.edge("enrich", "report")
.build()?;
let output = dag.execute("name=Alice age=thirty email=alice@example.com").await?;
println!("{}", output.result);

Process many items through the same agent with controlled concurrency.

use heartbit::{AgentRunner, BatchExecutor};
let classifier = AgentRunner::builder(provider.clone())
.name("classifier")
.system_prompt(
"Classify the support ticket into exactly one category: \
BUG, FEATURE_REQUEST, QUESTION, COMPLAINT. \
Reply with only the category."
)
.build()?;
let batch = BatchExecutor::builder(classifier)
.max_concurrency(4)
.build()?;
let tickets = vec![
"My app crashes when I click save".into(),
"Can you add dark mode?".into(),
"How do I reset my password?".into(),
"Your service has been down for 3 hours!".into(),
"Login button does nothing on mobile".into(),
];
let results = batch.execute(tickets).await;
for (i, result) in results.iter().enumerate() {
match result {
Ok(output) => println!("Ticket {}: {}", i, output.result.trim()),
Err(e) => eprintln!("Ticket {} failed: {}", i, e),
}
}
let usage = batch.aggregate_usage(&results);
println!(
"Total tokens: {} input, {} output",
usage.input_tokens, usage.output_tokens
);