Custom Tools
Code Tools
Code tools run custom JavaScript in secure, VM-isolated Cloudflare containers. They handle anything that needs real logic — calculations, data processing, API integrations, scheduled reports, and more.
You don't write this code
Your AI generates the JavaScript. You just describe what you want: "build a tool that calculates statistics for any list of numbers."
How Code Tools Work
Code tools run in Cloudflare Sandbox containers with:
- Node.js 20 runtime (ESM only)
- VM-level isolation — no access to your Kyew data or other users
- Per-user SQLite — 10 GB persistent database via
db.mjs - Connection injection — OAuth tokens and API keys as environment variables
- Configurable limits — timeout up to 5 minutes, 128 MB memory
Handler Format
Every code tool must export a default object with a fetch method. This is the only format that works.
export default {
async fetch(request) {
const input = await request.json();
// your logic here
return Response.json(result);
}
};
Common mistake
export default async function(input) { ... } will NOT work. You'll get "handler.fetch is not a function". Always use the export default { async fetch(request) {...} } format.
Creating a Code Tool
Code tools are active immediately when created. Your AI will ask you to configure features like visibility, scheduling, and email integration.
tool(action="create",
name="my_tool",
description="What this tool does",
tool_type="code",
input_schema={
type: "object",
properties: {
numbers: { type: "array", items: { type: "number" } }
},
required: ["numbers"]
},
code_config={
code: `
export default {
async fetch(request) {
const { numbers } = await request.json();
const sum = numbers.reduce((a, b) => a + b, 0);
return Response.json({ sum, mean: sum / numbers.length });
}
}`,
runtime: "javascript",
allowed_domains: []
})
Test Anytime
tool(action="test", tool_id="tool_abc123", test_input={ numbers: [1, 2, 3, 4, 5] })
code_config Fields
| Field | Type | Default | Description |
|---|---|---|---|
code | string | required | ESM module string — must use the handler format above |
runtime | string | required | "javascript" or "typescript" |
allowed_domains | string[] | required | Domains the code can fetch from. Use [] if no HTTP calls needed |
timeout_ms | number | 30000 | Execution timeout in milliseconds (max 300000) |
memory_limit_mb | number | 128 | Memory limit in MB (max 128) |
connection_ids | string[] | — | Connection IDs to inject as environment variables |
db_name | string | — | Named database for data isolation (1-64 chars). Tools sharing the same db_name share the same database |
Per-User SQLite
Every code tool gets access to a per-user SQLite database (10 GB) through the db.mjs helper. No setup required — just import and use.
import { query, exec, batch } from "./db.mjs";
query(sql, ...params)
Runs a SQL query. Parameters are spread, not passed as an array:
// Correct — spread params
const result = await query(
"SELECT * FROM users WHERE age > ? AND city = ?",
25, "NYC"
);
// Wrong — do NOT wrap params in an array
// query("SELECT * FROM users WHERE age > ? AND city = ?", [25, "NYC"])
Return Format
query returns an object with a rows array, not a plain array:
const result = await query("SELECT * FROM users");
// Correct
const users = result.rows;
const first = result.rows[0];
// Wrong — this is undefined
// const first = result[0];
The full return shape:
{
rows: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }],
meta: { columns: ["id", "name"], rowsRead: 2, rowsWritten: 0, rowCount: 2 }
}
exec(sql, ...params)
Same as query but for write operations. Uses spread params:
await exec(
"INSERT INTO users (name, email) VALUES (?, ?)",
"Alice", "[email protected]"
);
batch(statements)
Runs multiple statements in a transaction. Here, params is an array:
await batch([
{ sql: "INSERT INTO users (name) VALUES (?)", params: ["Alice"] },
{ sql: "INSERT INTO users (name) VALUES (?)", params: ["Bob"] }
]);
For more patterns and recipes, see the Database Guide.
Connection Access
Add connection_ids to your code_config to inject credentials as environment variables. OAuth tokens are auto-refreshed on every execution.
tool(action="create",
name="github_stars",
description="Get star count for a GitHub repo",
tool_type="code",
code_config={
code: `
export default {
async fetch(request) {
const { owner, repo } = await request.json();
const res = await fetch(
\`https://api.github.com/repos/\${owner}/\${repo}\`,
{ headers: {
"Authorization": process.env.KYEW_CONN_GITHUB_AUTH_HEADER,
"User-Agent": "kyew-tool"
}}
);
const data = await res.json();
return Response.json({ stars: data.stargazers_count });
}
}`,
runtime: "javascript",
allowed_domains: ["api.github.com"],
connection_ids: ["my-github-connection"]
})
Injected Environment Variables
For each connection, three environment variables are set:
| Variable | Description |
|---|---|
KYEW_CONN_{NAME}_TOKEN | Raw token or API key |
KYEW_CONN_{NAME}_BASE_URL | Base URL from the connection config |
KYEW_CONN_{NAME}_AUTH_HEADER | Pre-formatted auth header value (e.g., Bearer xxx) |
The connection name is uppercased with non-alphanumeric characters replaced by underscores. A connection named my-github becomes KYEW_CONN_MY_GITHUB_TOKEN, etc.
For more on setting up connections, see Connections.
Scheduling
Code tools can run automatically on a schedule with optional email summaries. See Scheduling for the full guide.
Common Pitfalls
1. Wrong export format
Symptom: "handler.fetch is not a function"
// Wrong
export default async function(input) { return { result: 42 }; }
// Correct
export default {
async fetch(request) {
const input = await request.json();
return Response.json({ result: 42 });
}
};
2. Wrapping query params in an array
Symptom: Wrong or missing query bindings
// Wrong — params wrapped in array
await query("SELECT * FROM t WHERE a = ? AND b = ?", [val1, val2]);
// Correct — spread params
await query("SELECT * FROM t WHERE a = ? AND b = ?", val1, val2);
3. Treating query result as an array
Symptom: undefined when accessing results
// Wrong — result is not an array
const first = (await query("SELECT * FROM t"))[0];
// Correct — access .rows
const first = (await query("SELECT * FROM t")).rows[0];
4. Missing allowed_domains
Symptom: Validation error on create
// Wrong — field omitted
code_config={ code: "...", runtime: "javascript" }
// Correct — provide empty array if no HTTP calls
code_config={ code: "...", runtime: "javascript", allowed_domains: [] }
5. eval() or new Function()
Symptom: Blocked by sandbox security
Dynamic code generation is not allowed in the sandbox. Restructure your logic to avoid eval() and new Function().
Example: Statistics Calculator with History
A tool that calculates statistics and stores results in the per-user database:
tool(action="create",
name="statistics",
description="Calculate statistics for numbers and save to history",
tool_type="code",
input_schema={
type: "object",
properties: {
numbers: { type: "array", items: { type: "number" }, description: "Numbers to analyze" },
label: { type: "string", description: "Optional label for this calculation" }
},
required: ["numbers"]
},
code_config={
code: `
import { query, exec } from "./db.mjs";
export default {
async fetch(request) {
const { numbers, label } = await request.json();
// Ensure history table exists
await exec(\`
CREATE TABLE IF NOT EXISTS stat_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT,
count INTEGER,
mean REAL,
median REAL,
std_dev REAL,
created_at TEXT DEFAULT (datetime('now'))
)
\`);
// Calculate
const sorted = [...numbers].sort((a, b) => a - b);
const count = numbers.length;
const sum = numbers.reduce((a, b) => a + b, 0);
const mean = sum / count;
const median = count % 2 === 0
? (sorted[count / 2 - 1] + sorted[count / 2]) / 2
: sorted[Math.floor(count / 2)];
const variance = numbers.reduce((acc, n) => acc + (n - mean) ** 2, 0) / count;
const stdDev = Math.sqrt(variance);
// Save to history
await exec(
"INSERT INTO stat_history (label, count, mean, median, std_dev) VALUES (?, ?, ?, ?, ?)",
label || "unlabeled", count, mean, median, stdDev
);
// Get recent history
const history = await query(
"SELECT label, count, mean, median, std_dev, created_at FROM stat_history ORDER BY created_at DESC LIMIT 5"
);
return Response.json({
stats: { count, sum, mean, median, min: sorted[0], max: sorted[count - 1], standardDeviation: stdDev },
recentHistory: history.rows
});
}
}`,
runtime: "javascript",
allowed_domains: []
})
Security
What Code Tools Can Do
- Process input data and perform calculations
- Fetch from domains listed in
allowed_domains - Use standard JavaScript/Node.js 20 APIs
- Read and write to the per-user SQLite database
- Access injected connection credentials via environment variables
What Code Tools Cannot Do
- Access the Kyew worker environment or other users' data
- Make requests to domains not in
allowed_domains - Use
eval(),new Function(), or other dynamic code generation - Run past the configured timeout
- Exceed the memory limit
API Reference
For the full technical reference including all parameters and options, see tool Tool Reference.