The work on this page is covered by a non-disclosure agreement with Smoothops Consulting and their end client. The client is not named and not described. No hostname, workflow id, database id or record count belonging to them appears anywhere on this page. What is described is the architecture and the rules it runs on.

How Abhiman Labs built a six workflow sync for a Smoothops Consulting client
Attio decides what is true. Most of the code exists so that nothing gets written twice.
The Problem
Smoothops Consulting builds Notion workspaces. Tim Jeffries brought Abhiman Labs in as the backend automation specialist for a client build where three systems had to hold the same state: Attio for the CRM, Granola for the meeting recordings, Notion for the work.
The short path was Granola straight into Notion. That path was rejected. Attio is the source of truth, so every meeting has to resolve to a canonical person and a canonical company before it reaches Notion, and everything flows Granola to Attio to Notion.
That one decision sets the rest of the build. Six workflows now sit on that path, and most of what they do is decide whether a record already exists before writing anything. They keep the numbering they carry in production, which is why there is no workflow 4 below. That one handles deletions and is out of scope here.
Three systems, one state
Granola supplies the recordings. Attio resolves who and what they belong to. Notion holds the surface people work on.
What it runs on
Self hosted n8n in queue mode. Postgres holds execution state, Redis holds the job queue, and workers pull from it. A single process instance would run these one after another and could not carry the load.
That is what lets a burst land safely. If 100 webhooks fire at the same moment, every one becomes a queued job instead of being handled inline, so none is dropped on the floor. Where several of them touch the same record they are taken first in, first out, and the order they arrived in is the order they are applied.
Postgres is doing real work in that sentence. n8n's default database takes a lock on the whole file to write, so concurrent executions end up queueing behind the execution log itself. Postgres writes many at once, which is the difference between workers that scale and workers that wait on each other.
Redis does a second job here. The application level locks live in it, and that is what stops two of those queued jobs from being inside the same record at the same time. It is the next section.
Two edits, one person, one lock
The mutex is INCR on a lock key with a 30 second TTL. It returns 1 to the winner and a higher number to everyone else.
A second key holds a random ownership token, also 30 seconds, and only the run whose token matches may release. Without it, a slow run could release a lock that a later run now legitimately holds.
A third key maps the n8n execution id to the lock at 90 seconds, deliberately outliving the lock itself, so a crashed run can still be traced afterwards. Crash recovery is contested on purpose: a separate INCR on a steal key that carries its own short expiry, so when several runs all notice the same stuck lock exactly one of them wins the right to force clear it, and the next stuck lock gets a fresh contest.
Signatures, and deliveries that arrive twice
The webhook verifies an HMAC signature over the raw body. Unsigned traffic, which any public URL gets constantly, is dropped silently. A wrong signature stops the run loudly.
Idempotency is a separate problem from locking, and it has a separate mechanism. Repeat deliveries are dropped through an n8n Data Table keyed on the idempotency header the CRM sends: read, then write if absent. That pair is only safe because it happens inside the lock. A dedicated housekeeping workflow prunes it.
Attio to Notion People Sync
Webhook triggered on a person being created or on any attribute being edited. It re-reads the person from Attio rather than trusting the webhook payload, because Attio keeps superseded values in the same list and the newest is not always first. It also recognises the link it wrote back to Attio and stops there, which is what prevents an endless loop.
- Webhook
- Compute HMAC
- Signature Matches?
- Acquire Lock
- Wait
- Retry Acquire Lock
- Seen This Delivery?
- Re-read Person
- Route On Change
- Resolve Company
- Find Person Page
- Pick Duplicate Keeper
- Release Lock
Attio to Notion Company Sync
The company sync is narrow on purpose. Its webhook fires only on Name, Domains or Description, so editing any other field on an Attio company does not reach Notion at all.
- Webhook
- Which Field Changed
- Stored Notion Link?
- Update Linked Page
- Search By Domain
- Search By Name
- Company Id Matches?
- Halt On Conflict
- Write Company Page
Granola to Notion Sync Queue
Scheduled, with no webhook. It lists notes from two separate recorder API keys, because neither key is a superset of the other, and each note is then fetched with a key proven to have access to it.
- Schedule Trigger
- List Notes Key A
- List Notes Key B
- Diff Fingerprints
- Fetch Note By Id
- Group Same Call
- Override Set?
- Ensure Queue Column
- Value Actually Differs?
- Write Queue Row
Granola to Attio Notes
Scheduled, plus an on demand button. It pages through Attio meetings with a cursor, and if that pagination is cut short it says so rather than guessing. A note is matched to a meeting on the exact start instant, with five minutes of tolerance.
- Schedule Trigger
- Read Queue Rows
- Page Attio Meetings
- Cursor Complete?
- Match Start Instant
- Choose Target Record
- List Notes On Record
- Marker Present?
- Create Note
- Write Note Id Back
- Delete Replaced Note
- Count Failures
Attio to Notion Meeting Pages
Two entry doors: called directly by 3b for every note, or an Attio call recording event. A third door, the note created event, is deliberately switched off, because it does not know which queue row a note came from and created a stray page every time a note was re-edited.
- Called By 3b
- Which Entry Door
- Find By Meeting Id
- Verify Key On Page
- Find By Queue Relation
- Find By Legacy Column
- Match Title And Date
- Ambiguous?
- Create Meeting Page
- Rebuild Visibility
- Reconcile Duplicates
- Key 1Attio meeting or note idno match, fall through
- Key 2The queue relationno match, fall through
- Key 3Legacy text columnno match, fall through
- Key 4Same title, same date, no keysmatch, stop here
- Key 5Create the pageonly on a conclusive miss
Notion to Attio People Nudge
Workflows 1 and 2 only run when Attio says something changed, so there is no way to ask them to run. A button on a Notion row solves it: change one field on the Attio record, wait fifteen seconds, put the original value back. Two stored changes, so two events fire, and the field ends exactly as it started.
- Notion Button
- Read Notion Row
- Resolve Attio Record
- Match Conclusive?
- Change Field
- Wait 15s
- Restore Field
- Log Result
Reconcile instead of lock
The brief asked for a lock by meeting id during upserts. n8n cannot hold a distributed lock across executions, and check then create is not atomic, so 3c converges instead of excluding.
Straight after the write it re-queries the key, sorts the live matches by creation time and then by id, keeps the oldest and trashes the rest. Two racing runs sort identically, so they agree on the survivor without shared state. Everything downstream reads the reconciled page id.
When it stops and asks a person
Stopping loudly is deliberate. A silent skip would look identical to a deletion.
- Two duplicates that both carry hand made links, because both hold real work.
- More than 100 pages sharing one id.
- A page matched by name or domain that belongs to a different company record.
- Two untagged meeting pages sharing a title and a date.
- A note that matches two meetings.
- A row that has failed five times.
- Either API being unreachable.
The obvious build, and this one
The Result
Six workflows run in production, more than 250 nodes between them, on self hosted n8n in queue mode with Postgres and Redis behind it.
Attio, Granola and Notion hold one state. A recording resolves to a person and a company, lands on the right Attio record, and reaches the right Notion page without a second page appearing next to it.
Nothing in it improvises. When two answers are equally good, it stops and a person decides.
This is one build of several for Smoothops Consulting, and the arrangement has not changed: they hold the client, Abhiman Labs holds the backend. If that is the shape of the gap in your own delivery, say so here.