Notion Action
Create and update Notion pages, read their content as Markdown, and query rows out of a database — as a step in a pipeline. Twelve operations against Notion's REST API, with the page body handled as plain Markdown instead of a hand-built block tree. And the other way round: a Notion trigger fires a flow when a page is added or edited.
Overview
The Notion Action node runs one operation per event. It appears on the canvas in a warm grey card — Notion's own brand is monochrome.
A new Notion node starts paused: it creates, edits and archives pages in a real Notion, so it does not run until you turn it on — Activate on its page or on the canvas. Duplicating an active node keeps it active.
··· › Connections), every picker in this node comes back empty and every call 404s. It is the number one cause of "I set it up and nothing happens".Credential
Two ways in, and they coexist. Both are created either inline on the node or in Settings › Credentials.
Connect Notion (recommended)
Click Connect Notion, authorize, and pick which pages and databases the connection can reach — right there on Notion's consent screen. Nothing to copy, and no integration for the user to create. Stored as notion_oauth.
Your own integration
Prefer to run against a single workspace with your own app? Create an internal integration at notion.so/profile/integrations and paste its secret — it starts with ntn_. Stored as notion_internal. It works the same minute you create it and needs nothing published on our side. To use Add comment, also tick Read comments and Insert comments under its Capabilities.
Operations
Twelve, grouped by the Notion object they work against. The dropdown shows each one's endpoint next to its name, so you can cross-check it against Notion's own documentation.
| Operation | Endpoint | What it does |
|---|---|---|
| Create page | POST /pages | A row in a database, or a subpage. Body goes in as Markdown. |
| Get page | GET /pages/{id} | The page object and its properties — not its content. |
| Get page as Markdown | GET /pages/{id}/markdown | The whole body as one Markdown string. |
| Update page properties | PATCH /pages/{id} | Status, dates, relations. Only what you list is touched. |
| Update page content | PATCH /pages/{id}/markdown | Replace the body, or find-and-replace inside it. |
| Append content | PATCH /pages/{id}/markdown | Add at the top or the end without touching what is there. |
| Archive page | PATCH /pages/{id} | To the trash, or back out of it. Recoverable for 30 days. |
| Query data source | POST /data_sources/{id}/query | Rows matching a filter. Output is iterable. |
| Get data source schema | GET /data_sources/{id} | Which properties exist and what type each one is. |
| Get block children | GET /blocks/{id}/children | The raw block tree, one level down. iterable |
| Search | POST /search | By title only, not content. iterable |
| Add comment | POST /comments | On a page, or as a reply in an existing thread. Needs the comment capability. |
Comments need their own capability
Notion keeps comments behind two capabilities of their own — Read comments and Insert comments — apart from reading and editing pages. A connection without them still creates and edits pages, but Notion answers Add comment (and the toolkit's add_page_comment) with a 403 restricted_resource. The run fails with a message that says the Notion connection lacks the comment capability, and what to do:
- Connect Notion — reconnect the credential in
Settings › Credentials. Authorizing again gives the connection the capabilities HostWebhook's Notion integration has today, comments included. - Your own integration — open it at
notion.so/profile/integrations, tick Read comments and Insert comments under Capabilities, save, and run it again. The secret stays the same.
Content is Markdown, not blocks
Writing content to Notion used to mean building a block tree by hand: every paragraph, heading and list item as its own typed object. It is the most expensive part of that API, and it is where most integrations stop at "create an empty page".
This node uses Notion's Markdown endpoints instead, so writing a page is sending text. Templates work inside it like anywhere else:
## Incident {{payload.id}}
Reported by **{{payload.reporter}}** at {{payload.timestamp}}.
- Severity: {{payload.severity}}
- Service: {{payload.service}}In the Markdown fields, backslashes arrive as written. Notion's Markdown reads a backslash as an escape, which would turn a path like C:\new\test into C:newtest; the node sends such backslashes so that Notion keeps them, and the page shows C:\new\test. A backslash in front of Markdown punctuation is still an escape, on purpose: \* is how you write a literal asterisk. Inside code —`C:\new` or a fenced block— nothing is touched, since Notion keeps backslashes there. The same goes for what you type in the field, what comes from the event, and the Markdown the AI toolkit writes.
What the fields send
Every text field — title, content, find and replace, comment, search query, filter value, icon — takes templates, and the result goes to Notion as plain text, exactly as typed: nothing is escaped. A title like [Alert] {{payload.msg}} with a message that reads she said "hello" arrives with its brackets and its quotes, and a multi-line value keeps its line breaks.
Properties (JSON) and Filter (raw JSON) take templates too, written inside the JSON, inside quotes where a value goes. Each value is escaped the way JSON needs, so quotes or line breaks in it do not break the object, and a string that is only one template keeps the value's type — a number stays a number. This is how a new row gets its values from the event — the title alone has a field of its own:
{
"Status": { "select": { "name": "{{payload.status}}" } },
"Due": { "date": { "start": "{{payload.dueDate}}" } },
"Amount": { "number": "{{payload.total}}" }
}{ "property": "Email", "email": { "equals": "{{payload.email}}" } }Through the API, either can also be saved as an object instead of a string — properties, filterJson — and is sent as that object.
Databases and data sources
Since Notion's 2025-09-03 API version, a database holds one or more data sources, and rows live in the data source — not in the database. Most databases have exactly one, and the picker resolves it for you. Anything you read online that queries databases/{id}/query is written against the previous API.
A row is a page in Notion — same object, different parent. That is why "Create page" is also how you add a row.
Filtering a query
The three filter fields — property, condition, value — cover the common case. Behind them the node fetches the data source schema first, because Notion has no generic filter: the key inside a filter clause is the type of the property, and the same "equals" is spelled differently for a select, a date and a number. That is one extra request, and it is the difference between a filter that works and a 400 that does not name the field.
The property to filter or sort by is picked from the chosen data source, with its type next to it; sorting also offers Notion's created_time and last_edited_time. With a data source that comes from the flow there is nothing to list yet, so the name is typed as it reads in Notion.
Once the property is picked, the condition list offers only what Notion accepts for its type. A multi-select, a person or a relation is checked with contains, since Notion has no equals for them; a select or a status with equals, not contains; and only dates offer is after and is before. Formulas and rollups have no simple conditions at all: they go in the raw JSON below. A property typed by hand has no known type, so its list stays whole, and a condition that does not fit the type fails the run with a 400.
Page and database ids
Pick from the list when you can. It opens on the pages edited most recently, and typing in its search asks Notion, so a page nobody has touched in months is found by its title too. When you cannot pick — because the target comes from the event — paste a URL, an id, or a template like {{payload.pageId}}, which is the page that fired a Notion trigger. All three work: the node normalizes whatever you give it.
A Data source field also takes what Notion lets you copy — the database's link or id. Since 2025-09-03 a database and its data source have different ids, so the node looks up the database's data source before calling. A database with several data sources needs the data source id itself: pick it from the list. After a Notion trigger, {{payload.dataSourceId}} is the data source of the row that fired it.
?v= is also a 32-character id. The node trims the query first, so it takes the page and not the view.Rate limits
Notion allows about three requests per second on average per connection, plus a per-workspace ceiling that scales with the workspace's plan. A 429 here is ordinary, not an incident.
The node retries on 429, 529 and the usual gateway errors, waiting as long as Notion asks in its Retry-After header. Failures that are ours — a missing credential, malformed JSON, a property that does not exist — come back as 400 and are not retried, because they will not improve.
AI toolkit mode
Once the credential is picked, the node asks for a Mode. AI toolkit turns its twelve operations into tools that an AI Node — or an MCP client, through one of your MCP servers — calls by name. Properties go in Notion's own format, e.g. {"Status": {"select": {"name": "Done"}}}, and the model can read a database's schema first so names and types match.
| Tool | Arguments |
|---|---|
| create_page | parent_type? · data_source_id? · parent_page_id? · title · markdown? · icon? · properties? |
| get_page | page_id · filter_properties? |
| get_page_markdown | page_id · include_transcript? |
| update_page_properties | page_id · title? · properties? · icon? |
| update_page_content | page_id · mode? · markdown? · find? · replace? — blocked by default |
| archive_page | page_id · in_trash? |
| query_data_source | data_source_id · filter_property? · filter_condition? · filter_value? · sort_property? · sort_direction? · page_size? · filter_json? |
| get_data_source_schema | data_source_id |
| append_to_page | page_id · markdown · position? |
| get_block_children | block_id · page_size? |
| search_notion | query? · filter_object? · sort_direction? · page_size? |
| add_page_comment | target? · page_id? · discussion_id? · text |
When the node has a database —picked in the toolkit settings or, when empty, the Single operation's—, data_source_id becomes optional and defaults to it. It takes a data source id, or the database's URL or id — what a user pastes into the chat —, and the node looks up the database's data source (one with several data sources needs the data source id). create_page with a parent_page_id and no parent_type creates a subpage of that page, not a row in the node's database.
What you control
- What the model can reach is what this connection reaches: every page and database shared with it in Notion. To give the agent less, share less with the connection.
- Characters per call sent to the model. 20,000 by default, anything from 1,000 to 200,000.
- Replacing a page's content starts blocked.
update_page_contentcan drop the whole body, and the API cannot undo it. Unblock it under Block these tool calls if the agent needs it.archive_pageis not blocked: it sends the page to Notion's trash, where it stays 30 days, and the same tool brings it back.
What goes back to the model
The same response as the operation, made readable: rich text comes as plain text, without the request and author metadata. A Created by or Last edited by column is the row's data, not metadata, so it keeps its person — the agent can answer who created a row. Queries and searches return 25 unless the model asks for more; results that don't fit the characters limit are left out whole, and long page text is cut, with a notes field saying so. Arguments are used as written: a {{payload.x}} the model writes is not resolved against the flow.
Testing a tool
operation in their payload that names another tool is refused.{ "operation": "query_data_source", "filter_property": "Status", "filter_value": "In progress", "page_size": 5 }It works the other way round too. Switch the node back to Single operation and its tools stop working right away: an AI Node that picked them earlier gets a refusal saying the node's AI toolkit is off. Otherwise each call would run the node's own configured operation — an Archive page, say — with the model's arguments. Turn the toolkit back on, or take the tools out of the AI Node.
Firing a pipeline from Notion
This node is the write side. To have Notion start a pipeline, use the Trigger node with its Notion service — it polls a database or the workspace and fires on pages added or edited. Behind it, this node reaches the page that fired as {{payload.pageId}} and, for a database row, its data source as {{payload.dataSourceId}}.
A flow that writes back — marks the page that fired it as seen, adds a row to the database it watches — does not fire itself again: Ignore changes made by this flow is on by default.
Cost
Notion's API is free on every plan, with no per-call charge. The only paid piece in Notion's own product is the no-code webhook action inside their automations, which this node does not use.