Skip to content

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 the run function 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 existing tool() helper (used in Agent Lee etc.) takes the same Zod-schema-plus-handler shape; defineTool in 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 run executes.
  • + Resource bindings are first-class — Cloudflare's env pattern 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-cloudflarecanonical wiki entry. The r2_host_file example 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.
Last updated · 542 distilled / 1,571 read