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 ```

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: -separated list of LinkedIn 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