Custom tools: one file, typed, gated
Cloudflare's control plane keeps every custom tool in a single file —
src/tools/custom-tools.ts— so adding one is a typed code change, not new infrastructure.
The shape
defineTool({
name: "lookup_user",
description: "Look up a user profile from the internal users service.",
inputSchema: z.object({
userId: z.string().describe("Stable user id, e.g. usr_abc123"),
}),
requires: (env) => Boolean(env.USERS),
run: async ({ userId }, { env }) => {
const r = await env.USERS.fetch(`http://users.local/v1/${userId}`);
return r.ok ? await r.text() : `error: ${r.status}`;
},
});
Five fields, no ceremony: a unique name, a description the model reads to decide when to reach for it, a Zod inputSchema (must resolve to an object — Anthropic requires tool inputs to be JSON objects), an optional requires predicate that hides the tool when its binding isn't configured, and run, the implementation, with full access to the Worker's env.
Why gate on requires
A tool whose binding isn't wired up on this deployment simply doesn't register — it renders disabled with a "binding not configured" hint on the dashboard instead of failing at call time. That's what lets one custom-tools.ts file describe every tool a fork might have, while each deployment only ever offers the ones it actually configured.
What ships out of the box
| Family | Tools | Needs |
|---|---|---|
| Workspace (Isolate only) | cf_read, cf_write, cf_edit, list, find, cf_grep, delete |
always on |
| Code execution (Isolate only) | execute, run_file |
LOADER (Worker Loader) |
| Browser Rendering | cf_web_fetch, fetch_to_markdown, browse, screenshot, browser_search, browser_execute |
BROWSER (+ LOADER for the last two) |
| Workers AI | image_generate |
AI |
| Workers VPC | call_service |
vpc_services bindings |
| Email Routing | email_send, email_inbox, email_read |
SEND_EMAIL |
Drift detection
A long-lived Isolate dispatcher can outlast a deploy. The control plane compares the running dispatcher's tool names against the current build on every event; a mismatch aborts the old dispatcher and starts a fresh one with the updated set. A new or renamed tool takes effect on the next Anthropic event for that session, not the next deploy.
Related
- Overview
- Sandboxes
- Open models — the same typed-tool shape, without a paid Anthropic dependency
a247d35ff30c6cac · verify