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

FieldTypeDefaultDescription
codestringrequiredESM module string — must use the handler format above
runtimestringrequired"javascript" or "typescript"
allowed_domainsstring[]requiredDomains the code can fetch from. Use [] if no HTTP calls needed
timeout_msnumber30000Execution timeout in milliseconds (max 300000)
memory_limit_mbnumber128Memory limit in MB (max 128)
connection_idsstring[]Connection IDs to inject as environment variables
db_namestringNamed 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:

VariableDescription
KYEW_CONN_{NAME}_TOKENRaw token or API key
KYEW_CONN_{NAME}_BASE_URLBase URL from the connection config
KYEW_CONN_{NAME}_AUTH_HEADERPre-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.

Previous
Overview