Trigger when the user asks to:
Keywords: sync applications, update my application, job tracker, application status, add to notion, update notion, check my tracker, update my sqlite tracker, got an offer, got rejected, interview invite, job emails, Gmail job sync, career tracker, application manager, track job applications
Always read profile.md first. It contains the user's tracker backend choice,
Notion database ID or SQLite path, Gmail label IDs, and career email.
Resolve these fields before continuing (collect from user + write back to profile.md):
Integrations > Tracker Backend
notion or sqliteIntegrations > Notion > Career tracker database ID
notion.so/<workspace>/<DATABASE_ID>?v=...NOTION_DB_ID, COLLECTION_URL = collection://<NOTION_DB_ID>Integrations > Tracker Backend > SQLite DB path
~/.offerplus/applications.dbswelist tracker init --db <path>
Or directly: sqlite3 <path> "CREATE TABLE IF NOT EXISTS applications (name TEXT PRIMARY KEY, status TEXT NOT NULL, job_id TEXT, applied_on TEXT, notes TEXT, updated_at TEXT DEFAULT (datetime('now')));"Integrations > Gmail Labels table
mcp__claude_ai_Gmail__list_labels, show the list, ask the user
which labels are job-related, fill in the table in profile.md.Personal Info > Career email
Populate from the user's own labels ( mcp__claude_ai_Gmail__list_labels):
Company keyGmail sender patternGmail label IDLabel nameamazon``noreply@mail.amazon.jobs_(run list_labels)e.g. amazonlinkedin``jobs-noreply@linkedin.com(run list_labels)_e.g. linkedingoogle``@google.com——meta``@meta.com——_any_———
Add rows for any other companies the user has labeled. If no labels exist, rely on sender pattern alone.
Map email signals → pipeline status value (pick the first match). The pipeline is ordered from early to late stage; never move a status backwards.
PrioritySignal (case-insensitive)Status1"offer" OR "congratulations" OR "pleased to inform" OR "we'd like to extend" OR "offer letter"Offer2"on-site" OR "final round" OR "technical interview" OR "virtual interview" OR "interview loop"Interviewing3"schedule" OR "next steps" OR "move forward" OR "hiring manager" OR "phone interview" OR "video call"Interviewing4"online assessment" OR "coding challenge" OR "hackerrank" OR "codility" OR "take-home" OR "assessment link"OA5"recruiter" OR "phone screen" OR "initial screen" OR "introductory call" OR "talent acquisition"Recruiter screen6"unable to move forward" OR "not selected" OR "no longer considering" OR "other candidates" OR "position has been filled" OR "not moving forward"Rejected7"application received" OR "thank you for applying" OR "keep track" OR "under review" OR "successfully submitted"Applied / Received8(no email found, only a job listing)Applied / Received
If multiple threads exist for the same role, use the most recent email's status.
Auto-set Next action when creating or updating a Notion page:
StatusNext actionApplied / Received``Waiting``OA``Prep``Recruiter screen``Prep``Interviewing``Prep``Offer``Send availability``Rejected(leave blank / clear)Withdrawn(leave blank / clear)
Scan the full email body (not just the snippet) for ATS application URLs. Use the first match from the priority list; stop once found.
PriorityPlatformURL pattern to match1Greenhouseboards.greenhouse.io/ OR grnh.se/2Leverjobs.lever.co/3Workdayapply.workday.com/ OR myworkdayjobs.com/4Ashbyjobs.ashbyhq.com/5SmartRecruitersjobs.smartrecruiters.com/6LinkedIn job postinglinkedin.com/jobs/view/7Indeed job postingindeed.com/viewjob OR indeed.com/rc/clk8Any URL containing /jobs/ or /careers/ + company domaine.g. stripe.com/jobs/
Rules:
?gh_src=, ?utm_*, etc.) before saving.link = null; do not write the field.Infer Tags values from the email body using these rules (all case-insensitive).
Only write tags that already exist in the Notion Tags options list — check first, omit if not present.
TagSignals to look forReferral"referred by", "referral from", "your referral", "employee referral", or application link contains ref= / referral``Remote"remote", "fully remote", "work from home", "distributed team", "location: remote", "anywhere"Urgent"respond by [date]", "deadline", "respond within [N] days", "as soon as possible", "ASAP"Visa"visa sponsorship", "H-1B", "OPT", "CPT", "work authorization provided"
Present inferred tags to the user before writing when running interactively:
"Inferred tags: [Referral, Remote] — write to Notion? (y/n/edit)"
When running non-interactively (bulk sync), write them silently and note in the report.
Used in Step 3 to match email-extracted company names against existing tracker entries. Check aliases before running edit-distance; aliases are authoritative.
Canonical nameKnown aliasesMetaMeta Platforms, Meta Platforms Inc., Facebook, InstagramGoogleGoogle LLC, Alphabet, Google DeepMind, Google Cloud, YouTubeAmazonAmazon.com, Amazon Web Services, AWSMicrosoftMicrosoft Corporation, AzureAppleApple Inc.NetflixNetflix Inc.UberUber Technologies, Uber Technologies Inc.LyftLyft Inc.StripeStripe Inc., Stripe PaymentsAirbnbAirbnb Inc.CoinbaseCoinbase Global, Coinbase Inc.FigmaFigma Inc.NotionNotion Labs, Notion Labs Inc.OpenAIOpenAI Inc., OpenAI LPAnthropicAnthropic PBC
Add rows to this table whenever the user encounters a new company variant and confirms a merge.
Write updated rows back to profile.md under a Company Aliases table so they persist across runs.
Edit-distance fallback (when no alias matches):
text
FUNCTION fuzzy_match(extracted_name, existing_names[]):
candidates = []
FOR each existing_name in existing_names:
dist = levenshtein(extracted_name.lower(), existing_name.lower())
IF dist <= 2 OR one_is_prefix_of_other(extracted_name, existing_name):
candidates.append((existing_name, dist))
RETURN sorted(candidates, by=dist)[:3] # top 3 closest
If candidates found: present to user — "Found similar entry: '{existing}'. Is this the same company as '{extracted}'? (y/n)"
(skip if tracker_backend is sqlite)
FieldValueCollection URLcollection://<NOTION_DB_ID>Parent ID (for creates)<NOTION_DB_ID>
Expected schema (updated pipeline schema):
PropertyTypeAllowed values / notesNametitle"Company — Role Title"``statusselectApplied / Received, OA, Recruiter screen, Interviewing, Offer, Rejected, Withdrawn``Companymulti-selectCompany name (e.g. Google, Amazon)Tagsmulti-selectLabels like Referral, Remote, NYC, Top choice, Visa, OA, Onsite``Applied ondateISO date when first appliedLast touchdateDate of most recent email in the threadInterview datedateParsed date of the next scheduled interview (if any)Next actionselectWaiting, Prep, Follow up, Send availability``LinkurlJob posting URL or Greenhouse/application portal link
If the database still uses the old schema (status values Not started, In progress, Done instead of the pipeline above), notify the user and map as follows until they upgrade:
Old valueMaps to in old schemaApplied / Received``In progress``OA``In progress``Recruiter screen``In progress``Interviewing``In progress``Offer``Done``Rejected``Rejected``Withdrawn``Rejected
(skip if tracker_backend is notion)
Current (v0.5.0) schema:
sql
CREATE TABLE IF NOT EXISTS applications (
name TEXT PRIMARY KEY, -- "Company — Role Title"
status TEXT NOT NULL, -- pipeline status (see Status Mapping)
job_id TEXT,
company TEXT,
applied_on TEXT, -- ISO date YYYY-MM-DD
last_touch TEXT, -- ISO date of most recent email
interview_date TEXT, -- ISO date of next scheduled interview
next_action TEXT, -- Waiting | Prep | Follow up | Send availability
link TEXT, -- job posting or portal URL (ATS-extracted)
tags TEXT, -- comma-separated inferred tags e.g. "Referral,Remote"
notes TEXT,
updated_at TEXT DEFAULT (datetime('now'))
);
Migration from v0.2.x (run once if the DB already exists):
sql
ALTER TABLE applications ADD COLUMN company TEXT;
ALTER TABLE applications ADD COLUMN last_touch TEXT;
ALTER TABLE applications ADD COLUMN interview_date TEXT;
ALTER TABLE applications ADD COLUMN next_action TEXT;
ALTER TABLE applications ADD COLUMN link TEXT;
ALTER TABLE applications ADD COLUMN tags TEXT;
Run via: sqlite3 <DB_PATH> < migration.sql
Or inline: sqlite3 ~/.offerplus/applications.db "ALTER TABLE applications ADD COLUMN last_touch TEXT; ..."
At Step 0, check whether last_touch column exists ( PRAGMA table_info(applications)) and run the migration automatically if not.
text
INPUT: company (optional), date_range (default: newer_than:6m)
STEP 0 Load profile
Read profile.md → extract tracker_backend, CAREER_EMAIL, label table
IF tracker_backend == "notion": resolve NOTION_DB_ID, COLLECTION_URL
IF tracker_backend == "sqlite":
resolve DB_PATH; run init if DB does not exist
check PRAGMA table_info(applications) for last_touch column
IF missing: run migration SQL (see Config: SQLite Schema)
IF any required field blank: collect from user → write to profile.md → continue
IF company given AND not in label table:
run list_labels → confirm with user → append to profile.md label table
STEP 1 Search Gmail
IF company given:
query = build_query(company) # see Query Builder below
ELSE:
query = 'subject:"application" OR subject:"your application" OR subject:"interview" newer_than:6m'
threads = gmail_search(query, max_results=20)
FOR each thread WHERE snippet is ambiguous:
fetch full thread via gmail_get_thread(thread_id)
STEP 2 Parse threads → applications[]
FOR each thread:
company_name = extract_company(thread)
role_title = extract_role(thread)
status = map_status(thread) # use Status Mapping table above
next_action = map_next_action(status) # use Next Action Mapping table above
date = most_recent_message_date(thread)
applied_date = earliest_message_date(thread)
job_id = extract_job_id(thread) # if present in email
interview_date = extract_interview_date(thread)
# scan body for patterns: "June 9", "Monday June 9 at 2pm PT",
# "scheduled for <date>", "your interview is on <date>"
# normalise to ISO date YYYY-MM-DD; set null if not found
link = extract_link(thread)
# match ATS URL patterns (see Config: Link Extraction)
# strip tracking params; null if not found
suggested_tags = infer_tags(thread)
# apply Tag Inference rules; returns list e.g. ["Referral","Remote"]
# empty list if no signals found
page_name = f"{company_name} — {role_title}"
APPEND { page_name, company_name, status, next_action, date, applied_date,
job_id, interview_date, link, suggested_tags, thread_id }
STEP 3 Deduplicate (with fuzzy company matching)
IF tracker_backend == "notion":
all_pages = notion_fetch(COLLECTION_URL) # cache for reuse in Steps 6+7
existing_names = [p.name for p in all_pages]
FOR each application:
# 1. exact match
existing = find_exact(application.page_name, existing_names)
IF not found:
# 2. alias match on company portion
canonical = resolve_alias(application.company_name) # Config: Company Aliases
existing = find_exact(canonical + " — " + role_title, existing_names)
IF not found:
# 3. edit-distance match on company name portion
company_candidates = fuzzy_match(application.company_name,
[extract_company(n) for n in existing_names])
IF company_candidates:
ask user to confirm merge (show top match + distance)
IF confirmed: existing = candidate; write alias to profile.md
IF found: set existing_status, action = "update" or "skip"
ELSE: action = "create"
IF tracker_backend == "sqlite":
existing_rows = sqlite3 DB_PATH "SELECT name, company, status FROM applications"
FOR each application:
# 1. exact match
result = find_exact(application.page_name, existing_rows)
IF not found:
# 2. alias match
canonical = resolve_alias(application.company_name)
result = find_exact(canonical + " — " + role_title, existing_rows)
IF not found:
# 3. edit-distance match on company column
company_candidates = fuzzy_match(application.company_name,
[r.company for r in existing_rows if r.company])
IF company_candidates:
ask user to confirm merge
IF confirmed: result = candidate; write alias to profile.md
IF result is not null: set existing_status, action = "update" or "skip"
ELSE: action = "create"
STEP 4 Apply changes
IF tracker_backend == "notion":
FOR each create/update:
confirm_tags = filter suggested_tags to only values in existing Notion Tags options
IF interactive AND confirm_tags not empty:
prompt user to confirm / edit tag suggestions
creates → notion_create_pages(batch) with all fields incl. Interview date, Link, Tags
updates → notion_update_page per entry with status, Last touch, Next action,
Interview date (if extracted), Link (if extracted, don't overwrite existing),
Tags (merge with any already on page; add new confirmed ones only)
IF tracker_backend == "sqlite":
FOR each create:
sqlite3 DB_PATH "INSERT OR IGNORE INTO applications
(name,status,company,job_id,applied_on,last_touch,interview_date,next_action,link,tags)
VALUES (?,?,?,?,?,?,?,?,?,?)"
FOR each update:
sqlite3 DB_PATH "UPDATE applications SET status=?, last_touch=?,
next_action=?, interview_date=COALESCE(?,interview_date),
link=COALESCE(?,link), tags=COALESCE(?,tags),
updated_at=datetime('now') WHERE name=?"
STEP 5 Enrich page content (Notion only, when company is given OR when status changed)
FOR each application that was created or updated (and tracker is notion):
fetch full thread via gmail_get_thread(thread_id)
extract:
timeline[] = list of { date, event_summary } sorted chronologically
key_contacts[] = list of { name, email, role } from From/Cc headers
conversation[] = key quotes or summaries from each email in thread
prep_suggestions = generate 3-5 bullet prep suggestions based on status + role
Build structured page content:
## Timeline
| Date | Event |
...
## Key Contacts
| Name | Email | Role |
...
## Conversation Notes
[bullet summary of each email in thread]
## Preparation
[3-5 tailored bullet points based on company + role + stage]
notion_update_page(page_id, content=structured_content)
IF interview_date is not null AND Google Calendar MCP is available:
ask user: "Create a calendar event for <company> interview on <date>?"
IF yes:
google_calendar_create_event(
summary = "<company> — <role> Interview",
start = interview_date + "T09:00:00", # use extracted time if available
end = interview_date + "T10:00:00",
description = "Application tracked in 2026 Career Notion database."
)
STEP 6 Staleness detection
stale_threshold = today - 30 days
IF tracker_backend == "notion":
# all_pages already fetched and cached in Step 3 — no second fetch needed
stale = [p for p in all_pages
if p.status NOT IN ("Rejected", "Withdrawn", "Offer")
AND (p.last_touch < stale_threshold OR p.last_touch is null)
AND p.next_action != "Follow up"]
FOR each stale page:
notion_update_page(page_id, properties={"Next action": "Follow up"})
IF tracker_backend == "sqlite":
stale = sqlite3 DB_PATH "SELECT name, status, last_touch FROM applications
WHERE status NOT IN ('Rejected','Withdrawn','Offer')
AND (date(COALESCE(last_touch, updated_at)) < date('now','-30 days'))
AND COALESCE(next_action,'') != 'Follow up'"
FOR each stale row:
sqlite3 DB_PATH "UPDATE applications SET next_action='Follow up',
updated_at=datetime('now') WHERE name=?"
STEP 7 Report
IF no specific company was given (full sync):
IF tracker_backend == "notion":
counts = tally status values from all_pages (fetched in Step 6)
IF tracker_backend == "sqlite":
counts = sqlite3 DB_PATH "SELECT status, COUNT(*) FROM applications GROUP BY status"
print funnel summary line (see Report Format below)
print per-application summary (see Report Format below)
Triggered when the user says "re-enrich all pages", "add notes to all applications",
"update all pages with prep notes", or uses the --enrich-all flag.
Runs Step 5 enrichment (timeline, key contacts, conversation notes, prep suggestions) against all existing Notion pages, not just the ones touched in the current sync. Requires Notion backend. Skip gracefully if tracker_backend is sqlite (no page content).
text
INPUT: include_closed (default: false) — whether to enrich Rejected/Withdrawn pages
STEP R0 Load profile (same as main STEP 0)
STEP R1 Fetch all pages
all_pages = notion_fetch(COLLECTION_URL)
IF include_closed == false:
pages = [p for p in all_pages if p.status NOT IN ("Rejected", "Withdrawn")]
ELSE:
pages = all_pages
print: "Found {len(pages)} pages to enrich."
STEP R2 Match pages to Gmail threads
FOR each page in pages:
company_name = extract_company_from_name(page.name)
role_title = extract_role_from_name(page.name)
query = build_query(company_name) with date_range="newer_than:12m"
threads = gmail_search(query, max_results=5)
IF no threads found:
skip page; note in report as "no emails found"
CONTINUE
best_thread = most_recent_thread(threads)
full_thread = gmail_get_thread(best_thread.id)
STORE { page, full_thread }
STEP R3 Enrich each page (same as main STEP 5)
FOR each { page, full_thread }:
extract timeline, key_contacts, conversation notes, prep_suggestions
notion_update_page(page.id, content=structured_content)
IF page.interview_date is null:
interview_date = extract_interview_date(full_thread)
IF found: notion_update_page(page.id, properties={"Interview date": interview_date})
IF page.link is null:
link = extract_link(full_thread)
IF found: notion_update_page(page.id, properties={"Link": link})
STEP R4 Report
print:
Re-enriched: {N} pages
✦ {Company} — {Role} (+ interview date | + link | full notes)
Skipped (no emails): {M} pages
· {Company} — {Role}
Rate-limiting note: Enriching many pages will issue many API calls. Pause 1 second between Gmail thread fetches to avoid hitting rate limits. Process pages in batches of 10 and report progress after each batch.
text
FUNCTION build_query(company_key, date_range="newer_than:6m"):
cfg = COMPANY_CONFIG[company_key]
parts = []
IF cfg.sender: parts.append(f"from:{cfg.sender}")
IF cfg.label_id: parts.append(f"label:{cfg.label_id}")
subject_terms = 'subject:application OR subject:interview OR subject:offer OR subject:position OR subject:assessment OR subject:next steps'
RETURN f'({" OR ".join(parts)}) ({subject_terms}) {date_range}'
Examples:
(from:noreply@mail.amazon.jobs OR label:<label_id>) (subject:application OR ...) newer_than:6mfrom:@<company>.com or company name in subjectjson
{
"data_source_id": "<NOTION_DB_ID>",
"pages": [
{
"properties": {
"Name": "Amazon — SDE, AWS",
"status": "Applied / Received",
"Company": ["Amazon"],
"Tags": ["Referral"],
"Applied on": "2026-05-17",
"Last touch": "2026-05-17",
"Interview date": null,
"Next action": "Waiting",
"Link": "https://boards.greenhouse.io/amazon/jobs/12345"
},
"content": "Applied: 2026-05-17\n\n## Timeline\n| Date | Event |\n|---|---|\n| 2026-05-17 | Application submitted |\n\n## Preparation\n- Research AWS products and services\n- Practice system design at scale"
}
]
}
json
{
"page_id": "<page-id>",
"command": "update_properties",
"properties": {
"status": "Interviewing",
"Last touch": "2026-05-28",
"Interview date": "2026-06-09",
"Next action": "Prep"
},
"content_updates": []
}
json
{
"page_id": "<page-id>",
"command": "update_properties",
"properties": {
"Next action": "Follow up"
},
"content_updates": []
}
json
{
"page_id": "<page-id>",
"command": "update_properties",
"properties": {
"status": "Interviewing",
"Last touch": "2026-05-28",
"Next action": "Prep"
},
"content": "## Timeline\n| Date | Event |\n|---|---|\n| 2026-05-01 | Application submitted |\n| 2026-05-20 | Recruiter reached out |\n| 2026-05-28 | Interview scheduled for June 9 |\n\n## Key Contacts\n| Name | Email | Role |\n|---|---|---|\n| Jane Smith | jane@company.com | Recruiter |\n\n## Conversation Notes\n- 2026-05-28: Email from Jane Smith scheduling a technical interview for June 9 at 2pm PT.\n\n## Preparation\n- Review data structures and algorithms\n- Prepare STAR stories for behavioral questions\n- Research company mission and recent products\n- Practice whiteboard-style coding problems\n- Prepare 3–5 questions to ask the interviewer"
}
json
{
"query": "Amazon — Software Development Engineer",
"data_source_url": "collection://<NOTION_DB_ID>",
"filters": {}
}
bash
swelist tracker add "Amazon — SDE, AWS" \
--status "Applied / Received" \
--job-id 10414382 \
--applied-on 2026-05-17 \
--db <DB_PATH>
bash
swelist tracker update "Amazon — SDE, AWS" \
--status "Interviewing" \
--db <DB_PATH>
bash
swelist tracker get "Amazon — SDE, AWS" --db <DB_PATH>
# Returns JSON object if found, null if not found. Exit code 1 when not found.
bash
swelist tracker list --db <DB_PATH>
swelist tracker export --format json --db <DB_PATH>
bash
sqlite3 ~/.offerplus/applications.db \
"SELECT status, COUNT(*) AS n FROM applications GROUP BY status ORDER BY n DESC;"
bash
sqlite3 ~/.offerplus/applications.db \
"SELECT name, status, COALESCE(last_touch, updated_at) AS last_activity
FROM applications
WHERE status NOT IN ('Rejected','Withdrawn','Offer')
AND date(COALESCE(last_touch, updated_at)) < date('now','-30 days')
ORDER BY last_activity ASC;"
json
{
"summary": "Applied Intuition — Software Engineer Interview",
"start": { "dateTime": "2026-06-09T14:00:00", "timeZone": "America/Los_Angeles" },
"end": { "dateTime": "2026-06-09T15:00:00", "timeZone": "America/Los_Angeles" },
"description": "Application tracked in 2026 Career Notion database."
}
text
"{Company} — {Role Title}"
GoodBadAmazon — Software Development Engineer, AWS``SDE at Amazon``Google — Software Engineer II, Early Career``Google SWE``Goldman Sachs — Software Engineer - Associate``Goldman
Rules:
—), not hyphentext
Pipeline: {N} Applied · {N} OA · {N} Interviewing · {N} Offer · {N} Rejected
(omit pipeline line when syncing a single company)
Synced {N} application(s) on {date} [{backend}: notion|sqlite]
Created:
✦ {Company} — {Role} → {status} [next: {next_action}]
🔗 {link} (omit if not found)
🏷 Tags: {tag1}, {tag2} (omit if none inferred)
Updated:
↑ {Company} — {Role} → {new_status} (was: {old_status}) [next: {next_action}]
+ Page enriched: timeline, key contacts, prep notes added
📅 Interview date: {date} (calendar event created / skipped)
🔗 Link extracted: {link} (omit if not found)
🏷 Tags added: {tag1}, {tag2} (omit if none)
Skipped (already up to date):
· {Company} — {Role} → {status}
Flagged as stale (no activity > 30 days → Follow up):
⏰ {Company} — {Role} [last touch: {date}]
Merged (fuzzy company match confirmed):
⟳ "{extracted}" → matched to "{existing}" [edit distance: {n}]
Errors:
✕ {description of issue}
SituationHandlingMultiple threads for same roleUse most recent thread's statusRole title not in emailUse job ID or "Role" as placeholder; note it in content/notesCompany exists under a different name variantSearch by company name alone first, prompt user to confirm mergeEmail snippet enough to determine statusSkip full thread fetch for status; still fetch for page enrichment when company is givenCompany or Tags value not in Notion options listOmit the field; do not create new options automaticallyPage name collision (same company, same title, different role)Add disambiguator: Amazon — SDE, AWS (Job ID 12345)Notion DB ID or schema unknownFetch database URL with notion-fetch to inspect schema firstNotion DB uses old 4-status schemaMap pipeline status to old values (see Config: Notion Database), notify user to upgradeSQLite DB does not existRun swelist tracker init or the CREATE TABLE statement before Step 3SQLite DB path missing from profile.mdUse default ~/.offerplus/applications.db; confirm with userStatus would move backwards (e.g. Interviewing → Applied)Keep existing status; only update Last touch dateKey contacts not extractable from threadOmit key contacts section; do not hallucinate names or emailsNo prep suggestions applicableOmit Preparation section rather than generating generic adviceInterview date ambiguous (e.g. "sometime next week")Set interview_date to null; note ambiguity in conversation notesInterview date already in the pastStill write it to the page; do not create a calendar event for past datesGoogle Calendar MCP not connectedSkip calendar event creation; note it in the reportSQLite DB exists but missing new columns (v0.2.x)Run migration SQL automatically at Step 0 before proceedingStaleness check finds 0 stale rowsOmit the stale section from the report entirelynext_action already "Follow up"Skip staleness update for that row; it's already flaggedATS URL found but already saved in Link propertyDo not overwrite existing value; preserve what was thereMultiple ATS URLs in one email (e.g. Greenhouse + LinkedIn)Use highest-priority match per Link Extraction tableURL contains tracking params ( ?gh_src=, ?utm_*)Strip before saving; store clean canonical URL onlyInferred tag not in Notion Tags options listSilently omit; never create new Notion optionsInferred tag is already on the pageSkip; do not duplicateFunnel counts fetched from Notion but all_pages is emptySkip pipeline line; note "0 applications tracked"Funnel query when syncing a single companySkip pipeline summary entirelyFuzzy match finds a candidate but user declines mergeTreat as new company; create new row; do not add aliasFuzzy match distance is 1 (e.g. "Gogle" vs "Google")Still prompt user — never auto-merge without confirmationTwo existing rows both match fuzzily (ambiguous)Show top 2 candidates; let user pick or decline allAlias resolved but role title doesn't matchDo not merge — same company, different role is a different applicationBulk re-enrichment on SQLite backendNot supported; print "Page enrichment requires Notion backend" and exitBulk re-enrichment: page has no matching Gmail threadSkip; add to "no emails found" section of reportBulk re-enrichment: page already has rich contentOverwrite with fresh extraction — user triggered this explicitlyBulk re-enrichment: more than 50 pages to enrichWarn user of expected time (~2 min per 10 pages); ask to confirm before proceeding