Give your Notion Custom Agent a real data tool with `worker.tool()`

Custom Agents can't see your Notion databases by default — they can only reason over text already in the conversation. `worker.tool()` registers a TypeScript function in Notion's hosted runtime that the agent can call on-demand with typed arguments. This tip walks through building a `querySprintDatabase` tool: scaffold, schema with the `j` builder, `readOnlyHint` for auto-execution, local dry-run, deploy in 30 seconds, and connect to a Custom Agent via the "+ Add connection" UI. Free through August 11, 2026; Business/Enterprise plan required.

리서치 브리프

Plan required: Notion Business or Enterprise ($20/user/month minimum). 1
Beta window: Workers are free through August 11, 2026, then join the Notion credit system at approximately $10 per 1,000 credits. 2
Custom Agents can summarize text, draft documents, and run scheduled reports — but by default they can only act on whatever text is already in the conversation. Ask one "What's the sprint completion rate?" and it will either guess or admit it can't see your database. The fix is worker.tool(): a Workers SDK method that registers a TypeScript function the agent can actually call, receive typed data from, and reason over. 3
Unlike MCP server integrations — which require external hosting and manual wiring — Worker Tools deploy directly into Notion's runtime and appear in the agent's tool list automatically after ntn workers deploy. 4 The agent reads the tool's description and schema, decides when to call it, generates typed arguments, and gets back a structured result — no extra plumbing. 3
This tip walks through building a querySprintDatabase tool that a Custom Agent can call on-demand to report sprint status.

Prerequisites

RequirementDetailsWhere to get it
Notion Business or Enterprise planntn workers deploy is gated to Business+notion.com/pricing
Notion Internal Integration Token (ntn_...)Authenticates the Worker to read the sprint databaseapp.notion.com/developers → Create integration
Integration capabilitiesRead ContentSet in the integration's Capabilities tab
Notion CLI (ntn)Scaffolds and deploys Workerscurl -fsSL https://ntn.dev | bash
Node.js 18+Required for local developmentnodejs.org or nvm install 18
Sprint database ID32-character hex from the database URLnotion.so/workspace/DATABASE_ID?v=…
Sprint database shared with the integrationIntegrations require explicit page/database accessDatabase → ··· → Connections → select integration

How worker.tool() works

worker.tool() takes three arguments: 3
  1. A stable string key (e.g., "querySprintDatabase") — the identifier the agent references internally
  2. A config object with title, description, schema (built with the j schema builder), optional outputSchema, optional hints, and an execute function
  3. The execute function receives typed, validated input and a context object that exposes an authenticated Notion SDK client via context.notion
The schema builder j is imported from @notionhq/workers/schema-builder. Every property in j.object({...}) is automatically required with additionalProperties: false. 5 Mark fields optional with .nullable(); add .describe() to every field so the agent knows what to pass. 5
One flag matters a lot for how the agent behaves: hints: { readOnlyHint: true }. Tools with this hint can be auto-executed by the agent without asking for user confirmation each time. Leave it off for any tool that writes data. 3

Step-by-step: build and connect the tool

Step 1: Scaffold the Worker

curl -fsSL https://ntn.dev | bash
ntn workers new sprint-tool
cd sprint-tool

Step 2: Write the tool (src/index.ts)

Replace the scaffolded src/index.ts with the following:
import { Worker } from "@notionhq/workers";
import { j } from "@notionhq/workers/schema-builder";

const worker = new Worker();
export default worker;

worker.tool("querySprintDatabase", {
  title: "Query Sprint Database",
  description:
    "Returns task counts, completion rate, and blocked items for a given sprint. " +
    "Call this when the user asks about sprint status, progress, or velocity.",
  schema: j.object({
    sprintName: j
      .string()
      .describe("Exact sprint name as it appears in the Notion database, e.g. 'Sprint 42'."),
    status: j
      .enum("all", "completed", "in_progress", "blocked")
      .nullable()
      .describe("Filter tasks by status. Pass null to return all statuses."),
  }),
  hints: { readOnlyHint: true },
  execute: async ({ sprintName, status }, context) => {
    const databaseId = process.env.SPRINT_DATABASE_ID!;

// Build filter: always filter by sprint name; optionally filter by status
    const andFilters: object[] = [
      {
        property: "Sprint",
        rich_text: { equals: sprintName },
      },
    ];
    if (status && status !== "all") {
      andFilters.push({
        property: "Status",
        status: { equals: statusLabel(status) },
      });
    }

const response = await context.notion.databases.query({
      database_id: databaseId,
      filter: andFilters.length === 1 ? andFilters[0] : { and: andFilters },
      page_size: 100,
    });

const tasks = response.results as any[];
    const total = tasks.length;
    const completed = tasks.filter(
      (t) => t.properties?.Status?.status?.name === "Done"
    ).length;
    const blocked = tasks.filter(
      (t) => t.properties?.Status?.status?.name === "Blocked"
    ).length;
    const inProgress = tasks.filter(
      (t) => t.properties?.Status?.status?.name === "In Progress"
    ).length;

return {
      sprint: sprintName,
      total,
      completed,
      inProgress,
      blocked,
      completionRate:
        total > 0 ? `${Math.round((completed / total) * 100)}%` : "0%",
    };
  },
});

function statusLabel(s: string): string {
  return s === "completed" ? "Done" : s === "in_progress" ? "In Progress" : "Blocked";
}
Adapt status names to your database. The "Done", "In Progress", and "Blocked" strings must match your Notion Status property options exactly. Check them in your database settings before deploying.

Step 3: Test locally before deploying

# Create a .env file with your credentials
echo "NOTION_TOKEN=ntn_..." > .env
echo "SPRINT_DATABASE_ID=<your-32-char-id>" >> .env

# Run the tool against local code without writing to Notion
ntn workers exec querySprintDatabase --local -d '{"sprintName":"Sprint 42","status":null}'
A successful local run returns a JSON object like:
{
  "sprint": "Sprint 42",
  "total": 24,
  "completed": 16,
  "inProgress": 6,
  "blocked": 2,
  "completionRate": "67%"
}
This is the payload the agent will receive and summarize. 6

Step 4: Deploy

ntn workers env set NOTION_TOKEN=ntn_...
ntn workers env set SPRINT_DATABASE_ID=<your-32-char-id>
ntn workers deploy
Deployment takes under 30 seconds. 4

Step 5: Connect the tool to a Custom Agent

After deployment, the tool appears in the Custom Agent's configuration UI. Open your Custom Agent's settings, click + Add connection, and select the deployed Worker. 3
Notion Custom Agent settings panel — blue arrow points to the '+ Add connection' button where deployed Worker tools appear
Notion Custom Agent settings panel — blue arrow points to the '+ Add connection' button where deployed Worker tools appear
Set the agent's system prompt to include something like:
"You are a sprint status assistant for this team's Notion workspace. When asked about sprint progress, call the querySprintDatabase tool with the sprint name. Summarize the returned JSON as a human-readable status update: mention completion rate, in-progress count, and any blocked items."

Step 6: Try it

In any Notion page where the Custom Agent is available, type:
"What's the status of Sprint 42?"
The agent calls querySprintDatabase, receives the typed JSON payload, and writes a structured summary — completion rate, in-progress count, blocked items — into the page. Thomas Wiegold, an independent developer who built a similar tool against Shopify, described the experience: "Question to answer, maybe four seconds." 6
Check execution logs if anything looks off:
ntn workers runs logs <runId>

Expected outcome

Type a natural-language sprint question in any Notion page. The Custom Agent determines which tool to call, generates the right arguments, retrieves live data from your database, and writes a structured summary — without you touching a filter, formula, or dashboard.

Gotchas

Description quality directly affects whether the agent calls the tool. The description field and each field's .describe() annotation are what the LLM reads to decide whether this tool is relevant. Narrow and precise beats broad. Notion's docs put it plainly: "Use .describe() on every field. Field descriptions tell the agent what each value means." 3 Thomas Wiegold put it more vividly: treat the description like "API documentation written for a literal-minded colleague who hasn't had coffee yet." 6
The schema builder marks all fields as required by default. j.object({...}) sets additionalProperties: false and requires every listed property. 5 Use .nullable() (not field omission) when a field is optional — otherwise the agent will always pass it, even if it shouldn't.
page_size: 100 is the API maximum per query. Sprints with more than 100 tasks need a pagination loop using response.has_more and response.next_cursor. The code above works for most sprint sizes but will silently truncate at 100 tasks without the loop.
Multiple tools can coexist in one Worker. Add a getRoadmapItems or getTeamCapacity tool in the same src/index.ts file. The agent will orchestrate which tool to call based on the user's question — no additional wiring needed. 4
Cost cliff in August. Workers are free through August 11, 2026. 2 After that, tool invocations join the Notion credit system. Notion has confirmed per-call cost will be "a fraction of agent token cost" but has not published the exact conversion rate. Wiegold estimates $5–$15/month for a small business use case, but notes this is speculative — not an official figure. 6 Log your invocations during the free beta to project your August bill.
Operational limits are not yet published. Concurrent execution limits, CPU time caps, and memory limits for Workers remain unspecified in Notion's public documentation as of the beta. 7 For production workloads, treat the current beta as a prototyping environment until Notion publishes SLAs.

이 콘텐츠를 둘러싼 관점이나 맥락을 계속 보강해 보세요.

  • 로그인하면 댓글을 작성할 수 있습니다.