CrewAICrewAI 1.15 · Python 3.10 to 3.13
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
35 small wins to finish your pathNext lesson →

Streaming a crew's output

Streaming hands you a crew's output as it is produced: Crew(stream=True) yields text chunks, and a flow's stream_events yields frames for each step.

Last updated: 28 Sep, 2026 · CrewAI 1.15

So far a kickoff returned only after the whole run finished. Streaming lets a program show the answer as it forms, and watch each step of a flow as it happens, instead of waiting for the end.

The streaming API

A crew built with stream=True returns an object you iterate for chunks; its .result holds the final output once you have read them all. A flow's stream_events returns frames tagged by channel.

python
crew = Crew(agents=[...], tasks=[...], stream=True)
streaming = crew.kickoff(inputs={...})
for chunk in streaming:      # StreamChunk objects
    print(chunk.content, end="")
result = streaming.result    # the CrewOutput, after the loop

for frame in flow.stream_events(inputs={...}):   # StreamFrame objects
    print(frame.channel, frame.type)

A stand-in that streams

A hosted model streams its tokens as it writes them. The stand-in returns a whole string, so it emits the words as chunks itself, which is what makes this a real streamed run with no key.

python
class StreamingShopLLM(ShopLLM):
    def call(self, messages, tools=None, **kwargs):
        answer = super().call(messages, tools=tools, **kwargs)
        if isinstance(answer, str):
            for i, word in enumerate(answer.split(" ")):
                piece = word if i == 0 else " " + word
                self._emit_stream_chunk_event(piece, from_task=kwargs.get("from_task"),
                                              from_agent=kwargs.get("from_agent"))
        return answer

Turning streaming on

Build the crew with stream=True and give the clerk the streaming stand-in.

python
clerk = Agent(role="Order clerk", ..., llm=StreamingShopLLM(model="shop"),
              tools=[lookup_order])
crew = Crew(agents=[clerk], tasks=[look], stream=True, verbose=False)
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
tools.py
from crewai.tools import tool

ORDERS = {"A17": "shipped on 3 March", "C40": "waiting for stock"}


@tool
def lookup_order(order_id: str) -> str:
    """Look up an order's shipping status by its id, such as A17."""
    status = ORDERS.get(order_id)
    return f"{order_id} {status}." if status else f"{order_id} is not an order we have."
shop_llm.py
import json
import os
import re

from crewai import BaseLLM

os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
os.environ["CREWAI_TRACING_ENABLED"] = "false"
os.environ["CREWAI_DISABLE_VERSION_CHECK"] = "true"


class ShopLLM(BaseLLM):
    script: list = []

    def supports_function_calling(self):
        return True

    def call(self, messages, tools=None, **kwargs):
        if isinstance(messages, str):
            messages = [{"role": "user", "content": messages}]
        if self.script:
            return self.script.pop(0)
        names = [t["function"]["name"] for t in tools or []]
        return self.decide(messages, names)

    def decide(self, messages, tools):
        last = messages[-1]
        if last["role"] == "tool":
            return last["content"]
        text = last["content"]
        orders = re.findall(r"\b[A-Z]\d+\b", text)
        wanted = "refund_order" if "refund" in text.lower() else "lookup_order"
        matches = [name for name in tools if name.endswith(wanted)]
        if orders and matches:
            args = json.dumps({"order_id": orders[0]})
            return [{"id": f"call_{orders[0]}", "type": "function",
                     "function": {"name": matches[0], "arguments": args}}]
        if "working with:" in text:
            context = text.split("working with:")[1].strip().split("\n\n")[0]
            return f"Dear customer, {context}"
        if orders:
            return f"I have no way to look up {orders[0]} yet."
        return "Hello. Which order is this about?"
stream_llm.py
from shop_llm import ShopLLM


class StreamingShopLLM(ShopLLM):
    def call(self, messages, tools=None, **kwargs):
        answer = super().call(messages, tools=tools, **kwargs)
        if isinstance(answer, str):
            for i, word in enumerate(answer.split(" ")):
                piece = word if i == 0 else " " + word
                self._emit_stream_chunk_event(
                    piece, from_task=kwargs.get("from_task"),
                    from_agent=kwargs.get("from_agent"))
        return answer

Streaming a crew chunk by chunk

Iterate the streaming object for chunks, then read the final result.

Example
import os
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"

from crewai import Agent, Crew, Task
from stream_llm import StreamingShopLLM
from tools import lookup_order

clerk = Agent(role="Order clerk", goal="Find the status of customers' orders",
              backstory="You can look up any order in the shop's system.",
              llm=StreamingShopLLM(model="shop"), tools=[lookup_order])
look = Task(description="Find the order in this message: {question}",
            expected_output="The order's status.", agent=clerk)
crew = Crew(agents=[clerk], tasks=[look], stream=True, verbose=False)

streaming = crew.kickoff(inputs={"question": "Where is my order A17?"})
for chunk in streaming:
    print(f"{chunk.chunk_type.value}: {chunk.content!r}")
print("full reply:", streaming.result.raw)

What the chunks carried

  • Each chunk is a StreamChunk whose content is a piece of the answer and whose chunk_type is text.
  • The tool call emitted no text chunks; only the final answer streamed, word by word.
  • streaming.result is the full CrewOutput, available once every chunk has been read.

Streaming a flow's frames by channel

A flow streams frames, not text. stream_events returns a session; its .flow view yields only the lifecycle frames.

Example
import os
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"

import re
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel


class Ticket(BaseModel):
    message: str = ""
    order_id: str = ""
    reply: str = ""


class Desk(Flow[Ticket]):
    @start()
    def read_ticket(self):
        found = re.findall(r"\b[A-Z]\d+\b", self.state.message)
        self.state.order_id = found[0] if found else ""

    @listen(read_ticket)
    def answer(self):
        self.state.reply = f"Looking into {self.state.order_id}."
        return self.state.reply

desk = Desk(suppress_flow_events=True)
session = desk.stream_events(inputs={"message": "Where is my order A17?"})
for frame in session.flow:
    print(f"{frame.channel} | {frame.type}")
print("result:", session.result)

A StreamFrame carries a channel (llm, flow, tools, messages, lifecycle or custom), a type, a seq and a data dict. Filtering to the flow channel here shows the run start and finish.

Crew chunks vs flow frames

Crew stream=Trueflow.stream_events
What you getStreamChunk objectsStreamFrame objects
The payloadchunk.content, the textframe.channel and frame.type
Final resultstreaming.resultsession.result

When to stream

  • A person is waiting on the answer and should see it appear rather than a blank pause.
  • You want to trace a flow's steps live, for a progress display or a log.
Watch out. A crew emits chunks only when its model streams them, so a model that returns a whole string at once produces no chunks; the stand-in emits them by hand for this reason. Read every chunk before touching streaming.result, or it raises because the stream has not finished.
Try it yourself
  • Join the chunks with "".join(c.content for c in streaming) and compare with result.raw.
  • Print chunk.agent_role alongside each chunk.
  • Iterate session instead of session.flow and print every frame's channel.

Every expert started right here.