PATTERN Cited by 1 source
Pluggable Zod tool definition¶
Pattern¶
Extend an agent's tool surface by adding a single short JS / TS config block that pairs (a) Zod-typed input schema, (b) a plain-English description for the model, (c) a runtime function body, and (d) bound platform resources. No additional infrastructure, no new service, no SDK code-gen — the agent gets a new capability in one file.
The Zod schema does triple duty: it (i) generates the JSON
Schema the model uses to know how to call the tool, (ii) provides
runtime input validation at the proxy / runtime boundary, and
(iii) carries .describe() annotations the model uses to
disambiguate parameters.
Canonical disclosure¶
The pattern is concretely demonstrated in Cloudflare's Claude Managed Agents launch post (2026-05-19), quoted verbatim:
"For example, you could add a custom tool to host a public file on Cloudflare's R2 object storage. Just add the relevant binding in wrangler config, write a zod definition, and short function in
custom-tools.js."
defineTool({
name: "r2_host_file",
description: "Upload from sandbox to R2 and get a public URL.",
inputSchema: z.object({
key: z.string().describe("Object key"),
content: z.string().describe("UTF-8 file body"),
contentType: z.string().describe("MIME type"),
}),
run: async ({ key, content, contentType }, { env }) => {
await env.PUBLIC_BUCKET.put(
key, content, { httpMetadata: { contentType }}
);
return `${env.PUB_R2_URL.replace(/\/$/, "")}/${encodeURI(key)}`;
}
}),
(Source: sources/2026-05-19-cloudflare-announcing-claude-managed-agents-on-cloudflare.)
Anatomy¶
| Field | What it does | Who it's for |
|---|---|---|
name |
Stable string identifier | Logging / model invocation |
description |
One-sentence summary of the capability | The model — picked by Claude during tool-use selection |
inputSchema |
Zod object schema with .describe() per field |
(1) Model — generates the JSON Schema for valid calls; (2) Runtime — validates input at the boundary |
run |
Async function executing the tool | The runtime — receives validated input + the platform binding env |
env |
Cloudflare bindings (R2 buckets, KV, D1, secrets) | The operator — wires the tool to platform resources without leaving the file |
Why Zod, not OpenAPI / JSON Schema directly¶
The pattern is structurally different from OpenAPI schema as agent tool contract (Cloudflare KYC ingest, 2026-04-23) in deployment shape:
- OpenAPI route — the contract lives in a separate spec file, the runtime is a separate service, the agent calls the tool over HTTP. This is right when the tool is a service with its own lifecycle (other clients, separate deploys).
defineTool+ Zod route — the contract, the runtime, and the binding all live in one file co-deployed with the agent control plane. This is right when the tool exists only to serve the agent — the marginal cost of adding a capability is one config block.
For Cloudflare-Claude-Managed-Agents, the forking-the-template ergonomics drive the choice: operators are expected to extend the integration with one-off business capabilities (e.g. "upload to my R2 bucket and return a public URL") where building a separate service would be friction with no upside.
Composition with adjacent patterns¶
- Composes with concepts/agent-brain-hands-decoupling —
the brain (Claude) sees the tool's
name+description+ Zod-derived JSON Schema; the hands (Cloudflare) execute therunfunction with bound resources. The contract between brain and hands is the JSON Schema generated from Zod. - Generalises the existing
Agents SDK
tool()helper — the Agents SDK's existingtool()helper (used in Agent Lee etc.) takes the same Zod-schema-plus-handler shape;defineToolin the Claude integration is the same shape pinned at the Claude-managed-agents harness boundary. - Sibling to patterns/specialized-agent-decomposition — decomposing into specialised sub-agents is the architectural move at the agent-loop altitude; pluggable Zod tool definition is the architectural move at the per-tool- capability altitude. They compose: each specialised agent has its own tool surface defined this way.
Trade-offs¶
- + Adding a capability is a one-file change — no new service, no separate deploy, no SDK regen.
- + Schema validation is automatic — Zod runs at the
runtime boundary; invalid model output (parameter type
errors, missing fields) is rejected before
runexecutes. - + Resource bindings are first-class — Cloudflare's
envpattern means the tool gets typed access to R2 / KV / D1 / secrets without manual wiring. - + Per-field
.describe()improves model selection — the model picks the right tool more reliably when each parameter has prose context. - − JS / TS only at launch — the post's example is JS; language alternatives are not enumerated. Operators wanting Rust or Python tools have to build their own equivalent surface.
- − Tool versioning is implicit — the file is the source of truth; there's no built-in deprecation flow if a tool's schema changes mid-session.
Seen in¶
- sources/2026-05-19-cloudflare-announcing-claude-managed-agents-on-cloudflare
— canonical wiki entry. The
r2_host_fileexample is the literal demonstration; the broader integration ships a tool surface (browser_search/browser_execute/screenshot/browse/fetch_to_markdown/web_fetch/send_email/email_read/email_list/call_service/image_generate) presumably built on the same primitive.
Related¶
- Concept: concepts/agent-brain-hands-decoupling / concepts/schema-constrained-llm-output
- Sibling patterns: patterns/openapi-schema-as-agent-tool-contract / patterns/specialized-agent-decomposition
- Substrate: systems/cloudflare-workers / systems/cloudflare-agents-sdk / systems/claude-managed-agents / systems/cloudflare-r2
- Adjacent: systems/model-context-protocol (MCP is the cross-vendor sibling shape — same schema-plus-handler contract, different deploy boundary)
- Companies: companies/cloudflare