All posts
Technical

Five n8n and Notion mistakes I made so you don't have to

Notion is a good database for an n8n workflow right up until it isn't. The filter the node can't build, the property write that fails silently, and three more things I wish I had known before the first scanner shipped.

I run two scanners on the same pattern. One watches for new n8n templates every day and files them into a Notion database with a name, a description, a category, a difficulty and a set of tags. The other watches Ryanair and three regional airports for new routes out of Exeter, Newquay and Bristol, which started as a way to catch cheap flights for a stag do before everyone else did and has stayed running since. Both end the same way: anything new goes into Notion, and a Telegram bot tells me about it.

Notion is a good choice for this. It is where I already look every day, the database views are free, and the API is decent. It is also full of small traps, and the n8n node on top of it adds a couple of its own. Two of these cost me an afternoon each. The other three I got to skip because someone else had already written them up, so this is me returning the favour.

The pattern, for context

Every scanner is the same seven nodes, give or take.

Schedule → Fetch source → Notion: Get Many (known items)
         → Code: diff new against known
         → Notion: Create page (one per new item)
         → Telegram: send message

The interesting work is the diff in the middle. The source returns everything it knows about, Notion returns everything I have already seen, and the Code node subtracts one from the other. Most of what follows is about the two Notion nodes either side of it.

1. The filter you want is not in the filter builder

The Notion node’s Get Many operation for database pages has a filter builder. It works for the plain cases: a select equals this, a checkbox is ticked, a date is after that. The moment the thing you want to filter on is more involved than a single property against a fixed value, it runs out of road. Filtering against a value that came from an earlier node, filtering on a formula, combining conditions across property types: none of it fits the UI.

The node does have a JSON mode that takes the raw API filter object, and it is worth trying before you reach for code. I ended up in code anyway, because the values I was filtering against were coming from the scraper, not from a constant.

So the honest version of the pattern is: fetch everything, then filter in a Code node. Turn on Return All, and do the work in JavaScript where you can see it.

// Known URLs, from the Notion "Get Many" node.
const known = new Set(
  $('Notion: Known').all().map((item) => item.json.property_url)
);

// Candidates, from the scraper. Keep only the ones Notion has not seen.
return $('Scrape').all()
  .filter((item) => !known.has(item.json.url))
  .map((item) => ({ json: item.json }));

It feels like a step backwards. It is not. A Code node that reads as a sentence is easier to maintain than a filter builder with six rows in it, and it does not break when the property is renamed, because the failure is loud.

2. Property keys are Name|type, and a wrong one fails silently

When you write to a database page and set the properties by expression rather than by picking them from the dropdown, the key the node wants is the property name and the property type joined with a pipe: Name|title, URL|url, Category|select, Tags|multi_select. That is n8n’s convention, not Notion’s, and it is not obvious until you have seen the dropdown values.

The trap is what happens when the key is wrong. If you write to Catagory|select and the database has Category, the page is still created. The node reports success. The value just is not there. Nothing errors, because from Notion’s side a page with fewer properties filled in is a perfectly valid page.

Two habits fix this.

  • Run the Database → Get operation once and copy the property names out of the response, exactly as they are, capitals and all. Notion property names are case sensitive.
  • After the first real run, open the database and look at a row. Not the execution log, the actual row. If a column is empty, the key is wrong.

3. The database has to be shared with the integration

The API cannot see a database until you have connected the integration to it. Not to the workspace, to the specific page. A database you can see perfectly well in the app returns object_not_found to the API, and the error message does not say “share it with the integration”, it says the object does not exist.

This is the one everyone hits exactly once, usually when they add a second database to a workflow that has been running fine against the first. Open the database page, the menu in the top right, Connections, and add the integration. Then it exists.

4. Rich text stops at 2,000 characters

Every rich text value in a Notion property is limited to 2,000 characters per block. A template description, a scraped product blurb, a long error message: any of them can go over, and when they do the whole write is rejected with a validation error rather than truncated.

The fix belongs in the same Code node as the diff, because that is the last place you can see the data before it is written.

const clip = (text, max = 1900) =>
  text && text.length > max ? text.slice(0, max - 1) + '…' : text;

return items.map((item) => ({
  json: { ...item.json, description: clip(item.json.description) },
}));

I clip at 1,900 rather than 2,000, because the limit is on the rich text object Notion builds from your string, and the exact accounting for multi-byte characters is not something I want to be relying on at two in the morning.

5. Notion rate limits, and a daily scan looks like a burst

Notion asks for an average of about three requests per second per integration. Read that again with a scheduled workflow in mind: the scanner sits idle for twenty-four hours, then finds forty new items and tries to create forty pages at once. That is a burst, and Notion answers it with 429 Too Many Requests.

Two things, together, make this a non-issue.

  • Put the Create node inside a Loop Over Items with a small batch size and a short Wait between batches. A second between batches of three is plenty.
  • Turn on Retry On Fail on the Notion node, with a wait of a few seconds. Then the occasional 429 that gets through is retried rather than dropped.

The same applies on the read side. Return All on a database with a few hundred rows is several paginated requests, not one, and it counts against the same limit.

What I would do differently

Not much, which is the point of writing this down: the pattern works, it just has edges. If I were starting a third scanner tomorrow I would begin with the Code node in the middle, write the diff and the clipping first, and only then wire Notion on either side of it. The database schema would be fetched once and pasted into a note at the top of the workflow. And I would create the Telegram message last, because it is the only part that is fun, and it is a reward for the rest.

If you are running something like this and it is misbehaving in a way not listed here, I would genuinely like to hear about it. It probably belongs in a follow-up.