# Triage Workflow Batch-process job alert emails into a living Job Triage document. **Trigger:** "evaluate the next N mails", "triage jobs", "batch analyze", "check job alerts" --- ## Prerequisites 1. **Switch SL user**: Call `sl_switch_user(name: "matthias")` before any SL API calls 2. **Load preferences**: Call `sl_get_preferences()` for evaluation criteria 3. **Determine triage file**: `Notes/YYYY/MM/Job Triage.md` (current month). Create if missing. --- ## Step 1: Find Job Alert Emails Search Gmail for recent LinkedIn job alerts (or other sources as specified): ``` gmail search: from:linkedin.com newer_than:Nd in:inbox subject:job ``` - Use `search_gmail_messages` to get message IDs - Default: last 7 days unless user specifies otherwise - Identify which emails are job alert digests (vs. connection requests, notifications) **IMPORTANT:** Do NOT rely on subjects or metadata snippets. Proceed to Step 2 immediately for full body reads. --- ## Step 2: Read Full Email Bodies and Extract ALL Job IDs ### MANDATORY: Full content read for every email For EVERY job alert email identified in Step 1, call `get_gmail_message_content(message_id)` to read the COMPLETE email body. Never skip this step. Never rely on metadata, snippet, or subject line alone. ### Extract ALL LinkedIn job URLs Parse the full body text for every URL matching this pattern: ``` linkedin.com/jobs/view/XXXXXXXXXX ``` Extract every single job ID found. Do not stop at the first match. Do not skip URLs that look similar to others already found. ### Per-email manifest (REQUIRED) Before moving on, record a manifest entry for every email processed: ``` Email: () Message ID: Jobs extracted: Job IDs: ``` Log this to the session notes or a scratch variable. You will use it for the reconciliation report in Step 8. ### Quality gate - If an email yields 0 extracted job IDs, flag it explicitly as a PARSING FAILURE - do not silently skip it - LinkedIn job alert emails typically contain 5-15 job listings; fewer than 3 may indicate extraction failed - Re-read the raw body if extraction seems low ### Build the master job list After processing ALL emails, deduplicate by job ID. The master list is your working set for Steps 3-6. --- ## Step 3: Dedup Against Existing Triage Before evaluating any job, check each job ID (or company + title) against the current month's Job Triage file: - If the job is already in the Active Pipeline or Dismissed table, skip it - Note how many were skipped as duplicates for the reconciliation report This prevents re-evaluating jobs already processed in a previous session. --- ## Step 4: Quick Triage (Title/Location Filter) Before scraping full JDs, do a fast pass to eliminate obvious mismatches: **Auto-reject (score 1/5, Triage: No):** - Wrong country (US, UK, etc. - unless remote with CH contract) - Pure developer/engineer roles (frontend, backend, full-stack, embedded, ML engineer) - Academic positions (postdoc, PhD, graduate assistant) - Apprenticeships, internships, stagiaire - Hardware/electronics/mechanical engineering - Roles clearly below senior level - Salary explicitly below CHF 150k **Keep for full evaluation (score TBD):** - IT leadership, management, director, head of - Consulting, advisory, business analyst (senior) - Program/project management (IT/digital) - Product management (tech companies) - Architecture, strategy, transformation - Suisse romande or remote-CH --- ## Step 5: Scrape Full JDs (Parallel) For jobs that pass the quick triage (scored >= 2/5): **Scraping priority order:** 1. **Scribe** `extract_content` - try first (fast, no browser needed) 2. **Playwright** - if Scribe returns 403/empty (required for Glassdoor, some LinkedIn) - `browser_navigate` to URL - `browser_evaluate` with appropriate JS selector: - Glassdoor: `document.querySelector('[class*="JobDetails_jobDescription"]').innerText.substring(0,5000)` - LinkedIn: `document.querySelector('main').innerText.substring(0,5000)` 3. **sl_scrape_job** - for supported job sites (job-room.ch, LinkedIn) Launch parallel agents for scraping when possible (one per URL). --- ## Step 6: Evaluate Each Job For each scraped JD, evaluate against the user's profile: **Scoring criteria (from preferences):** - IT leadership / consulting alignment (weight: high) - Seniority level (must be senior/director/head) - Domain fit (IT, digital transformation, ERP, data, cloud) - Location (Suisse romande preferred, hybrid/remote OK) - Salary (floor CHF 150k) - Company type (consulting engagement vs permanent - note which) - Language requirements (FR/EN/DE - all OK) **Score scale:** - **5/5**: Perfect match - IT leadership, right location, right level - **4/5**: Strong match with minor gap (e.g., slightly different domain) - **3.5/5**: Good match, worth pursuing (consulting roles, senior scope) - **3/5**: Possible match, needs closer look - **2/5**: Partial match, significant gaps - **1/5**: Clear mismatch (wrong domain, level, location, or role type) **BatchAnalyze threshold:** Only create jobs in SeriousLetter for scores >= 3/5. --- ## Step 7: Update Job Triage File ### File Structure The Job Triage file (`Notes/YYYY/MM/Job Triage.md`) has this structure: ```markdown # Job Triage Living document. Jobs flow through here: new batch -> triage -> pursue or dismiss. - **Pursue**: detail section gets fleshed out, moves to own 0XXX folder when applied - **Dismiss**: row removed (or moved to Dismissed section at bottom) - **Goal**: always shows what's active right now --- ## Active Pipeline | # | Company | Title | Location | Score | Triage | Notes | | --- | ------------------------------------- | ------------------------------ | --------- | ----- | ------ | ------------------ | | 1 | [[#Company Name\|Company Name]] | Job Title Here | City | 3/5 | No | Brief reason | --- ## Completed | # | Company | Title | Outcome | Folder | |---|---------|-------|---------|--------| --- ## Dismissed | # | Company | Title | Reason | |---|---------|-------|--------| --- ## Detail Sections --- ### Company Name **Location:** City | **Salary:** Not listed | **Score:** X/5 **Source:** [Glassdoor](https://...) **Why this fits:** Explanation of alignment with profile... **Gaps/concerns:** What doesn't match or needs investigation... **Next step:** Scrape full JD / Create in SL / Skip ``` ### Table Row Format Each job gets a numbered row in the Active Pipeline table: - **#**: Sequential within the file (1, 2, 3...) - **Company**: Obsidian wiki-link to detail section: `[[#Company Name\|Company Name]]` - If company appears multiple times, use a unique anchor: `[[#Company-suffix\|Company]]` - **Title**: Job title (keep concise, max ~50 chars) - **Location**: City or region - **Score**: X/5 rating - **Triage**: "Yes" (pursue), "No" (skip), or empty (not yet evaluated) - **Notes**: 2-4 word reason for the score/decision ### Detail Section Format Each job gets a detail section below `## Detail Sections`: ```markdown --- ### Company Name **Location:** City | **Salary:** CHF XXXk or "Not listed" | **Score:** X/5 **Source:** [Site Name](URL) **Why this fits:** 2-3 sentences on alignment with IT leadership/consulting profile. **Gaps/concerns:** 1-2 sentences on mismatches or unknowns. **Next step:** What to do next (Scrape full JD / Create in SeriousLetter / Skip) ``` For jobs scoring >= 3/5 with full JD scraped, add more detail: ```markdown --- ### Company Name **Location:** City (hybrid/remote) | **Salary:** CHF XXXk | **Score:** X/5 | **Triage:** Yes **Source:** [Site Name](URL) **Type:** Permanent / Consulting engagement **Full JD summary:** 2-3 sentence summary of the role, responsibilities, requirements. **Why this fits:** - Bullet point 1 - Bullet point 2 - Bullet point 3 **Gaps/concerns:** - Bullet point 1 - Bullet point 2 **Next step:** Create in SeriousLetter, write cover letter, apply ``` ### Creating a New Triage File If `Notes/YYYY/MM/Job Triage.md` doesn't exist for the current month: 1. Create the file with the full template (header, empty Active Pipeline table, empty Completed/Dismissed, Detail Sections header) 2. Start numbering from 1 ### Appending to Existing Triage File If the file already exists: 1. Read the current file 2. Find the highest # in the Active Pipeline table 3. Append new rows starting from max + 1 4. Append new detail sections at the bottom (before the last `---` or at end) 5. Do NOT renumber existing rows 6. Do NOT modify existing detail sections --- ## Step 8: Archive Processed Emails Archive each Gmail job alert email ONLY AFTER all jobs from that email have been extracted and recorded in the manifest. Do not archive early. - Remove INBOX label - Add Jobs/* label (e.g., Jobs/01 - Pipeline) --- ## Step 9: Summary and Reconciliation Report Present a summary table to the user: ``` | # | Company | Position | Fit | Action | |---|---------|----------|-----|--------| | 1 | Acme | CTO | 4/5 | Create in SL | | 2 | BigCo | Dev Lead | 2/5 | Skip | ``` Then present the per-email manifest so the user can spot any gaps: ``` Email manifest: - "Your job alert for CTO roles" (2026-03-09): 8 jobs extracted - "Weekly digest: IT leadership" (2026-03-08): 12 jobs extracted - "New matches for you" (2026-03-07): 0 jobs extracted *** PARSING FAILURE - review manually *** ``` Reconciliation totals: - Total emails processed: N - Total job IDs extracted (before dedup): N - Duplicate / already-known: N - New jobs evaluated this session: N - Jobs worth pursuing (>= 3/5): N - Jobs skipped (< 3/5): N - Scraping failures: N - Parsing failures (0 jobs from non-empty email): N (flag each one) **Extraction rate check:** If (total jobs extracted / emails processed) < 3, warn the user that extraction may have failed on one or more emails. --- ## Important Rules - **NEVER use em-dashes** in any generated content - use hyphens with spaces - **ALWAYS read full email body**: Use `get_gmail_message_content`, never rely on metadata/snippet alone - **ALWAYS extract ALL job IDs**: Parse every `linkedin.com/jobs/view/XXXXXXXXXX` URL from the full body - **ALWAYS produce per-email manifest**: Record subject, date, message ID, and count of jobs extracted per email - **ALWAYS dedup first**: Cross-check against existing triage file before evaluating - **ARCHIVE AFTER extraction**: Never label/archive an email before all its jobs are recorded - **Obsidian links**: Always use `[[#Heading\|Display]]` format in tables (standard markdown anchors don't work in Obsidian tables) - **Parallel scraping**: Launch multiple Playwright agents when scraping 3+ URLs - **Gmail archiving**: Always remove INBOX label when adding Jobs/* labels - **File is append-only**: Never remove or modify existing entries unless user explicitly asks --- **Last Updated:** 2026-03-09