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.
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.
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 answerTurning streaming on
Build the crew with stream=True and give the clerk the streaming stand-in.
clerk = Agent(role="Order clerk", ..., llm=StreamingShopLLM(model="shop"),
tools=[lookup_order])
crew = Crew(agents=[clerk], tasks=[look], stream=True, verbose=False)View the code here
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."
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?"
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.
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)text: 'A17' text: ' shipped' text: ' on' text: ' 3' text: ' March.' full reply: A17 shipped on 3 March.
What the chunks carried
- Each chunk is a
StreamChunkwhosecontentis a piece of the answer and whosechunk_typeistext. - 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.
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)flow | flow_started flow | flow_finished result: Looking into A17.
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=True | flow.stream_events | |
|---|---|---|
| What you get | StreamChunk objects | StreamFrame objects |
| The payload | chunk.content, the text | frame.channel and frame.type |
| Final result | streaming.result | session.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.
streaming.result, or it raises because the stream has not finished.Related
- Previous: Human feedback: pausing a flow
- Next: Knowledge sources: a reference library for agents
- Reference: Streaming
- Join the chunks with
"".join(c.content for c in streaming)and compare withresult.raw. - Print
chunk.agent_rolealongside each chunk. - Iterate
sessioninstead ofsession.flowand print every frame's channel.
Every expert started right here.