AWRA OpsHub Search

A Failed Lookup Looks Like Nothing There

Notion has no single call that means "create this or update it if it exists", so the connector asks first and then writes. Everything interesting about it follows from the gap between those two requests.

Integrations & Data AWRA OpsHub Team 13 min read

Some systems will take a record and work out for themselves whether it is new. Others make you find out first. The difference sounds like a detail of the interface and it is not: the moment you have to look before you write, you have taken ownership of a question the other system used to answer, including what happens when the looking goes wrong. This connector looks first, because there is no alternative, and this post is about what that costs.

Two requests where other connectors need one

The Airtable connector in this series sends one request per record. It says here is a record, merge it on this field, and the matching happens where the data lives.

Notion has no equivalent. Creating a page and updating a page are different operations, at different addresses, with different verbs and different request bodies — a create names the database it belongs to, an update must not.

So each record becomes a query for a page with that exact title, followed by either an update of what was found or the creation of a new page. Two calls, and the second one's shape depends on the first one's answer.

Three ways to decide whether a record already exists: computed identity, a server-side merge, and a lookup followed by a write.
Three connectors, three answers. The right-hand column is the only one with a gap between deciding and writing — and every entry below the line is a consequence of that gap.

What null means

The lookup returns either the identifier of a matching page or nothing. Nothing means create.

Read that again with a network in mind, because there are two ways to get nothing. The query can succeed and find no match — the ordinary case for a new customer. Or the query can fail: a rate limit, a timeout, a bad gateway, a moment when Notion was busy.

Both produce nothing, and nothing means create. So a transient failure on the lookup does not surface as an error; it surfaces as a second page with the same title as the first, some weeks later, noticed by somebody wondering why there are two entries for the same supplier.

A lookup that fails and a lookup that finds nothing are the same value. The difference between them is a duplicate.

This is worth stating plainly rather than leaving in the code, because it is the characteristic failure of every read-then-write integration and it is invisible from both ends. Notion logged a request that failed and was retried by nobody. We logged a page created successfully. Both records are accurate.

Distinguishing the two states is a small change — the query already knows whether it succeeded, and treating a failed lookup as skip this record and try next run rather than create turns a silent duplicate into a record that syncs a few minutes later. It is a good example of the kind of improvement that is invisible until it is written down, which is the reason to write it down.

And the gap between the two calls

The second consequence of looking first is a window, and it is a real one rather than a theoretical one.

Two runs of the same sync starting close together — somebody presses the button while a scheduled run is already going — both query for the same title, both find nothing, and both create. There is no lock, and there is nothing in Notion preventing two pages from sharing a title.

A server-side merge cannot do this. The decision and the write are one operation, so there is no interval for a second caller to occupy. That is the real advantage of the one-request design, and it is not about speed.

The practical mitigation today is that the sync is queued rather than run inline, so the ordinary case is one run at a time. The honest description is that the guarantee comes from the arrangement rather than from the write itself.

One match, and only ever one

The lookup asks for a single result, which has a consequence worth knowing if duplicates already exist.

Where two pages share a title, the query returns the first and the sync updates it. The second is never touched — not deleted, not updated, not reported. It sits there indefinitely holding whatever it held when it was created.

That is the right default. Merging duplicates is a decision with data loss in it and it should be made by a person looking at both pages, not by a sync that noticed there were two. What it means in practice is that duplicates are cleaned up in Notion, once, and then stay cleaned up.

A value has to be shaped for its column

The other thing a two-request write inherits is that both branches take the same property shapes, and Notion's property shapes are typed.

An email address is not a string in a Notion payload. It is a structure whose form depends on whether the destination column is an email column, a phone column, a link column or a text column — four different envelopes around the same characters.

So the database's schema is read once per run, each optional value is looked up against it, and the value is wrapped in the envelope its column needs. A column that is absent is skipped, exactly as in the Airtable connector.

A column that is present but of a type this connector does not shape — an email held in a select column, say, or a phone number in a number column — is also skipped, and the page still lands with its title and whatever else was shapeable. A property it cannot express is left out rather than allowed to reject the page, which is the same degrade-rather-than-stop principle applied one level down.

The four shapes covered — email, phone, link and text — are the ones a contact record actually needs. The remaining types are mostly things a name and an email address have no business being put into, and adding a shape for a specific one is a contained piece of work when a workspace turns out to want it.

What a run costs

Two requests per record is worth pricing, because it is the number that decides how this behaves at volume.

  1. One schema read per run

    The database structure is fetched once and reused, rather than once per record — which is the difference between one request and two hundred.

  2. Two requests per record after that

    A lookup and a write. Two hundred customers is four hundred calls, and Notion rate-limits per integration, so the ceiling on a run is theirs rather than ours.

  3. Two hundred records per type, newest first

    The same bound as the sibling connector, and the same statement about it: it is a number, and pushing a complete list is a different feature.

The one-per-record lookup is the obvious thing to improve, and the shape of the improvement is already in the code for a different fact: the schema is read once and reused. A single query returning the titles already present would replace two hundred lookups with one page of results, and it would close the failed-lookup ambiguity at the same time, because a list that arrived is distinguishable from a list that did not.

What is in place

How a record becomes a page

A lookup on the exact title

Each record is matched against pages whose title property equals its title, which is the same identity rule the destination shows in its own first column.

Built in

Update and create as separate calls

A found page is patched at its own address and a new one is posted with its parent database named, because the two operations take different shapes.

Built in

The schema read once per run

One structural request serves every record, so the per-record cost is the lookup and the write rather than three calls.

Built in

Values wrapped for their column type

Email, phone, link and text columns each take a different envelope around the same characters, and the envelope is chosen from the destination's own schema.

Built in

An absent column skipped

A value whose property does not exist is left out, so a database with fewer columns still receives the record.

Built in

An unexpressible column skipped too

A property of a type this connector does not shape is omitted rather than sent malformed, so the page lands with everything else intact.

Built in

Titles truncated to the platform limit

Text is cut to the two thousand characters a Notion fragment holds, so a long value is shortened rather than rejected.

Built in

Only the first match updated

Where duplicate titles already exist the first is updated and the others are left untouched, because merging pages is a decision with data loss in it.

Built in

A missing title property stops the run

Checked once before any record is attempted, so the reason is reported rather than repeated two hundred times.

Built in

Record types isolated

Customers and suppliers run independently, and a failure in one is logged and reported as zero for that type while the other continues.

Built in

The sync queued, not inline

Runs happen on a queue, which is what keeps the ordinary case to one run at a time and the request that started it fast.

Built in

A bounded wait per call

Fifteen seconds, applied to both the lookup and the write, so a slow workspace costs a known amount of time per record.

Built in

Six of these exist because the write is two requests rather than one. That is the honest cost of a destination with no merge operation, and it is worth seeing listed rather than described as a detail of the interface.

Three positions held on purpose

  • Duplicates in the destination are left alone. A sync that found two pages with one title updates the first and reports nothing about the second, because deciding which of two records survives is a judgement with data loss in it and it belongs to a person with both pages in front of them.
  • A property we cannot express is dropped, not forced. Sending a malformed value would reject the whole page, so an unshapeable column costs that one field and nothing else.
  • The two-request cost is stated rather than hidden. Four hundred calls to sync two hundred records is the price of a destination with no merge, and a single-query improvement is worth naming out loud because it closes the duplicate ambiguity as a side effect.

Five questions about a read-then-write sync

Is it one request per record or two?

A good answer sounds like

They should know.

What ours actually is

Two — a lookup then a write — because the destination has no merge operation.

What happens if the lookup fails?

A good answer sounds like

The record is retried.

What ours actually is

Today a failed lookup is indistinguishable from no match and results in a new page; separating the two is a contained change.

Can two runs at once create duplicates?

A good answer sounds like

Not if writes are merged remotely.

What ours actually is

Here the protection comes from running on a queue rather than from the write itself.

What if there are already two pages with one name?

A good answer sounds like

The first is updated.

What ours actually is

Exactly that, and the second is untouched rather than merged or deleted.

What if a column is the wrong type?

A good answer sounds like

That field is skipped.

What ours actually is

The page still lands with its title and everything shapeable; nothing is sent malformed.

Our take

Whether a destination has a merge operation looks like a piece of trivia about its interface, and it decides more about an integration than anything else in it. With one, the question of whether a record already exists is answered by the system holding the records, in the same instant as the write, and there is no window and no ambiguity. Without one, you look first — and now a failed lookup, a slow response and a second run happening at the same time are all your problem, and each of them produces a duplicate rather than an error. None of that is a reason to avoid the destination; people keep their supplier lists in Notion and want them current. It is a reason to know which of the two you are running, because the failure modes are not the same shape and the one that is silent is the one worth naming.

Ask whether the write is one request or two

The answer tells you where duplicates come from, and whether anybody has thought about a lookup that fails.

Talk through record syncing

Frequently asked questions

Why do I sometimes see two Notion pages for the same customer?

The sync looks for an existing page by exact title and creates one when it finds none. A lookup that fails for a transient reason currently produces the same result as a lookup that found nothing, which means a new page. Separating those two cases is a contained change and it is the right fix.

Will the sync clean up duplicates it finds?

No. Where two pages share a title the first is updated and the others are left exactly as they are, because choosing which record survives is a decision with data loss in it. Once they are merged in Notion they stay merged.

My database has an Email column but nothing lands in it. Why?

Notion property values are typed, and the connector shapes values for email, phone, link and text columns. A column named Email that is actually a select or a number is skipped, and the page still lands with its title. Adding a shape for a specific type is a small piece of work.

How many requests does a sync make?

One to read the database structure, then two per record — a lookup and a write. Two hundred records is about four hundred calls, which is why the per-record lookup is the first thing worth replacing with a single query.

What if I press Sync while a scheduled run is going?

Runs go through a queue, which is what keeps the ordinary case to one at a time. The protection comes from that arrangement rather than from the write, which is an honest distinction worth making.

Help Center

Need a quick answer while you read?

Run inventory, procurement, assets, sales, and field work with approved AWRA guidance for setup, migration, integrations, security, pricing, and support.

Search all approved AWRA public help articles.

Open Help Center