Chapter 525m

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.

TaskGroupadd()RegressionContext summarization

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.

models.pypython
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.0
models.tstypescript
import { 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:

collect_payment.pypython
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))
collectPayment.tstypescript
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:

order_taker_agent.pypython
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}"
orderTakerAgent.tstypescript
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)}`;
}
}
What's happening

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.

order_taker_agent.pypython
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}."
orderTakerAgent.tstypescript
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.

regression_example.pypython
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."
regressionExample.tstypescript
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.`;
}
}
What's happening

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.

summarization_example.pypython
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 results
summarizationExample.tstypescript
import { 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

  • TaskGroup orchestrates multiple tasks in sequence, collecting results from each
  • add() queues new tasks, and can be called dynamically while the group is running
  • regress() 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.

Concepts covered
TaskGroupadd()RegressionContext summarization