Compare

build the crew, orchestrate it as a pipeline

CrewAI is a Python framework for multi-agent crews with roles, tasks, and Flows. ZenML is the orchestration layer around it: versioned artifacts, step caching, human approval, sandboxes, your own cloud.

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.

CrewAI · framework + platformDefines the agent stack from harness to platform
CrewAI framework
Agents · roles · goalsTasks · Crews · FlowsTools · Memory · KnowledgeAMP · Studio · tracing · automations
Adopt CrewAI's abstractions across the stack.
One product, top to bottom.
ZenML · orchestration, one layer of fourOwns the orchestration layer, leaves harness and platform untouched
Platformyour auth, observability, policy
HarnessCrewAI, PydanticAI, OpenAI Agents, raw Python
OrchestrationZenML: @pipeline, @step, wait(), sandboxes, artifacts
ModelOpenAI, Anthropic, Google, open-weights
ZenML owns one layer. The rest of your stack stays as you picked it.
  • 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.

CrewAI FlowStateful Flow, a re-run starts from the top
step 1research()LLM
step 2plan()LLM
step 3draft()LLM
step 4critique()crash
Re-runAll 4 LLM calls billed again on the next run.
ZenML · @stepRetry reuses the completed steps above the failure
step 1research()reused
step 2plan()reused
step 3draft()reused
step 4critique()re-runs
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.

CrewAI AMP · managed agent platformDeploy from GitHub or ZIP, hit endpoints with tokens
Agent Management Platform
deploymentsenv varsendpointstokenslive monitoringtracing
Control planehosted by CrewAI
Platform managed for you. Data flows through the AMP.
ZenML · self-hosted orchestrationSingle service, your cloud, your bucket
zenml-serversingle service · Helm-deployable
S3 / GCS / Azure Blobartifacts and step outputs in your own bucket
stack abstractionKubernetes · Vertex AI · SageMaker · AzureML · more
zenml pipeline deploylong-running HTTP service, a tracked run per request
No mandatory hosted control plane in the data path.
  • 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 sagemaker repoints the same code from a local orchestrator to Kubernetes, Vertex AI, SageMaker, AzureML, or any other orchestrator flavor.
  • Serving: zenml pipeline deploy runs 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.

CrewAI · adopt the abstractionsModel agents as Agents, Tasks, Crews, and Flows
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"})
Your agent code is built inside CrewAI's model.
ZenML · wrap whatever you already use@pipeline + @step over ordinary Python
# 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
One orchestration layer under whatever harness you picked.
  • 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:

python
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

FeatureZenMLCrewAIWhat that means
Versioned artifacts and lineage across runsYesNot supportedStep outputs are stored and loadable by name.
Caching that skips unchanged steps on a re-runYesNot supportedKeyed on the step, its parameters and its inputs; a crew re-run starts from the top.
Pause the run for human approval, resume laterYesPartial 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 loopsYesPartial supportZenML'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 requestYesPartial 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 moreYesPartial supportCrewAI has a managed deployment platform; ZenML targets your own clouds.
Artifacts and run data stay in your own infrastructureYesPartial supportZenML writes to your bucket and self-hosts.
Works with any agent framework, not just oneYesNot supportedA step is ordinary Python; CrewAI is the harness itself.
Multi-agent roles, delegation, and collaborationNot supportedYesCrewAI's whole point. ZenML doesn't define agents, it runs them.
Stateful Flows for explicit agent controlNot supportedYesUse CrewAI Flows inside a ZenML step.
Tracing of LLM and tool calls inside the runNot supportedYesZenML records the step, its metadata and its artifacts, not the calls within it.
Open source, self-hostableYesYes

How the two surfaces map

ConceptCrewAIZenML
Unit of workTask, executed by an Agent@step (the crew runs inside it)
BoundaryCrew / Flow@pipeline
Control flowSequential or hierarchical processThe pipeline graph, or Python control flow with dynamic=True
Human approvalhuman_input=True on a Task, in the terminalwait() in a dynamic pipeline, resolved from CLI, dashboard or API
Code executionCode interpreter tool in DockerSandbox stack component (local, Docker, Kubernetes, Modal)
OutputsCrewOutput returned in memoryVersioned artifacts in your bucket
Avoiding repeat workRe-run starts from the topenable_cache across runs, zenml pipeline runs retry after a failed dynamic run
Where it runsYour process, or CrewAI’s platformStack orchestrator (zenml stack set)
Cross-run reuseBring your own storeget_artifact_version(...).load()
ServingYour own wrapper, or the platformzenml pipeline deploy --name ...

Code comparison

ZenML (wrapping the crew)
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()
CrewAI alone
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.