Content

Connecting Notion to an AI Agent

A Notion database row is a page, and that one fact breaks most agent integrations. Here is how blocks, pages and rows actually work, and how to wire Notion up safely.

Chaitrali Kakde

DevRel Engineer

IntegrationsOct 23, 2026

Here is the bug you are going to hit. You tell your agent to "update the Notion row with the summary." It calls the API, gets a 200 back, and reports success. You open Notion and the Summary column is empty. Somewhere else, buried inside the row, there is a new paragraph of text nobody asked for.

Nothing failed. The agent just did a different thing than you meant, because in Notion a database row is a page, and a page has two completely separate places to put text.

The one thing to get straight: properties vs blocks

Every row in a Notion database is a page object. That page has:

  • Properties. The columns you see in the table view. Status, Owner, Due date, your Summary field. You change these by updating the page object.
  • Blocks. The document body you see when you click into the row and it opens as a page. Paragraphs, headings, to-do items, toggles. You change these by appending child blocks.

Two different operations, two different endpoints. "Set the Status to Done" is a property update. "Write the meeting notes into the doc" is a block append. An agent handed a vague instruction will happily pick the wrong one, and because both succeed, nothing tells you.

So when you give an agent Notion access, name the operation in the instruction, and more importantly, only allow the operations you actually want. That second part is what the rest of this post is about.

Notion API limits worth knowing before you build

These are the numbers that will bite an agent specifically, because agents loop and batch in ways a human clicking around never would. All current as of this writing, but check Notion's own docs since they do change.

  • 100 blocks per append request, with at most two levels of nesting in one call. A longer array errors out. If your agent is writing a long document, it has to chunk.
  • 2,000 characters per rich text content field. Models write long paragraphs. This is the single most common validation error we see. Oversized payloads come back as a 400 with validation_error.
  • 1,000 block elements and 500 KB per whole payload, whichever you hit first.
  • Roughly 180 requests per minute per connection on most plans, around 600 on Business and Enterprise, and a tighter limit of about 3 requests per second for querying a data source. Rate-limited calls return 429 with rate_limited and a Retry-After header you should actually respect.
  • Permissions are per page, not per workspace. If the page was never shared with your integration you get a 404, not a 403, which reads like "this page does not exist" and sends everyone debugging in the wrong direction. If the connection lacks insert content capability you get a 403.

One more: the after parameter for positioning a new block is deprecated. Use position instead, which takes start, end (the default) or after_block. You cannot send both. Also note that appending cannot move existing blocks, only create new ones.

Wiring it up: four commands

Install, create a project, pull the Notion integration, and connect your account. There is no .env file anywhere in this, which is the point. Your token lives in Swytchcode's own local store, outside the project folder, and gets injected when a call runs.

setup.sh
npx swytchcode
swy login
swy init --mode=sandbox        # sandbox first, always

swy get notion                 # pull the integration
swy auth connect notion        # opens a browser for OAuth
swy auth status                # confirm it is connected

Now find the methods you need. Do not guess IDs, and do not let the model guess them either. Search, then look, then allow:

discover.sh
# search by plain English, scoped to the provider
swy discover "append blocks to a page" --project notion
swy discover "update page properties" --project notion

# or just list everything Notion exposes
swy list methods notion

# inspect one before you allow it
swy info <canonical_id>

# allow exactly the ones you want, one at a time
swy add method <canonical_id>

Two things about discover worth knowing. Always pass --project, because semantic search runs across the whole catalog and will cheerfully return some other product's Notion-shaped endpoint. And run swy get before swy info, or info will report a method that genuinely exists as missing.

The allowlist is the real security control here. Only methods you added can run. If you never add the delete-block method, there is no prompt, no jailbreak and no confused tool call that can delete a block, because the capability is simply absent. That is different from telling the model not to delete things and hoping.

Then run one call to prove it works:

first-call.sh
swy exec <canonical_id> --input '{"params":{"block_id":"<page-or-block-id>"}}'

# see what actually happened
swy audit network

Using it from Claude Code or Cursor over MCP

If you want Notion available inside Claude Code, Cursor or Windsurf rather than in your own code, point the MCP client at Swytchcode's MCP server. Same kernel, same allowlist, same policies, so an agent in your editor cannot do anything your CLI setup did not permit. The setup per client is in the MCP server setup guide and the reference is in the MCP docs.

If you are writing the agent yourself, there is also a skills file at swytchcode.com/skills.md you can hand to a coding agent so it knows the commands and conventions without you explaining them every time.

Policies: the rules that stop the bad write

An allowlist decides which methods exist. A policy decides whether a particular call is allowed to go through, by reading its arguments. For Notion, the rules worth having on day one are about blast radius: writing to a scratch page is fine, writing to the company handbook is not.

policies.json
{
  "id": "protect-handbook",
  "target": ["<notion append-blocks canonical_id>"],
  "when": { "field": "block_id", "operator": "==", "value": "<handbook-page-id>" },
  "action": {
    "type": "REQUIRES_APPROVAL",
    "message": "Agent wants to write to the handbook page"
  },
  "approval_timeout": "2h"
}

Swap in the real canonical ID from swy list methods notion. Held calls go to Slack or Telegram and fail closed: if nobody approves within the window, the call never runs. It does not quietly go through because a timer expired. Full options in the policies docs and a walkthrough in how to set up policy guardrails. Because the file lives in your repo, a policy change is a pull request with a reviewer, which is also what makes it policy-as-code rather than a setting someone changed last March.

Retries and the duplicate-paragraph problem

Appending a block is not idempotent. If the request lands, Notion creates the block, and then the response gets lost on the way back, your agent thinks it failed. Retry it and you now have the same paragraph twice. This is the Notion version of the double-charge problem, and it looks silly rather than expensive, which is why nobody plans for it until a customer-facing page has duplicated sections in it.

Swytchcode's rule here is deliberately conservative: writes are retried automatically only when an idempotency key is set on the call. No key, no automatic retry of a write, so the default outcome is a visible failure rather than a silent duplicate. Rate-limit errors get backoff, which matters for Notion's 429s specifically. The mechanics are in the retries guide and the idempotency guide, and the general version of the argument is in API idempotency: the complete guide.

A practical trick for Notion: before appending, read the page's existing children and check whether a block with your exact content is already there. It costs one extra read and removes the whole class of duplicate.

What you can actually build

Four things that work well, roughly in order of how safe they are to ship:

  1. A read-only digest agent. Query a projects database every Monday, summarise what moved, post it to Slack. Zero write access to Notion at all, so the worst case is a wrong summary rather than a damaged workspace.
  2. Meeting notes into tasks. Take a transcript, create one row per action item in a tasks database with owner and due date as properties. Allowlist exactly two methods: create a page in that database, and query it. Nothing else.
  3. A support ticket to spec pipeline. When a ticket gets tagged a certain way, draft a page in a product database with the customer context attached. Pairs nicely with an Intercom or FreshDesk agent on the other end.
  4. Doc drafting with an approval gate. The agent writes a full PRD into a page, but the append to anything outside a designated drafts area requires approval. This is the one where policies earn their keep.

Things we got wrong

We assumed a 404 meant a bad page ID. It usually means the page was never shared with the integration. Check sharing before you check your IDs.

We let a model write a 3,000 character paragraph into one rich text field and spent an hour reading a validation error that was, in hindsight, telling us exactly that. Chunk long text into multiple blocks at the 2,000 character boundary.

And we pinned nothing. Notion takes a version header, and not pinning it means your integration's behaviour can shift when the default moves. Pin it, then upgrade deliberately.

FAQ

Can the agent create a whole page with content in one call?

Partly. You can create a page with properties and some initial children in one go, but you are still bound by the per-request block count and nesting limits, so anything long becomes a create followed by one or more appends. Plan for the multi-call version from the start.

How do I stop the agent touching the wrong database?

Two layers. Share only the pages and databases it needs with the integration, since Notion permissions are per page. Then add a policy that checks the parent ID on write calls, so even a correct method aimed at the wrong target gets held or denied.

Does this work with a self-hosted or private Notion-like tool?

If it has an OpenAPI spec or a Postman collection, yes, you can add it as a custom integration and it gets the same allowlist, policy, retry and audit treatment as a catalog API. That is usually the answer for internal wikis too.

Where do I see what the agent did?

swy audit network for calls that ran and swy audit policy for calls that were blocked. The second one is the one people forget to look at, and it is usually where the interesting stuff is. More on what to capture in how to log every API call an AI agent makes.

More content