The OpenAI Agents SDK is a deliberately small framework: agents with instructions and tools, handoffs between them, guardrails on inputs and outputs, and sessions that carry conversation history. It stays out of your way, and for building the agent itself that restraint is the point.
That restraint is also why teams reach for something else once the agent stops being a script. The SDK runs your agent. It doesn’t decide where the run is recorded, what happened to the output, who approved it, or how you avoid paying for the same work twice. ZenML is the orchestration layer that does: your Runner.run() goes inside a @step, and the pipeline around it gives you 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 for a human, a sandbox for the code the agent’s tools execute, and a stack that targets Kubernetes, Vertex AI, SageMaker, AzureML and more.
ZenML
Use ZenML if you are
- Running agent work 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 agent 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
OpenAI Agents SDK
Use the OpenAI Agents SDK if you are
- Building the agent itself: instructions, tools, handoffs between specialised agents
- Wanting guardrails that validate inputs and outputs as a first-class concept
- Relying on Sessions to carry conversation history across turns
- Prototyping quickly, with the smallest possible framework in the way
The SDK runs the agent. ZenML records what the run produced, holds it until someone approves, and makes sure the next run doesn't pay for the parts that didn't change.
Re-running without starting from step one
A multi-step agent pipeline that crashes at the end is expensive to restart. So is tuning the last step while the first three keep re-executing.
- 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 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, so the retry doesn’t re-pay for the work above the failure. ZenML retries at step granularity. It won’t resume a step part-way through its body. - The agent step: Mark it
@step(enable_cache=False)when it must execute every time, and keep the cache on the deterministic steps around it. Addretry=StepRetryConfig(...)for the flaky ones.
Artifacts you can open, not just traces of what happened
The SDK’s tracing shows you the shape of a run: which agent ran, which tools fired, where a handoff happened. That’s the right tool for debugging one run. It isn’t a store of what the run produced.
model.requesttool.search_docsmodel.requesthandoffmodel.request@step researchbrief.json@step draftdraft.md@step reviewreview.jsonget_artifact_version()runs retrypipeline deploy- Artifacts: Every step output is written to your artifact store, versioned, and attached to the run. Name it with
Annotated[str, "draft"]and it becomes addressable across runs. - Cross-run load:
get_artifact_version(...).load()on the ZenML client pulls any earlier run’s output into a notebook or a later pipeline. - Serving:
zenml pipeline deployruns the pipeline as a long-running HTTP service, and every request becomes a run with the same artifacts and lineage as a batch job. Self-host the server with Helm, or use ZenML Pro for the managed control plane; the artifacts stay in your bucket either way.
Where the human comes in
An agent that drafts something is easy. An agent 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 agent_pipeline(topic: str):
draft = run_agent(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 draft still in the artifact store. The schema can be a Pydantic model, so the approver can hand back edits, not just a yes. on_pause and on_resume hooks on the pipeline are where the Slack message goes.
wait() only works in the pipeline body of a dynamic pipeline, not inside a step. The SDK’s own interruption for tool approval lives inside the run and hands you the state to persist yourself. Different scopes, and they compose.
What ZenML doesn’t do is trace the model and tool calls inside Runner.run(). Want to record those runs and replay them against a prompt or model change? That’s Kitaru, which has a first-class OpenAI Agents SDK adapter: see Kitaru vs OpenAI Agents SDK.
What makes ZenML different
| Feature | ZenML | OpenAI Agents SDK | What that means |
|---|---|---|---|
| Versioned artifacts and lineage across runs | Yes | Not supported | Step outputs are stored and loadable by name; the SDK traces a run. |
| Caching that skips unchanged steps on a re-run | Yes | Not supported | Keyed on the step, its parameters and its inputs. |
| Pause the run for human approval, resume later | Yes | Partial support | `wait()` in a dynamic pipeline pauses the whole run. The SDK can interrupt a run for tool approval, but you own the storage and the process between turns. |
| Isolated code execution for tool loops | Yes | Partial support | ZenML's sandbox is a stack component with local, Docker, Kubernetes and Modal flavors. The SDK's hosted code interpreter runs on OpenAI's side, and its beta sandbox agents give an agent a container of its own. |
| One stack abstraction for Kubernetes, Vertex AI, SageMaker, AzureML and more | Yes | Not supported | Configure once, every pipeline uses it. |
| Serve it and get lineage per request | Yes | Not supported | `zenml pipeline deploy` makes every request a tracked run. |
| Works with any agent framework, not just one | Yes | Not supported | A step is ordinary Python. |
| Agents, tools, and handoffs between specialised agents | Not supported | Yes | The SDK's whole point. ZenML doesn't define agents, it runs them. |
| Input and output guardrails | Not supported | Yes | No ZenML equivalent. Keep them inside the step. |
| Sessions carrying conversation history | Not supported | Yes | ZenML runs aren't conversational. |
| Tracing of model and tool calls inside the run | Not supported | Yes | ZenML records the step, its metadata and its artifacts, not the calls within it. |
How the two surfaces map
| Concept | OpenAI Agents SDK | ZenML |
|---|---|---|
| Unit of work | Runner.run() | @step (the agent runs inside it) |
| Boundary | An agent and its handoffs | @pipeline |
| Control flow | Handoffs between agents | The pipeline graph, or Python control flow with dynamic=True |
| Validation | Input and output guardrails | Artifact types on the step signature |
| Human approval | Tool approval inside the run, your own persistence | wait() in a dynamic pipeline, resolved from CLI, dashboard or API |
| Code execution | Hosted code interpreter, sandbox agents (beta) | Sandbox stack component (local, Docker, Kubernetes, Modal) |
| Outputs | RunResult in memory | Versioned artifacts in your bucket |
| Avoiding repeat work | Not applicable | enable_cache across runs, zenml pipeline runs retry after a failed dynamic run |
| Observability | Traces of a single run | Runs, steps, and artifacts across runs |
| Where it runs | Your process | Stack orchestrator (zenml stack set) |
| Serving | Your own wrapper | zenml pipeline deploy --name ... |
Code comparison
from typing import Annotated
from agents import Agent, Runner
from zenml import pipeline, step, wait
from zenml.client import Client
researcher = Agent(name="Researcher", instructions="Research the topic.")
writer = Agent(name="Writer", instructions="Write a brief.")
@step
def gather_context(topic: str) -> Annotated[str, "context"]:
return retrieve_documents(topic)
@step(enable_cache=False)
def run_agent(context: str, topic: str) -> Annotated[str, "draft"]:
result = Runner.run_sync(
researcher, f"{context}\n\nTopic: {topic}"
)
return str(Runner.run_sync(writer, result.final_output).final_output)
@step
def publish(draft: str) -> None:
post_to_cms(draft)
@pipeline(dynamic=True)
def agent_pipeline(topic: str):
draft = run_agent(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)
agent_pipeline("AI orchestration")
# Edit publish() and re-run: gather_context is cached.
Client().get_artifact_version("draft").load()
# Same code on your cloud:
# zenml stack set vertex_stack && python agent_pipeline.pyfrom agents import Agent, Runner
researcher = Agent(name="Researcher", instructions="Research the topic.")
writer = Agent(name="Writer", instructions="Write a brief.")
topic = "AI orchestration"
context = retrieve_documents(topic)
result = Runner.run_sync(researcher, f"{context}\n\nTopic: {topic}")
draft = str(Runner.run_sync(writer, result.final_output).final_output)
if input("Publish this brief? ") == "y":
post_to_cms(draft)
# Tracing shows which agent ran, which tools fired, and where
# the handoff happened. What there isn't: 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 Vertex AI
# without writing the deployment yourself.Put a pipeline around
your OpenAI agents
Keep the OpenAI Agents SDK for building the agent: the handoffs, the guardrails, the tools. Add ZenML when the agent 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.









