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
@stepinside a@pipelineand 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 declaredasync 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. Acache_policyon 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 runPAUSEDso the orchestration process can be torn down, andzenml pipeline runs resumepicks 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
sandboxis 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, withsession.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 withzenml 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 setrepoints 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 deploystands 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
| Feature | ZenML | Claude Agent SDK | What that means |
|---|---|---|---|
| Versioned artifacts and lineage across runs | Yes | Not supported | Step outputs are stored and loadable by name; the SDK keeps a session history. |
| Caching that skips unchanged steps on a re-run | Yes | Not supported | Keyed on the step, its parameters, and its inputs. Prompt caching in the Claude stack is a different mechanism. |
| Durable human approval that outlives the process | Yes | Partial 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 execution | Yes | Yes | ZenML: the sandbox stack component (Docker, Kubernetes, Modal). The SDK: its own sandboxing options. |
| One stack abstraction for Kubernetes, Vertex AI, SageMaker, AzureML | Yes | Not supported | Configure once, every pipeline uses it. |
| Run history you can inspect and compare | Yes | Partial support | The SDK writes session transcripts; ZenML records runs, steps, and artifacts in a server. |
| Serve it and get lineage per request | Yes | Not supported | `zenml pipeline deploy` makes every request a tracked run. |
| Per-tool permission model and file-edit rewind | Not supported | Yes | The SDK's job. Keep using it inside the step. |
| Autonomous file editing, bash, and codebase search | Not supported | Yes | ZenML orchestrates the agent; it isn't an agent. |
| Open source, self-hostable | Yes | Partial support | The SDK is MIT. It drives the bundled Claude Code CLI against Anthropic's API, neither of which you host. |
How the two surfaces map
| Concept | Claude Agent SDK | ZenML |
|---|---|---|
| Unit of work | An agent session | @step (the session runs inside it) |
| Boundary | The task you give the agent | @pipeline |
| History | Session transcript on disk | Runs, steps, and artifacts in the server |
| Outputs | Files the agent edited | Versioned artifacts in your bucket |
| Avoiding repeat work | Prompt caching, file-edit rewind | enable_cache across runs |
| Human approval | Permission prompts in the session | wait() in a dynamic pipeline |
| Isolation | SDK sandboxing and allowed_tools | sandbox stack component |
| Where it runs | Wherever you start the session | Stack orchestrator (zenml stack set) |
| Serving | Your own wrapper | zenml pipeline deploy --name ... |
| Cross-run reuse | Not applicable | get_artifact_version(...).load() |
Code comparison
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.pyimport 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.









