How Notion Pipeline Synchronization Works: One-Way Read-Only Sync Explained
The /notion-sync command publishes a read-only, one-way view of your job-search pipeline into a Notion database, ensuring the local repository remains the single source of truth with zero write-back to source files.
The MadsLorentzen/ai-job-search repository implements a robust Notion pipeline synchronization system that mirrors your job application data without compromising data integrity. This one-way read-only sync architecture guarantees that your local seen_jobs.json and job_search_tracker.csv files remain authoritative while providing a convenient Notion interface for visualization. Understanding this synchronization mechanism is essential for maintaining data consistency across your job search workflow.
What Gets Synchronized
The sync process aggregates data from two primary local sources to create a unified view in Notion.
- Ranked job entries from
job_scraper/seen_jobs.jsonare filtered by theirrank_scoreand synchronized with their associated metadata including company, role, verdict, and status. - Application records from
job_search_tracker.csvare fully synchronized, with tracker status taking precedence over scraper status when conflicts arise. - Stable identification is maintained through a
Keyproperty calculated as the lower-cased<company>_<role>string, ensuring each job maps to exactly one Notion page regardless of subsequent syncs.
The following files comprise the complete data layer:
| File | Role |
|---|---|
job_scraper/seen_jobs.json |
Contains scraped and ranked job entries with rank_score, status, and dates. |
job_search_tracker.csv |
Tracks applications, outcomes, and associated document filenames. |
job_scraper/notion_sync.json |
Stores the Notion database ID and last-sync timestamp (git-ignored). |
The One-Way Read-Only Architecture
The system enforces a strict read-only, one-way data flow from repository to Notion, preventing any possibility of circular updates or data corruption.
The command exclusively reads local JSON and CSV files and performs upsert operations in Notion. Page bodies are write-once—created during initial page generation but never modified during subsequent updates unless explicitly rebuilding with --rebuild. Property fields such as status, score, and verdict receive updates, but the original content body remains immutable to preserve any user-added notes within Notion. No synchronization mechanism exists to write data back into seen_jobs.json, the tracker CSV, or any other repository file, ensuring the repository maintains absolute authority over the dataset.
The Six-Step Sync Workflow
The complete synchronization logic is defined in .claude/commands/notion-sync.md and executed through the following pipeline:
Step 0: Parse Arguments
The command evaluates CLI flags to determine the sync scope. Valid options include --min-score to set a threshold (default 60), --all to bypass score filtering entirely, and --rebuild to force regeneration of page bodies for existing entries.
Step 1: Pre-flight Notion Connection
The system verifies that the Notion MCP tool is present and authenticated. If the connection check fails, the command exits immediately with a diagnostic message before processing any local data.
Step 2: Build the Sync Set
The command reads job_scraper/seen_jobs.json and job_search_tracker.csv, selecting jobs that meet the score threshold and merging tracker rows. When a job exists in both files, the tracker status takes precedence over the scraper status.
Step 3: Locate or Create the Database
The system reads job_scraper/notion_sync.json to retrieve the existing database ID. If no database exists, it creates a new Notion database with the specified schema (Name, Company, Score, Verdict, Status, Key, URL, etc.) and persists the new ID to notion_sync.json for future runs.
Step 4: Upsert Rows with Stable Keys
For each job in the sync set, the system queries Notion for an existing page matching the Key property:
- No match: Creates a new page with all properties, then proceeds to Step 5 to write the detailed body.
- Match found: Updates only the property fields (Score, Verdict, Status) while leaving the existing page body untouched.
Step 5: Write Detail Pages (New Pages Only)
For newly created pages, the system fetches the job posting via WebFetch, composes a concise digest, and attaches links to local archives. This step executes only during initial page creation to prevent overwriting user annotations.
Step 6: Report and Persist State
The command prints a summary of created, updated, unchanged, and failed rows. It stores the latest sync timestamp in job_scraper/notion_sync.json to track synchronization history.
# Simplified pseudo-code from Step 4 implementation
if not notion_page_exists(key):
notion.create_page(
title=f"{role} — {company}",
properties={
"Company": company,
"Score": rank_score,
"Verdict": verdict,
"Status": status,
"Key": key,
"URL": posting_url,
}
)
notion.update_page_body(page_id, build_body(job))
else:
notion.update_properties(page_id, {
"Score": rank_score,
"Verdict": verdict,
"Status": status,
})
Data Integrity Safeguards
The synchronization implements multiple protective mechanisms to ensure reliable, non-destructive operations:
- Idempotent upserts on the
Keyproperty guarantee that re-running the command never creates duplicate pages, even across multiple execution cycles. - Write-once bodies protect user-added notes in Notion from being clobbered during property updates.
- Zero side-effects on empty sets: The command aborts before any external API calls if the sync set is empty, preventing unnecessary network requests.
- Local document privacy: Only filenames (
cv_file,cover_letter_file) are stored in Notion properties. The actual PDF documents remain indocuments/applications/and never leave the local machine.
Running the Sync Command
Execute the synchronization within the Claude Code CLI using the following patterns:
/notion-sync # Sync ranked jobs with score ≥ 60 and all tracked applications
/notion-sync --all # Sync every ranked job regardless of score
/notion-sync --min-score 80 # Only jobs scoring 80 or higher
/notion-sync --rebuild # Rewrite page bodies for newly created pages
After execution, the system updates job_scraper/notion_sync.json with connection metadata:
{
"database_id": "12345678-abcd-ef01-2345-6789abcdef01",
"database_url": "https://www.notion.so/Job-Search-Pipeline-12345678abcdef01",
"last_sync": "2026-07-26"
}
Summary
- One-way read-only sync ensures the repository remains the single source of truth while providing a Notion visualization layer.
- Stable Key identification using lowercase
<company>_<role>enables idempotent upserts without duplicate creation. - Write-once page bodies preserve user annotations in Notion, with property-only updates on subsequent syncs.
- Local file authority means
job_scraper/seen_jobs.jsonandjob_search_tracker.csvdrive all changes, withjob_scraper/notion_sync.jsonmaintaining only the database pointer and timestamp. - Privacy-preserving design keeps actual documents local, sharing only filenames and metadata with Notion.
Frequently Asked Questions
Can I edit data in Notion and have it sync back to the repository?
No. The architecture is strictly one-way. The /notion-sync command reads from local files and writes to Notion, but no mechanism exists to propagate changes from Notion back to seen_jobs.json or job_search_tracker.csv. The repository remains the immutable source of truth.
What happens if I run the sync command multiple times?
The sync is idempotent. Each run queries existing Notion pages by their Key property and updates only the property fields (Score, Status, Verdict) for matches. Page bodies remain untouched unless you specify the --rebuild flag, ensuring repeated executions never duplicate entries or destroy manual notes.
How does the system prevent duplicate job entries?
Each job is identified by a stable Key calculated as the lower-cased <company>_<role> string. Before creating any page, the system queries Notion for an existing page with that Key. If found, it updates the existing page; if not found, it creates a new one. This upsert pattern guarantees one page per job regardless of how many times you run the command.
Are my resume and cover letter files uploaded to Notion?
No. The synchronization stores only the filenames (cv_file, cover_letter_file) in Notion database properties. The actual PDF documents remain in your local documents/applications/ directory and are never transmitted to Notion's servers, ensuring your documents stay private and local.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →