TaskGroup: multi-step flows
TaskGroup: multi-step flows
Collecting a single order item is useful, but a real restaurant order involves multiple items, possibly a drink order, and payment details. Running these tasks one at a time with manual orchestration gets messy fast. TaskGroup gives you a clean way to queue, run, and manage multiple tasks as a single flow — with support for going back to a previous step and summarizing context between tasks.
What you'll learn
- How to orchestrate multiple tasks with TaskGroup
- How to add tasks dynamically with
add() - How to handle regression (going back to a previous step)
- How context summarization prevents token bloat between tasks
What is a TaskGroup?
A TaskGroup is a container that runs a sequence of tasks one after another. You add tasks to the group, then run it. The group executes each task in order, collecting results as it goes. When all tasks complete, you get back a list of results.
The real power of TaskGroup is what happens between tasks: context summarization compresses the conversation history so each new task starts with a clean, focused context. And regression support lets the customer go back and change a previous answer without starting over.
Building an order flow
Let's build a complete order flow with a TaskGroup that collects multiple menu items and then collects payment information.
from pydantic import BaseModel
class OrderItem(BaseModel):
name: str
quantity: int
modifications: list[str] = []
unit_price: float = 0.0
class PaymentInfo(BaseModel):
method: str # "card", "cash", "mobile"
tip_percentage: float = 0.0import { z } from "zod";
const OrderItemSchema = z.object({
name: z.string(),
quantity: z.number().int().min(1),
modifications: z.array(z.string()).default([]),
unitPrice: z.number().default(0),
});
const PaymentInfoSchema = z.object({
method: z.enum(["card", "cash", "mobile"]),
tipPercentage: z.number().default(0),
});
type OrderItem = z.infer<typeof OrderItemSchema>;
type PaymentInfo = z.infer<typeof PaymentInfoSchema>;Now create the payment task alongside the order item task from the previous chapter:
from livekit.agents import AgentTask, function_tool, RunContext
from models import PaymentInfo
class CollectPayment(AgentTask[PaymentInfo]):
def __init__(self, total: float):
super().__init__(
instructions=f"""Collect payment information from the customer.
Their order total is ${total:.2f}.
Ask how they would like to pay: card, cash, or mobile payment.
Then ask if they would like to add a tip (suggest 15%, 18%, or 20%).
Confirm the payment details before submitting.""",
output_type=PaymentInfo,
)
@function_tool
async def confirm_payment(
self,
context: RunContext,
method: str,
tip_percentage: float = 0.0,
) -> None:
"""Confirm the payment method and tip."""
self.complete(PaymentInfo(method=method, tip_percentage=tip_percentage))import { AgentTask, functionTool, RunContext } from "@livekit/agents";
import { z } from "zod";
const PaymentInfoSchema = z.object({
method: z.enum(["card", "cash", "mobile"]),
tipPercentage: z.number().default(0),
});
type PaymentInfo = z.infer<typeof PaymentInfoSchema>;
class CollectPayment extends AgentTask<PaymentInfo> {
constructor(total: number) {
super({
instructions: `Collect payment information from the customer.
Their order total is $${total.toFixed(2)}.
Ask how they would like to pay: card, cash, or mobile payment.
Then ask if they would like to add a tip (suggest 15%, 18%, or 20%).
Confirm the payment details before submitting.`,
outputType: PaymentInfoSchema,
});
}
@functionTool({ description: "Confirm the payment method and tip." })
async confirmPayment(
context: RunContext,
method: string,
tipPercentage: number = 0
): Promise<void> {
this.complete({ method: method as any, tipPercentage });
}
}Running tasks with TaskGroup
Now wire the tasks together in a TaskGroup inside the Order Taker agent:
from livekit.agents import Agent, TaskGroup, function_tool, RunContext
from collect_order_item import CollectOrderItem
from collect_payment import CollectPayment
from models import OrderItem
class OrderTakerAgent(Agent):
def __init__(self):
super().__init__(
instructions="""You are the order taker at Bella Vista.
Help customers build their complete order.
Use the take_order tool to start the ordering process.""",
)
async def on_enter(self):
self.session.generate_reply(
instructions="Welcome the guest and let them know you are ready to take their order."
)
@function_tool
async def take_order(self, context: RunContext) -> str:
"""Start the full ordering process: collect items then payment."""
group = TaskGroup()
# Add the first item task
group.add(CollectOrderItem())
# Run the group — this processes tasks in sequence
results = await group.run(self.session)
# Separate item results from payment
items = [r for r in results if isinstance(r, OrderItem)]
total = sum(item.unit_price * item.quantity for item in items)
item_summary = ", ".join(
f"{item.quantity}x {item.name}" for item in items
)
return f"Order complete: {item_summary}. Total: ${total:.2f}"import { Agent, TaskGroup, functionTool, RunContext } from "@livekit/agents";
import { CollectOrderItem } from "./collectOrderItem";
import { CollectPayment } from "./collectPayment";
class OrderTakerAgent extends Agent {
constructor() {
super({
instructions: `You are the order taker at Bella Vista.
Help customers build their complete order.
Use the take_order tool to start the ordering process.`,
});
}
async onEnter() {
this.session.generateReply({
instructions: "Welcome the guest and let them know you are ready to take their order.",
});
}
@functionTool({ description: "Start the full ordering process: collect items then payment." })
async takeOrder(context: RunContext): Promise<string> {
const group = new TaskGroup();
group.add(new CollectOrderItem());
const results = await group.run(this.session);
const items = results.filter((r): r is any => r?.name !== undefined);
const total = items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
const itemSummary = items.map((item) => `${item.quantity}x ${item.name}`).join(", ");
return `Order complete: ${itemSummary}. Total: $${total.toFixed(2)}`;
}
}TaskGroup runs tasks in sequence. Each task gets its own conversational context, collects its data, and calls complete(). The results accumulate in the list returned by group.run(). The parent agent receives all results at once and can process the complete order.
Adding tasks dynamically with add()
The customer might want one item or five. You do not know in advance. TaskGroup supports adding tasks dynamically — even while the group is running.
from livekit.agents import Agent, TaskGroup, function_tool, RunContext
from collect_order_item import CollectOrderItem
from collect_payment import CollectPayment
from models import OrderItem
class OrderTakerAgent(Agent):
def __init__(self):
super().__init__(
instructions="""You are the order taker at Bella Vista.
Use take_order to start collecting items.
After each item, ask if they want to add more.""",
)
self.task_group = None
self.order_items = []
@function_tool
async def take_order(self, context: RunContext) -> str:
"""Start collecting order items."""
self.task_group = TaskGroup()
self.task_group.add(CollectOrderItem())
results = await self.task_group.run(self.session)
self.order_items = [r for r in results if isinstance(r, OrderItem) and r is not None]
total = sum(item.unit_price * item.quantity for item in self.order_items)
return f"Collected {len(self.order_items)} items. Total: ${total:.2f}"
@function_tool
async def add_another_item(self, context: RunContext) -> str:
"""Add another item to the order."""
if self.task_group is None:
return "No active order. Use take_order first."
self.task_group.add(CollectOrderItem())
return "Ready to collect another item."
@function_tool
async def finish_order(self, context: RunContext) -> str:
"""Finish the order and proceed to payment."""
if not self.order_items:
return "No items in the order yet."
total = sum(item.unit_price * item.quantity for item in self.order_items)
payment_task = CollectPayment(total=total)
payment = await payment_task.run(self.session)
summary = ", ".join(f"{i.quantity}x {i.name}" for i in self.order_items)
return f"Order finalized: {summary}. Total: ${total:.2f}. Payment: {payment.method}."import { Agent, TaskGroup, functionTool, RunContext } from "@livekit/agents";
import { CollectOrderItem } from "./collectOrderItem";
import { CollectPayment } from "./collectPayment";
class OrderTakerAgent extends Agent {
private taskGroup: TaskGroup | null = null;
private orderItems: any[] = [];
constructor() {
super({
instructions: `You are the order taker at Bella Vista.
Use take_order to start collecting items.
After each item, ask if they want to add more.`,
});
}
@functionTool({ description: "Start collecting order items." })
async takeOrder(context: RunContext): Promise<string> {
this.taskGroup = new TaskGroup();
this.taskGroup.add(new CollectOrderItem());
const results = await this.taskGroup.run(this.session);
this.orderItems = results.filter((r) => r !== null && r?.name !== undefined);
const total = this.orderItems.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
return `Collected ${this.orderItems.length} items. Total: $${total.toFixed(2)}`;
}
@functionTool({ description: "Add another item to the order." })
async addAnotherItem(context: RunContext): Promise<string> {
if (!this.taskGroup) return "No active order. Use take_order first.";
this.taskGroup.add(new CollectOrderItem());
return "Ready to collect another item.";
}
@functionTool({ description: "Finish the order and proceed to payment." })
async finishOrder(context: RunContext): Promise<string> {
if (this.orderItems.length === 0) return "No items in the order yet.";
const total = this.orderItems.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
const paymentTask = new CollectPayment(total);
const payment = await paymentTask.run(this.session);
const summary = this.orderItems.map((i) => `${i.quantity}x ${i.name}`).join(", ");
return `Order finalized: ${summary}. Total: $${total.toFixed(2)}. Payment: ${payment.method}.`;
}
}Dynamic add() is powerful
You can call group.add() while the group is running. This means the LLM can decide — based on the conversation — to add more tasks to the queue. The group keeps running until all queued tasks are complete and no new ones have been added.
Regression: going back to a previous step
Sometimes the customer says "Actually, I want to change my first item." TaskGroup supports regression — returning to a previous task to redo it.
from livekit.agents import Agent, TaskGroup, function_tool, RunContext
from collect_order_item import CollectOrderItem
class OrderTakerAgent(Agent):
def __init__(self):
super().__init__(
instructions="""You are the order taker at Bella Vista.
If the customer wants to change a previous item,
use the change_item tool with the item number.""",
)
@function_tool
async def change_item(self, context: RunContext, item_number: int) -> str:
"""Go back and change a previously ordered item."""
if self.task_group is None:
return "No active order."
# Regress to the specified step (0-indexed)
self.task_group.regress(item_number - 1)
return f"Going back to item {item_number}. Let's redo that one."import { Agent, TaskGroup, functionTool, RunContext } from "@livekit/agents";
class OrderTakerAgent extends Agent {
private taskGroup: TaskGroup | null = null;
constructor() {
super({
instructions: `You are the order taker at Bella Vista.
If the customer wants to change a previous item,
use the change_item tool with the item number.`,
});
}
@functionTool({ description: "Go back and change a previously ordered item." })
async changeItem(context: RunContext, itemNumber: number): Promise<string> {
if (!this.taskGroup) return "No active order.";
this.taskGroup.regress(itemNumber - 1);
return `Going back to item ${itemNumber}. Let's redo that one.`;
}
}regress() takes a zero-based index and jumps the group back to that step. The task at that index runs again, and its new result replaces the old one. All subsequent tasks re-run as well, since their context may depend on the changed step. This is especially useful for ordering flows where changing an item affects the total, which affects the payment step.
Context summarization
Each task in a TaskGroup runs its own conversational loop. Without summarization, the conversation history from previous tasks would pile up, consuming tokens and potentially confusing the LLM with irrelevant context.
TaskGroup automatically summarizes the conversation between tasks. When one task completes and the next begins, the previous conversation is compressed into a brief summary that gives the new task the context it needs without the full transcript.
from livekit.agents import TaskGroup
from collect_order_item import CollectOrderItem
from collect_payment import CollectPayment
# Context summarization is enabled by default
group = TaskGroup(
summarize_between_tasks=True, # This is the default
summary_instructions="Summarize what has been ordered so far, including item names, quantities, and modifications.",
)
group.add(CollectOrderItem())
group.add(CollectOrderItem())
group.add(CollectPayment(total=0)) # Total will be calculated from resultsimport { TaskGroup } from "@livekit/agents";
import { CollectOrderItem } from "./collectOrderItem";
import { CollectPayment } from "./collectPayment";
const group = new TaskGroup({
summarizeBetweenTasks: true,
summaryInstructions: "Summarize what has been ordered so far, including item names, quantities, and modifications.",
});
group.add(new CollectOrderItem());
group.add(new CollectOrderItem());
group.add(new CollectPayment(0));Summarization saves tokens and improves focus
Without summarization, a five-item order could accumulate hundreds of conversational turns before reaching the payment step. The payment task does not need to know that the customer asked about allergens on item two. Summarization compresses "the customer ordered 2x Bruschetta and 1x Grilled Salmon with no sauce" into a single context message, keeping the next task focused and efficient.
What you learned
TaskGrouporchestrates multiple tasks in sequence, collecting results from eachadd()queues new tasks, and can be called dynamically while the group is runningregress()jumps back to a previous step to redo it, re-running subsequent tasks- Context summarization compresses conversation history between tasks to save tokens and keep each task focused
Test your knowledge
Question 1 of 2
What happens when you call regress() on a TaskGroup?
Next up
Building custom tasks for every data collection need takes time. In the next chapter, you will discover LiveKit's prebuilt tasks — ready-made tasks for collecting emails, addresses, DTMF input, and handling warm transfers.