CrewAI is a framework for building multi-agent systems: agents with roles and backstories, tasks with expected outputs, and Flows for when you need explicit control over the sequence. It’s opinionated about how agents collaborate, and that opinion is the reason to pick it.
ZenML isn’t another opinion about agent collaboration; it’s the orchestration layer around whatever you built. Your crew runs unchanged inside a @step, and ZenML supplies the pipeline around it: a run record, versioned artifacts in your own bucket, a cache keyed on the step, its parameters and its inputs, a wait() that pauses the run until a human approves what the crew proposed, sandboxes for the code the crew’s tools execute, and a stack that puts the whole thing on Kubernetes, Vertex AI, SageMaker, AzureML and more.
CrewAI answers how the agents work together, and ZenML answers what happened when they did, who signed off on it, and what it costs to do it again.
ZenML
Use ZenML if you are
- Running a crew on a schedule or behind an endpoint, not from a terminal by hand
- Asked, weeks later, which output came from which run and which inputs
- Pausing for a human to approve what the crew produced before the next step acts on it
- Re-running often enough that skipping unchanged steps saves real money
- Deploying into your own cloud for security or compliance, with the outputs in your own bucket
CrewAI
Use CrewAI if you are
- Designing how multiple agents divide work, delegate, and collaborate on a task
- Wanting roles, goals, and task definitions as the primary abstraction
- Using Flows for explicit, stateful control over a multi-step agent process
- Prototyping quickly, where the crew itself is the thing you're iterating on
CrewAI decides how the agents work together. ZenML decides what the run produced, where it lives, who approved it, and whether you have to pay for it twice.
Orchestration around the agent stack
A crew is a program. A pipeline is the thing that runs programs and remembers what they did.
@pipeline, @step, wait(), sandboxes, artifacts- No rewrite:
crew.kickoff()goes inside a@step. Your agents, tasks, and tools are untouched. - What ZenML owns: The run record, the artifacts, the cache, the approval gate, the sandbox, and the deployment target. Not the agent logic.
- What CrewAI keeps owning: Roles, delegation, task routing, and the collaboration model. ZenML doesn’t define agents, it runs them.
Re-running without re-paying
A crew that makes a dozen LLM calls is expensive to run once and painful to run ten times while you tune the step after it. Re-running shouldn’t mean re-paying for the part you didn’t touch.
zenml pipeline runs retry <run> reads steps 1-3 from the artifact store.- Cache key: ZenML keys the cache on the step, its parameters and its inputs, plus its output definitions and the artifact store. If nothing changed, you get the stored artifact back and the step body never executes. That’s every run, not only after a failure.
- After a failure:
zenml pipeline runs retry <run>re-runs a failed dynamic pipeline run, and the steps that completed are reused rather than re-executed. ZenML retries at step granularity; it won’t resume a step part-way through its body. - The exception: Mark the crew step
@step(enable_cache=False)when you want it to execute every time, and keep the cache on the deterministic steps around it.
Your own cloud, your own bucket
CrewAI offers a managed platform for deploying and monitoring crews. ZenML’s answer is the stack. Here’s the difference: where the data ends up.
- Stack components: An orchestrator plus an artifact store, registered once with
zenml stack register, then used by every pipeline. Add a sandbox component and the crew’s tools get an isolated place to run code. - Switching:
zenml stack set sagemakerrepoints the same code from a local orchestrator to Kubernetes, Vertex AI, SageMaker, AzureML, or any other orchestrator flavor. - Serving:
zenml pipeline deployruns the pipeline as a long-running HTTP service, and every request becomes a tracked run with the same artifacts as a batch job. - Where data lives: Artifacts land in your own S3, GCS, or Azure Blob bucket. Self-host the server with Helm, or use ZenML Pro for the managed control plane; the artifacts stay in your bucket either way.
One pipeline, whichever framework each team picked
Most organizations don’t standardize on one agent framework. One team is on CrewAI, another on Pydantic AI, a third calls the OpenAI Agents SDK directly. A step is just Python, so ZenML sits under all of them.
from crewai import Agent, Task, Crew researcher = Agent(role="Researcher", ...) writer = Agent(role="Writer", ...) research = Task(agent=researcher, ...) draft = Task(agent=writer, ...) crew = Crew(agents=[researcher, writer], tasks=[research, draft]) crew.kickoff(inputs={"topic": "agents"})
# A CrewAI crew inside a ZenML step @step def run_crew(topic: str) -> str: return crew.kickoff(inputs={"topic": topic}).raw # A PydanticAI agent inside a ZenML step @step def critique(text: str) -> str: return pydantic_agent.run_sync(text).output # An OpenAI Agents SDK call inside a ZenML step @step def decide(text: str) -> str: return Runner.run_sync(agent, text).final_output
- No adapter needed: Whatever the framework, the call goes in a step and the return value becomes an artifact.
- One record: Runs from every team land in the same server, with the same artifact and lineage model.
- The trade: ZenML gives you nothing framework-specific in return. It doesn’t know what a crew is, and it won’t help you design one.
Where the human comes in
A crew that drafts something is easy. A crew whose draft gets acted on is where someone wants to sign off first. In a dynamic pipeline, wait() is that signature:
from zenml import pipeline, step, wait
@pipeline(dynamic=True)
def crew_pipeline(topic: str):
draft = run_crew(gather_context(topic), topic)
approved = wait(schema=bool, question="Publish this brief?")
if approved:
publish(draft)The run polls for the answer for timeout seconds (600 by default). Nobody answered? The run is marked paused and the orchestration process can be torn down instead of sitting open for days. Someone resolves the condition from the dashboard, the API, or zenml pipeline runs wait-conditions resolve --run <id> --interactive, and zenml pipeline runs resume <run> (or ZenML Pro, automatically) continues from that point with the crew’s draft still in the artifact store. The schema can be a Pydantic model, so the approver can hand back edits, not just a yes.
What ZenML doesn’t do is trace the LLM and tool calls inside the crew step. Want to record those runs and replay them against a prompt or model change? That’s Kitaru’s job.
What makes ZenML different
| Feature | ZenML | CrewAI | What that means |
|---|---|---|---|
| Versioned artifacts and lineage across runs | Yes | Not supported | Step outputs are stored and loadable by name. |
| Caching that skips unchanged steps on a re-run | Yes | Not supported | Keyed on the step, its parameters and its inputs; a crew re-run starts from the top. |
| Pause the run for human approval, resume later | Yes | Partial support | `wait()` in a dynamic pipeline pauses the run until it's resolved from the CLI, dashboard or API. CrewAI can prompt for input while the process waits. |
| Isolated code execution for tool loops | Yes | Partial support | ZenML's sandbox is a stack component with local, Docker, Kubernetes and Modal flavors. CrewAI's code interpreter tool runs in Docker. |
| Serve it and get a tracked run per request | Yes | Partial support | `zenml pipeline deploy` on your own infrastructure; CrewAI's platform hosts the deployment for you. |
| One stack abstraction for Kubernetes, Vertex AI, SageMaker, AzureML and more | Yes | Partial support | CrewAI has a managed deployment platform; ZenML targets your own clouds. |
| Artifacts and run data stay in your own infrastructure | Yes | Partial support | ZenML writes to your bucket and self-hosts. |
| Works with any agent framework, not just one | Yes | Not supported | A step is ordinary Python; CrewAI is the harness itself. |
| Multi-agent roles, delegation, and collaboration | Not supported | Yes | CrewAI's whole point. ZenML doesn't define agents, it runs them. |
| Stateful Flows for explicit agent control | Not supported | Yes | Use CrewAI Flows inside a ZenML step. |
| Tracing of LLM and tool calls inside the run | Not supported | Yes | ZenML records the step, its metadata and its artifacts, not the calls within it. |
| Open source, self-hostable | Yes | Yes |
How the two surfaces map
| Concept | CrewAI | ZenML |
|---|---|---|
| Unit of work | Task, executed by an Agent | @step (the crew runs inside it) |
| Boundary | Crew / Flow | @pipeline |
| Control flow | Sequential or hierarchical process | The pipeline graph, or Python control flow with dynamic=True |
| Human approval | human_input=True on a Task, in the terminal | wait() in a dynamic pipeline, resolved from CLI, dashboard or API |
| Code execution | Code interpreter tool in Docker | Sandbox stack component (local, Docker, Kubernetes, Modal) |
| Outputs | CrewOutput returned in memory | Versioned artifacts in your bucket |
| Avoiding repeat work | Re-run starts from the top | enable_cache across runs, zenml pipeline runs retry after a failed dynamic run |
| Where it runs | Your process, or CrewAI’s platform | Stack orchestrator (zenml stack set) |
| Cross-run reuse | Bring your own store | get_artifact_version(...).load() |
| Serving | Your own wrapper, or the platform | zenml pipeline deploy --name ... |
Code comparison
from typing import Annotated
from crewai import Agent, Crew, Task
from zenml import pipeline, step, wait
from zenml.client import Client
@step
def gather_context(topic: str) -> Annotated[str, "context"]:
return retrieve_documents(topic)
# Non-deterministic, so it always executes; the steps around
# it stay cached.
@step(enable_cache=False)
def run_crew(context: str, topic: str) -> Annotated[str, "draft"]:
researcher = Agent(role="Researcher", goal=f"Research {topic}")
writer = Agent(role="Writer", goal="Write a brief")
crew = Crew(
agents=[researcher, writer],
tasks=[
Task(description=context, agent=researcher,
expected_output="notes"),
Task(description="Write it up", agent=writer,
expected_output="a brief"),
],
)
return str(crew.kickoff())
@step
def publish(draft: str) -> None:
post_to_cms(draft)
@pipeline(dynamic=True)
def crew_pipeline(topic: str):
draft = run_crew(gather_context(topic), topic)
# Pauses the run until someone answers from the
# dashboard, the API, or the CLI.
approved = wait(schema=bool, question="Publish this brief?")
if approved:
publish(draft)
crew_pipeline("AI orchestration")
# Edit publish() and re-run: gather_context is cached.
Client().get_artifact_version("draft").load()from crewai import Agent, Crew, Task
topic = "AI orchestration"
context = retrieve_documents(topic)
researcher = Agent(role="Researcher", goal=f"Research {topic}")
writer = Agent(role="Writer", goal="Write a brief")
crew = Crew(
agents=[researcher, writer],
tasks=[
Task(description=context, agent=researcher,
expected_output="notes"),
Task(description="Write it up", agent=writer,
expected_output="a brief"),
],
)
draft = str(crew.kickoff())
if input("Publish this brief? ") == "y":
post_to_cms(draft)
# The crew runs and returns. What there isn't: a run record,
# a versioned artifact you can load back by name next month,
# a cache that skips retrieval on the next run, an approval
# that survives the process being torn down, or a way to put
# this on Kubernetes without writing the deployment.Put a pipeline
around your crew
Keep CrewAI for what it’s good at: deciding how agents divide work and collaborate. Add ZenML when the crew stops being a script: a human has to approve what it produced, someone will ask next month what a run produced, and it has to run on your own infrastructure with the outputs in a bucket you control.









