Human overview · for understanding

Notion-Truth Sync — Overnight Build

Your hand-sorted Notion kanban is now the single source of truth. The app mirrors it, every touchpoint lands in both systems, and the 3 AM pass drafts a follow-up for every active lead. · 2026-07-29

Your hand-sorted Notion kanban is now the single source of truth. The app mirrors it, every touchpoint lands in both systems, and the 3 AM pass drafts a follow-up for every active lead.

Master summary — the gist in 30 seconds

TL;DRYou sorted the CRM kanban by hand and asked the pipeline to adopt it as truth. Overnight, the app was rewired so the board shows your exact Notion columns and leads, contact pages got their own filtered activity views, every email and Meet event is now recorded in both systems, and the 3 AM sweep writes a tailored follow-up draft for every lead worth chasing. Along the way we caught two live bugs that were quietly corrupting the sync.

Input: your manually sorted Notion kanban plus everything that happens to a lead (emails in and out, Meet bookings, accepted invites, show-ups). Output: one board that always matches Notion, one activity log both systems share, and a fresh follow-up draft waiting on each active lead's card and Notion page every morning.

Why this mattersUntil tonight the app and Notion were drifting apart: renamed columns broke the mapping, history lived in two half-complete places, and follow-ups depended on you remembering. Now Notion is the constitution, the app obeys it, and the morning starts with drafts already written.
flowchart LR
 N["Your Notion kanban<br/>(hand-sorted truth)"] --> S["Sync engine<br/>(Notion always wins)"]
 E["Emails, Meets,<br/>invites, show-ups"] --> S
 S --> B["Pipeline board<br/>(same columns, same leads)"]
 S --> A["Activity Log<br/>(one shared history)"]
 S --> D["3 AM sweep<br/>(draft per lead)"]

1 · The goal — Notion becomes the constitution

TL;DROne truth, two windows. Whatever you arrange in Notion is what the app shows, and whatever happens to a lead is written to both systems at once.

Input: eight deliverables you dictated (mirrored board, touchpoint parity, contact-page views, a clean event taxonomy, ongoing two-way recording, a manual-wins sync rule, 3 AM drafts, a morning audio recap). Output: all of them built, tested twice, and verified live before morning.

Why it mattersYou work in Notion. If the app disagrees with Notion, you stop trusting the app, and a tool you do not trust is a tool you stop opening. Making Notion the constitution removes that entire class of doubt.
flowchart TD
 G["Owner's ask:<br/>make my kanban the truth"] --> D1["Mirror the board"]
 G --> D2["One shared<br/>activity history"]
 G --> D3["Filtered view on<br/>every contact page"]
 G --> D4["3 AM follow-up<br/>drafts"]
 D1 & D2 & D3 & D4 --> M["Working + twice-tested<br/>by morning"]

2 · What was broken — a rename and an invisible character

TL;DRTwo silent bugs were corrupting the sync: the app still spoke Hungarian column names after you renamed everything to English, and one column name contained an invisible non-breaking space that made exact matching impossible.

Input: your hand-renamed Notion columns. Output before the fix: the app compared its old Hungarian labels against your new English ones, matched nothing, and treated whole columns as unknown. Worse, the "Booked" column had an invisible special space character in its name, so even a correct-looking English comparison failed, and the five-minute sync was one push away from creating a duplicate "Booked" column in your Notion that would have looked identical to the real one.

Why it mattersThis was the core break the whole night was built around. An invisible character is the nastiest kind of bug: it looks right in Notion, looks right in a screenshot, and fails anyway. Both halves are fixed. Reads now tolerate either spelling, and writes use your exact characters so a duplicate column can never be created.
flowchart TD
 R["You renamed columns<br/>to English in Notion"] --> X["App still expected<br/>the old Hungarian names"]
 R --> Y["One name carried an<br/>invisible special space"]
 X --> B1["Whole columns<br/>read as unmapped"]
 Y --> B2["Near-miss: a duplicate<br/>lookalike column"]
 B1 & B2 --> F["Fixed: reads are tolerant,<br/>writes use your exact bytes"]

3 · The sync constitution — manual moves always win

TL;DRIf you drag a card in Notion, the app follows, no questions asked. The app only advances a lead on its own when a real event happened, and it writes down why.

Input: a card moved by hand in Notion, or a real event like a signed contract or a booked call. Output: the app copies your manual move immediately, even backwards. Automatic moves happen only on real events and always carry a logged reason. When the two disagree, Notion wins and the conflict shows up on the Health tab so nothing is ever silently overruled.

Why it mattersTwo systems editing the same field is how CRMs eat data. A clear constitution, human beats robot, robot must show its work, means you can reorganize freely without fearing the app will fight you five minutes later.
flowchart TD
 M["You move a card<br/>in Notion"] --> W["App follows,<br/>even backwards"]
 E["Real event happens<br/>(payment, booking)"] --> A["App may advance<br/>+ logs the reason"]
 W & A --> C{"Conflict?"}
 C -->|yes| H["Notion wins,<br/>Health tab shows it"]
 C -->|no| OK["Systems agree"]

4 · The Type taxonomy — from machine codes to seven human labels

TL;DREvery activity-log row now carries one of seven plain-English event types instead of cryptic machine codes, and all 1,653 existing rows were flipped without losing the original codes.

Input: an activity log where the event type read like robot shorthand. Output: seven human labels — Incoming email, Outgoing email, Meeting with transcript, Phone call, Google Meet event, Created Google Meet event, Accepted Google Meet invitation. The old machine codes were not thrown away, they moved into a hidden field, so nothing automated broke and nothing historical was lost. Four stray rows from the invisible warmup mailbox were archived on sight, as the house rule demands.

Why it mattersThe activity log is now something you can read at a glance instead of decode. And because it was a migration rather than a duplicate, there is still exactly one Type field, not two competing ones.
flowchart LR
 OLD["1,653 rows with<br/>machine codes"] --> MAP["Migration map:<br/>code to human label"]
 MAP --> NEW["7 readable types<br/>on every row"]
 MAP --> KEEP["Original code kept<br/>in a hidden field"]
 MAP --> ARC["4 warmup strays<br/>archived, invisible"]

5 · Contact pages — one filtered window per lead

TL;DREvery contact page now shows a live view of the shared activity log, filtered to that one lead, newest first, with the event type right up front. The old copy-pasted inline history is gone.

Input: a contact page that used to carry its own hand-maintained history blocks. Output: the same page with a linked view into the one shared Activity Log, showing only that lead's events, sorted newest on top. Rolled out carefully: one pilot contact with visual checks in a real browser, then two more with checks, then all remaining contacts in bulk. Inline history was only archived after the new view was verified to show at least as much.

Why it mattersHistory written in two places is history that disagrees. One log, many filtered windows means every contact page is always current without anyone maintaining it, and archiving instead of deleting means nothing is unrecoverable.
flowchart TD
 LOG["One shared<br/>Activity Log"] --> V1["Dupumix page:<br/>pilot + visual QA"]
 V1 --> V2["2 more leads<br/>+ QA each"]
 V2 --> ALL["All remaining<br/>contacts in bulk"]
 ALL --> CLEAN["Inline history archived<br/>only after parity check"]

6 · The 3 AM sweep — drafts in your own voice, plus honest triage

TL;DREvery night the sweep reads each active lead's conversation and writes a follow-up draft in your voice, reusing your own past replies wherever a similar situation exists. Nothing is ever sent automatically.

Input: each lead's full history, plus a library built from 112 of your real past replies, your proven best-scoring reply style, and 233 ready-made drafts. Output: a follow-up draft on the lead's pipeline card and in a text field on their Notion page, line breaks intact. The sweep also triages: a lead who clearly said no gets moved to Lost with a logged, visible reason, a merely quiet lead is left alone, and the four closed columns are skipped entirely. Drafts never reach an outbox, sending stays a human act.

Why it mattersFollow-ups are where deals die of neglect. Waking up to a written draft for every live conversation turns follow-up from a memory task into a review task. Grounding drafts in your own past replies is what keeps them sounding like you instead of like a robot.
flowchart TD
 S["3 AM sweep wakes"] --> R["Reads each active<br/>lead's conversation"]
 R --> G["Grounds in your 112<br/>real past replies"]
 G --> J{"Per-lead judgement"}
 J -->|worth chasing| DR["Draft on card +<br/>Notion page"]
 J -->|clearly said no| L["Moved to Lost,<br/>reason logged"]
 J -->|closed column| SK["Skipped"]
 DR --> H["Human reviews,<br/>human sends"]

7 · How it was tested — twice by strangers, then one live ritual

TL;DRTwo independent QA waves with fresh eyes and different instructions, then a single live end-to-end ritual on staging where a fake lead walks the entire funnel and every step is judged from actual screenshots.

Input: the finished build. Output: wave one checks it, then wave two checks it again with brand-new testers following different instructions so shared blind spots cannot pass twice. The finale is one continuous journey on staging: a sentinel test lead is created, moved by hand in Notion, emailed in and out, booked into a Meet, swept at 3 AM logic, and finally purged without a trace. Verdicts come from screenshot pixels, so a blank board can never be waved through as a timeout. Only after that does it ship to production and get verified live.

Why it mattersA build that passes its own author's tests has proven very little. Independent double-checking plus a pixel-judged live ritual is what earns the word verified, and the leftover test data is purged so your real CRM stays clean.
flowchart LR
 B["Build done"] --> W1["QA wave 1"]
 W1 --> W2["QA wave 2:<br/>fresh agents,<br/>different script"]
 W2 --> F["Live finale:<br/>fake lead walks<br/>the whole funnel"]
 F --> P["Judged from<br/>screenshot pixels"]
 P --> SHIP["Ship to prod +<br/>verify live"]
 SHIP --> PURGE["Test data purged"]

⏭️ Next steps — the calls only you can make

TL;DRThe build is done, but six small decisions are deliberately left on your desk. Each is a one-line change once you say the word.

Input: your answers. Output: instant adjustments. The open items: 1) "Website done & fully paid" currently sorts last on the board because Notion's API cannot read your hand-sorted column order — say where you want it. 2) The board deliberately shows 28 active leads your Notion view hides because they have no Deal Type — keep them visible or mirror the filter exactly? 3) Your view excludes two people under Assign, but the API cannot read which two — name them and it is fixed. 4) The stale "Ip. egyeztetve" option is now empty — deleting it is a one-click job in the Notion UI, left for you. 5) The legacy 50-row activity log turned out to be 17 test fixtures plus 33 rows from a different business — purge the fixtures, and where should the 33 go? 6) Optional: a 2-minute admin-console grant would upgrade Meet attendance from transcript-inference to direct detection.

Why it mattersEvery one of these touches your data or your view of it, and guessing wrong on any of them costs more trust than the ten seconds it takes you to answer.

💡 Fun facts & practical stuff

TL;DRFive things worth knowing from the night.

1) The nastiest bug of the night was a single invisible character — a non-breaking space you typed by accident while renaming a column. It defeated both the app and any screenshot-based check, because it renders identically to a normal space. 2) The cloud machine ran out of its monthly AI budget at 00:46, mid-run. The fallback armed at launch worked: your Mac detected the stall in the morning, took over, and finished the remaining steps locally. Nothing was lost because every commit was already pushed. 3) All 1,653 activity rows got their new human-readable type, and the census of 269 contacts processed matched your live CRM count exactly. 4) The drafts are grounded in your own 112 real past replies, and the golden rule is literal: when a similar conversation exists in your history, the sweep reuses your own words first. 5) Column identifiers were kept stable under the renames, so your saved board preferences like collapsed columns survived the whole operation.

Why it mattersThese are the details that explain the odd corners: why the morning brief mentions a cloud handover, why the board looks exactly right, and why the drafts sound like you.
Morning brief (full findings + numbers) →HANDOFF — goals, decisions, constraints →Build checklist (technical) →EBO — the live finale ritual →