
Somebody on the team wires a Claude agent to Notion using an internal integration token pasted into .env. It searches the wiki. Ask it where the incident runbook lives, and it finds the page and reads it back. Reasonable enough: the token belongs to whoever created the integration, so every search runs with that person's own workspace access.
Then somebody asks it to create a task. Then a second teammate starts using it.
Two failures now, precisely. Tasks the agent creates show up owned by nobody real, because the token has no identity of its own to assign work to. And the second teammate's searches return pages the first teammate shared with the integration, not the pages the second teammate can actually see in their own Notion account. Nobody made a bad decision here. This is what happens when a knowledge tool and a task tool get bolted onto the same static token instead of a real per-user connection.
Most "Notion agent" tutorials pick one job: search the wiki, or manage a task board. Real usage doesn't split that cleanly. The same person who asks "what's our refund policy" also wants "mark the API redesign task as in review and assign it to Maya" answered in the same conversation, against the same workspace, under the same permissions.
That's two Notion capabilities riding on one connected account: retrieval (search, read pages, query databases) and action (create pages, insert database rows, update properties, append content). Scalekit's Notion connector exposes both from the same OAuth connection, so the agent doesn't need two separate integrations or two separate tokens to do both jobs.
Recommended Reading: Notion MCP vs Notion API for AI Agents covers why background agents need the direct API path this post uses, rather than Notion's hosted MCP server, which requires a browser-based OAuth flow and can't run headlessly.
Your code never talks to Notion directly. It talks to Scalekit. Scalekit handles the Notion OAuth flow, token storage, and refresh on behalf of each user, and returns only the tools that user's connected account is authorized to call.
The flow is the same as every other Scalekit-backed Claude agent: a user connects their Notion account once, your agent calls list_scoped_tools to get that user's authorized tool surface in Anthropic's native format, and then Claude runs its normal tool-use loop, deciding what to call while your code executes each call through execute_tool with the user's identifier attached. Notion never sees your code. Your code never sees a Notion access token.
The complete agent. Copy it, set your environment variables, and run it.
Environment variables:
Expected output:
(Exact tool count and output will vary depending on which capabilities are enabled on your Notion connection. Run it against your own workspace to see your data.)
These workflows show the range of tasks the agent handles once a Notion account is connected. Each starts with a natural language prompt and ends with a concrete Notion action, either a retrieval or a write.
Knowledge retrieval
"Where's our incident escalation process?"
The agent calls notion_page_search to find the matching page, then notion_page_content_get to pull its blocks and answer from the actual content, not just the title.
"Find the Q3 planning doc and tell me what's still undecided"
The agent calls notion_data_fetch against the workspace search endpoint, then notion_page_get and notion_page_content_get to read the page before summarizing.
Task tracking
"What's assigned to me and still open?"
The agent calls notion_database_query (or notion_data_source_query if the database has multiple data sources) with a filter on the Assignee and Status properties.
"Add a task 'Draft launch email' due Friday to the Marketing database"
The agent calls notion_database_insert_row with the title and due date as property values.
"Move the API redesign task to In Review and reassign it to Maya"
The agent calls notion_page_update, passing a properties object that sets the Status and Assignee fields on that page.
"Drop a note on that task with the three blockers"
The agent calls notion_page_content_append to add the note as page content, or notion_comment_create if it belongs as a discussion comment instead.
Both at once
"Find our onboarding checklist and create a task in the Onboarding Tasks database for each unchecked item"
This is the default prompt in the agent script above, and it's the workflow that justifies merging the two roles into one agent. The agent calls notion_page_search, reads the checklist with notion_page_content_get, then calls notion_database_insert_row once per unchecked item. One connected account, one conversation, both capabilities.
Full parameter list in the Notion connector docs.
Notion's API has more sharp edges than most connectors in this series. These are the ones that actually bite when Claude is the one constructing the parameters.
notion_database_insert_row and notion_page_update both take a properties object keyed by the exact property name in the target database, case-sensitive. A database column labeled "Status" will not accept a status key. The title property is the one exception that trips up even careful implementations: regardless of what the column is displayed as in the Notion UI, the property key in the API payload must literally be title. Call notion_database_fetch first to read the real schema before writing, rather than guessing property names from what's visible on screen.
Notion's 2025-09-03 API introduced data sources to support merged and synced databases, and the older notion_database_query and notion_database_insert_row tools use the pre-2025 endpoints that don't understand them. If a query against a multi-source database returns an "Invalid request URL" error, that's the signal: call notion_data_source_fetch with the database_id to get the data_source_id, then use notion_data_source_query or notion_data_source_insert_row instead. Standard single-source databases work fine with the classic tools. You only hit this if the database has been merged or synced.
notion_page_content_append takes a simplified format: a list of objects with type and text fields, which the tool converts into Notion's block structure internally. notion_block_update, by contrast, edits a specific existing block by ID and expects the block type to match what's already there. Neither tool accepts raw Notion API block objects with rich_text arrays. Passing the wrong shape to either tool is a common first-run failure.
Notion's OAuth consent screen makes the user pick which pages and databases the integration can access, and API calls to anything outside that selection return the same "object not found" error Notion returns for pages that genuinely don't exist. There's no separate "permission denied" response to distinguish the two cases. If the agent reports it can't find a page you know exists, the workspace owner likely needs to re-run the connection and share that page or database during the consent step.
The Notion connector has no webhook support. This agent answers when a user asks something; it does not watch the workspace and act automatically when a page changes or a database row updates. If your use case needs the agent to react the moment a task's status changes rather than waiting to be asked, that's a background job checking notion_database_query on a schedule, not a workspace event the connector can push to you.
Unlike connectors that work out of the box with Scalekit-managed OAuth, Notion requires you to register your own public integration in the Notion Developer Hub, with a redirect URI pointed at Scalekit and your own Client ID and Secret entered into the Scalekit dashboard. Your users will see your app's name, not Scalekit's, on the Notion consent screen. Plan for this during setup, not after your first user tries to connect.
The USER_IDENTIFIER environment variable, the identifier value in the code, is what makes this agent work for more than one person without changing a line of it. Swap the identifier and the exact same script operates against a completely different person's Notion workspace, seeing only the pages and databases that person shared and writing tasks under their own connected account.
In production, resolve the identifier from your authenticated session, whether that's a cookie, a JWT, or a database lookup, and never accept it from client input. What the user can't see in Notion, the agent can't see either. Scope comes from the connected account, not from anything your code decides at request time.
For the full pattern behind this, see access control for multi-tenant AI agents.
Other connectors: Slack, Monday.com, Gmail, Linear, GitHub, HubSpot, and more: full connector list
Other frameworks: LangChain, Mastra, CrewAI, Vercel AI SDK, Google ADK: framework examples