Compare

build the agent, orchestrate it as a pipeline

ZenML and the Claude Agent SDK solve different problems. Keep the SDK for building the agent, and put a ZenML pipeline around it for versioned artifacts, step caching, approvals, sandboxes, and deployment on your own cloud.

ZenML and the Claude Agent SDK aren’t alternatives. The Agent SDK builds agents that read files, run commands, search the web, and edit code inside a session you start. It’s a very good way to write the agent itself.

ZenML is the orchestration layer you put around that work when it stops being a session and becomes a workload. The agent code stays exactly as you wrote it, inside a @step. What ZenML adds is everything around the call: the run record, the versioned artifacts, the cache that stops you paying twice for work that didn’t change, an approval gate a human can answer days later, and a stack that puts the whole thing on Kubernetes, Vertex AI, SageMaker, or AzureML without a rewrite.

For a one-off task at your keyboard, you don’t need any of that. For a hundred runs a day whose outputs someone will ask about next month, you do.

ZenML

Use ZenML if you are

  • Running agent work as a scheduled or served workload rather than an interactive session
  • Asked, weeks later, which output came from which run and which inputs
  • Re-running a pipeline often enough that skipping unchanged steps saves real money
  • Gating an agent's action behind a human approval that may take hours or days to arrive
  • Deploying into your own cloud (Kubernetes, Vertex AI, SageMaker, AzureML) for security or compliance
  • Combining the agent with the rest of a pipeline: retrieval, evaluation, batch inference, a training step

Claude Agent SDK

Use the Claude Agent SDK if you are

  • Building autonomous coding agents that edit files, run bash, and search codebases
  • Running interactive sessions where you're at the keyboard to approve and guide
  • Prototyping a one-off task or a short-lived workflow
  • Relying on its per-tool permission model, hooks, and file-edit rewind
The Agent SDK decides what the agent does, and ZenML decides what happens to the result: where it is stored, what it is called, and whether you ever have to compute it again.

What a pipeline adds around the agent

The Agent SDK gives you a session. A ZenML pipeline gives that session a place in a system of record.

  • The run: Wrap the agent call in a @step inside a @pipeline and each execution becomes a run with a start, an end, a status, and a history you can open in the dashboard.
  • Versioned artifacts: Whatever the step returns is stored in your artifact store, versioned, and addressable. Return Annotated[str, "patch"] and the output is retrievable by name across runs rather than buried in a session log.
  • Metadata: ZenML has no LLM-specific primitive, and it doesn’t try to parse what the SDK did. It gives you a place to put what you care about: log_metadata() attaches the turn count, cost, or tool-call count from the SDK’s result message to the step.
  • Async steps: The SDK’s query() is an async generator. A step can be declared async def, and ZenML runs the coroutine to completion when the step executes, so the stream is consumed inside the step with no wrapper.

Not re-paying for work that didn’t change

The Agent SDK writes session history to disk and can rewind file edits. That’s useful while you’re working, but it isn’t a cache across runs.

  • The cache key: ZenML keys the cache on the step, its parameters, and its inputs: the step’s source path, the parameter values, the input artifacts, the output definitions and materializers, plus the environment variables and secrets the step declares. If nothing changed, you get the stored artifact back and the body never executes. Every run, not only after a failure.
  • What that means in practice: Retrieve context, run the agent, evaluate the result. Edit only the evaluation step and only the evaluation step re-runs. Retrieval and the agent call come from cache.
  • Turning it off: A step that must always execute takes @step(enable_cache=False)That’s the right setting for the agent step itself whenever non-determinism is the point. A cache_policy on the step narrows what the key includes when the default is too eager.

Approvals, sandboxes, and retries

Three things the Agent SDK handles inside a session and ZenML handles around the step.

  • Waiting for a human: In a dynamic pipeline, wait(schema=bool, question="Apply this patch?") pauses the run until someone answers it from the dashboard, the CLI, or the API. Once the wait’s timeout elapses and no other work in the run is in flight, ZenML marks the run PAUSED so the orchestration process can be torn down, and zenml pipeline runs resume picks it up later, or ZenML Pro resumes it automatically. The SDK’s permission prompts go to a callback in the running process, so the process has to stay up to answer them but the wait doesn’t.
  • Isolated tool loops: A sandbox is a stack component with local, Docker, Kubernetes, and Modal flavors. Inside a step, Client().active_stack.sandbox.create_session() gives the agent an isolated environment to run generated code in, with session.exec([...]), streamed output, and, on the Docker and Modal flavors, snapshots you can store as artifacts and restore in a later run.
  • Retries: A step takes retry=StepRetryConfig(max_retries=3, delay=10, backoff=2). A failed dynamic pipeline run is retried with zenml pipeline runs retry, which reuses the steps that already completed. A static pipeline like the one below gets the same effect from the cache on a re-run. ZenML retries at step granularity; it won’t resume a step mid-body.

Your infrastructure, your data

  • Where it runs: zenml stack set repoints the same pipeline from your laptop to Kubernetes, Vertex AI, SageMaker, or AzureML. The agent code doesn’t change.
  • Where artifacts land: Your own S3, GCS, or Azure Blob bucket. Self-host the ZenML server and the run metadata stays with you too.
  • Serving: zenml pipeline deploy stands the pipeline up behind an endpoint, and every request becomes a run with the same artifacts and lineage as a batch job.

What ZenML doesn’t do

Three things the SDK does that ZenML won’t. They decide how you use the two together. ZenML has:

  • **No per-tool permission model: **The SDK’s allowed_tools, permission_mode, and hooks decide what the agent may touch inside the step, and there is no ZenML equivalent. A sandbox isolates where code runs; it doesn’t decide which tools the model may call.
  • No file-edit rewind: A step returns artifacts, and a re-run produces new artifact versions. It doesn’t roll back what the agent did to a working tree.
  • No agent abstractions: Doesn’t define agents, tools, or sessions. It runs whatever you built and records what came out.

Want to replay the same agent against a prompt or model change? Kitaru’s Claude Agent SDK adapter records one-shot query() calls and reruns them. See Kitaru vs Claude Agent SDK.

What makes ZenML different

FeatureZenMLClaude Agent SDKWhat that means
Versioned artifacts and lineage across runsYesNot supportedStep outputs are stored and loadable by name; the SDK keeps a session history.
Caching that skips unchanged steps on a re-runYesNot supportedKeyed on the step, its parameters, and its inputs. Prompt caching in the Claude stack is a different mechanism.
Durable human approval that outlives the processYesPartial support`wait()` pauses the run until a human resolves it. The SDK's permission prompts need the process to stay up to answer them.
Isolated environment for tool executionYesYesZenML: the sandbox stack component (Docker, Kubernetes, Modal). The SDK: its own sandboxing options.
One stack abstraction for Kubernetes, Vertex AI, SageMaker, AzureMLYesNot supportedConfigure once, every pipeline uses it.
Run history you can inspect and compareYesPartial supportThe SDK writes session transcripts; ZenML records runs, steps, and artifacts in a server.
Serve it and get lineage per requestYesNot supported`zenml pipeline deploy` makes every request a tracked run.
Per-tool permission model and file-edit rewindNot supportedYesThe SDK's job. Keep using it inside the step.
Autonomous file editing, bash, and codebase searchNot supportedYesZenML orchestrates the agent; it isn't an agent.
Open source, self-hostableYesPartial supportThe SDK is MIT. It drives the bundled Claude Code CLI against Anthropic's API, neither of which you host.

How the two surfaces map

ConceptClaude Agent SDKZenML
Unit of workAn agent session@step (the session runs inside it)
BoundaryThe task you give the agent@pipeline
HistorySession transcript on diskRuns, steps, and artifacts in the server
OutputsFiles the agent editedVersioned artifacts in your bucket
Avoiding repeat workPrompt caching, file-edit rewindenable_cache across runs
Human approvalPermission prompts in the sessionwait() in a dynamic pipeline
IsolationSDK sandboxing and allowed_toolssandbox stack component
Where it runsWherever you start the sessionStack orchestrator (zenml stack set)
ServingYour own wrapperzenml pipeline deploy --name ...
Cross-run reuseNot applicableget_artifact_version(...).load()

Code comparison

ZenML (wrapping the SDK)
from typing import Annotated

from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
from zenml import log_metadata, pipeline, step
from zenml.client import Client

@step
def gather_context(repo: str) -> Annotated[str, "context"]:
  return read_repo_summary(repo)

# The agent step is non-deterministic, so caching is off here
# while the steps around it stay cached. query() is an async
# generator, so the step is async and ZenML runs it to completion.
@step(enable_cache=False)
async def run_agent(context: str, task: str) -> Annotated[str, "patch"]:
  options = ClaudeAgentOptions(
      allowed_tools=["Read", "Edit", "Bash"],
      permission_mode="acceptEdits",
  )
  patch = ""
  async for message in query(
      prompt=f"{context}\n\nTask: {task}", options=options
  ):
      if isinstance(message, ResultMessage):
          patch = message.result or ""
          log_metadata(metadata={
              "task": task,
              "turns": message.num_turns,
              "cost_usd": message.total_cost_usd,
          })
  return patch

@step
def evaluate(patch: str) -> Annotated[dict, "scores"]:
  return score(patch)

@pipeline
def agent_pipeline(repo: str, task: str):
  evaluate(run_agent(gather_context(repo), task))

agent_pipeline("zenml-io/zenml", "add retry to the client")

# Edit evaluate() and re-run: gather_context is cached.
Client().get_artifact_version("patch").load()

# Same code on your cloud:
#   zenml stack set k8s_stack && python agent_pipeline.py
Claude Agent SDK alone
import asyncio

from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query

# The SDK builds and runs the agent. It reads files, runs
# commands, and edits code inside a session you start.
async def main() -> None:
  context = read_repo_summary("zenml-io/zenml")
  task = "add retry to the client"
  options = ClaudeAgentOptions(
      allowed_tools=["Read", "Edit", "Bash"],
      permission_mode="acceptEdits",
  )

  patch = ""
  async for message in query(
      prompt=f"{context}\n\nTask: {task}", options=options
  ):
      if isinstance(message, ResultMessage):
          patch = message.result or ""

  print(score(patch))

asyncio.run(main())

# The session transcript is written to disk, and with file
# checkpointing on, edits can be rewound. What there isn't: a run record, a versioned
# artifact you can load back by name next month, a cache that
# skips read_repo_summary on the next run, an approval that
# survives the process exiting, or a way to put this same code
# on Kubernetes without writing the deployment.

Put a pipeline around
your Agent SDK work

Keep the Claude Agent SDK. It builds the agent, decides which tools it may call, and does the work. Add ZenML when that work becomes a workload: someone needs to know what a run produced, a human has to approve the action and might not be around for a day, re-running everything from scratch starts costing real money, and it has to run on your own infrastructure rather than your laptop.