# Job Search Memory ## CRITICAL RULES (CHECK EVERY TIME) - **NEVER use em-dash (—) in ANY generated content** - cover letters, notes, emails, anything. Use a hyphen surrounded by spaces ( - ), a comma, or rephrase the sentence. "Says an old TeXie." This applies to ALL languages. - **FRENCH ACCENTS ARE MANDATORY**: When writing ANY French text (cover letters, notes, anything), ALWAYS use proper diacritics: é, è, ê, ë, à, â, ù, û, ô, î, ï, ç, etc. NEVER write "equipe" - write "équipe". NEVER write "experience" - write "expérience". This has been a REPEATED failure. Double-check every French word before saving. - **NEVER click Submit/Apply without asking the user first** - always pause at the review/confirmation page. NEVER pull the trigger on application sites without user reviewing first. - **NEVER send emails — ALWAYS draft only** - Use Gmail draft (not send) for ALL emails: applications, inquiries, support requests, everything. User reviews and sends manually. - **Prefer email route over job site application** - When a job listing provides both an email contact and an online application form, prefer drafting an email application over filling the web form. - **Contact person field ≠ email recipient**: The `contact_person` field on a job may contain multiple names, roles, and annotations (e.g. "Antoine Wormser (email@x.ch), Katrin Lüthy (job poster)"). When drafting emails, extract ONLY the relevant email address and name — never dump the raw field into the To: line. - **EMAIL ALIASES**: All mail (mnott@mnsoft.org, mnott@mnott.ch, etc.) delivers to mnott@mnott.de. Use coogle with mnott@mnott.de for ALL Gmail searches. The claude_ai_Gmail tool also works but prefer coogle. - **SeriousLetter job creation workflow**: (a) Search companies DB (`GET /api/v1/companies?q=`) - if found use it, if not create it. (b) Create/update job with `company_uuid` (pulls address automatically), full JD in `job_description`, and `priority` for the fit/star rating. NEVER leave job_description empty, NEVER manually copy address fields when company_uuid is available. - **CV selection for jobs**: Choose the right CV profile based on role type: **corporate/permanent → regular CV**, **consulting engagement → consultant version**. After copying to the job, user activates the correct language version of the exec summary in the UI. For consulting roles, also activate the consulting portfolio if relevant. **Always pull CV data from the job-specific CV** (GET /api/v1/jobs/{uuid}/cvs), not the base profile — it reflects the user's curation choices. - **Letters API uses PUT, not PATCH** - PATCH returns "Method Not Allowed". Always use PUT for /api/v1/letters/{id} - **MCP CONFIG LOCATION**: User-level MCP servers go in `~/.claude.json` (mcpServers key), NOT `~/.claude/.mcp.json`. The `.mcp.json` is a stale/duplicate file. Also add to `enabledMcpjsonServers` in `~/.claude/settings.json` and `mcp__` to `permissions.allow`. This has been a REPEATED mistake. - **1PASSWORD KILLS PLAYWRIGHT** - 1PW extension intercepts email fields, crashes CDP connection ("Cannot access chrome-extension://"). FIX: In browser_run_code, ALWAYS disable 1PW on ALL inputs FIRST, then use native value setters (NOT fill()): ```js // Step 1: Disable 1PW await page.evaluate(() => { document.querySelectorAll('input, textarea').forEach(el => { el.setAttribute('data-1p-ignore', 'true'); el.setAttribute('autocomplete', 'off'); }); }); // Step 2: Fill via native setter (not fill()) await field.evaluate((el) => { const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; setter.call(el, 'value'); el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }); ``` This applies to ALL sites, not just Sword Services. NEVER use Playwright fill() on email fields. - **NEVER use browser_fill_form with multiple fields** - it invalidates refs after the first field (always fails on e89). Use individual browser_type/browser_click calls instead, with browser_snapshot between if needed - **NEVER include #jobs or other Obsidian tags in cover letter content** sent to SeriousLetter API - tags belong in local .md files only - **COVER LETTER FORMAT** (NEVER deviate): ``` ## Position Title (Ref: XXX) Dear ..., [body paragraphs] Matthias Nott ``` - Subject line (Betreff) as H2 heading BEFORE the salutation - Two empty lines between subject line and "Dear..." - NEVER include address, phone, email, or LinkedIn at the bottom - those are in the letterhead template - Signature is ONLY "Matthias Nott" - nothing else - **Letter API field names**: POST uses `content`, PUT uses `final_content` - **is_recruiting_agency**: ALWAYS check if a company is a recruitment firm (JD says "on behalf of our client" etc.) and set `is_recruiting_agency: true` on BOTH the company AND the job - **Switzerland since 2004** (not 2003) - always use "depuis 2004" / "since 2004" / "seit 2004" - **Matthias is SWISS** - Nationality is SUISSE / Swiss / Schweizerisch. NEVER put "Allemande" or "German". He is a Swiss citizen. - **NEVER probe job-room.ch API endpoints directly** - The WAF detects API probing (curl to api.job-room.ch, fetching /v3/api-docs, etc.) and IP-bans immediately. Playwright for normal form filling is fine. To understand internal XHR calls, user must capture manually via Chrome DevTools - never probe programmatically. - **ORP internal API fully reverse-engineered** - see `memory/orp-internal-api.md` for complete endpoint map, payload schema, and auth flow. Extracted from Angular bundles via Wayback Machine. - **ORP API: curl returns 401 — use browser-context fetch() only** - The session is bound to browser internals. Run `page.evaluate(async () => fetch(...))` inside the Playwright session instead. Token comes from `sessionStorage.getItem('authenticationToken')`. The `_ng=ZGU=` parameter is just `base64("de")`, NOT a CSRF token. - **ORP working endpoints (confirmed 201)**: `POST /onlineform-service/api/npa/_action/add-work-effort?userId={userId}&_ng=ZGU=` and `GET /onlineform-service/api/npa/_search/by-owner-user-id?userId={userId}&_ng=ZGU=` - **BatchAnalyze threshold**: Only create jobs scoring >= 3/5 in SeriousLetter. 2/5 and below = skip entirely, don't bother recording. - **IC roles are valid targets**: Do NOT auto-skip individual contributor roles. Evaluate IC roles like Principal Engineer, Staff/Senior Architect, Solution Architect, Lead Data Scientist, Senior AI Engineer, Field CTO if they meet the CHF 150k floor and are at senior+ level. Only skip IC roles that are clearly junior or purely coding-focused. - **ORP updates: NEVER ASK, just do it** - When updating job status (rejection, applied, etc.), ALWAYS update ORP (job-room.ch) automatically without asking for confirmation. This applies to all status changes: Absage, Anstellung, applied sync, etc. - **PDF filename convention** - Name first, type last: `Matthias_Nott_Chef_Projet_IT_Geneva_CV.pdf` and `Matthias_Nott_Chef_Projet_IT_Geneva_Coverletter.pdf`. NEVER type-first like "CV_Matthias..." or generic like "Letter_381.pdf" - **LinkedIn full JD extraction**: sl_scrape_job, scribe extract_content, and webfetch ALL fail (truncated meta desc or HTTP 999). Only Playwright with logged-in Chrome session works. Use `browser_navigate` then `browser_evaluate(() => document.querySelector('main').innerText.substring(0,5000))` to extract. - **LinkedIn BatchAnalyze workflow**: Gmail search `from:linkedin.com newer_than:Nd in:inbox` → `get_gmail_messages_content_batch(metadata)` to identify job alerts → extract job IDs from URLs → triage by title/seniority/location (skip obvious mismatches) → Playwright for full JDs of >= 3/5 candidates → evaluate → create in SL - **JOB NOTE DIRECTORY CONVENTION** (REPEATED failure - CHECK EVERY TIME): - Path: `Notes/2026/MM/NNNN - YYYY-MM-DD - Position at Company/` - Inside: `NNNN - YYYY-MM-DD - Position at Company.md` + `artifacts/` subfolder - **NNNN is sequential 4-digit number** - to find next number: `glob Notes/2026/*/0*` and take max + 1 - Example: if last is `0209`, next is `0210` - NEVER create flat files or wrong numbering - ALWAYS glob first to find the correct next number ## Personal Details for Applications - Name: Matthias Nott - Email for applications: mnott@mnott.ch (NOT mnott@mnott.de) - Address: Chemin de la Tarpa 8a, 1872 Troistorrents, Switzerland - Phone: +41 76 450 26 98 - Country Phone Code: Switzerland (+41) - Phone Number (without code): 764502698 - Nationality: **Suisse / Swiss / Schweizerisch** - NEVER "Allemande" or "German" - Work permit: Citoyen.ne Suisse (NOT Livret C) - LinkedIn: https://www.linkedin.com/in/mnott/ (always include) - Facebook, X/Twitter, Website fields: NEVER fill these ## SeriousLetter External API - **FULL API SCHEMA cached at:** `memory/seriousletter-api.md` - READ THIS instead of calling /api/v1/discover. NEVER call discover when this file exists. - Prod server: jobs.seriousletter.com (deployed 2026-02-26) - Dev server: dev.jobs.seriousletter.com - Code: seriousletter.com:/opt/data/seriousletter/dev/code (develop branch) - Feature module: backend/features/external_api/ - Frontend: src/features/settings/ (token management UI at /settings) - API token auth via X-API-Token header - Prod API token: 9955f8699964b70eee3b4acf8b7ca5cf7f6997ddb23d483f1c5f178754f11196 - Dev API token: d6b3f193ce94f4d42b92dab708c06cb050cf861fdf35050a7f471a8ada1edf2b - Int API token: b1812a08070f2759f804f433bf657881f72432ab23c11ffe1394c07432f45e4e - Discovery endpoint: GET /api/v1/discover (returns full API schema with safety annotations for AI agents) - Endpoints: GET /api/v1/jobs, GET /api/v1/jobs/search?q=, GET /api/v1/jobs/{uuid}, GET /api/v1/jobs/{uuid}/arbeit - **Notes CRUD (new structured API):** - GET /api/v1/jobs/{uuid}/notes - list all notes (newest first) - POST /api/v1/jobs/{uuid}/notes - create: {"text": "...", "category": "application|interview|rejection|status|general", "note_date": "YYYY-MM-DD"} - PATCH /api/v1/jobs/{uuid}/notes/{note_id} - update fields - DELETE /api/v1/jobs/{uuid}/notes/{note_id} - remove note - Legacy format still works: {"note": "...", "date": "YYYY-MM-DD"} - **ALWAYS separate note entries with an empty line between them** - **Letter management: GET /api/v1/jobs/{uuid}/letters (list letters for a job), GET /api/v1/letters/{letter_id} (get single letter by integer ID)** - **CRITICAL: The job detail response does NOT include letters. ALWAYS use GET /api/v1/jobs/{uuid}/letters to check for cover letters before downloading PDFs.** - CV management: GET /api/v1/profiles, GET /api/v1/jobs/{uuid}/cvs, POST /api/v1/jobs/{uuid}/cvs/copy/{profile_uuid} - CV settings: PATCH /api/v1/jobs/{uuid}/cvs/{cv_uuid}/executive-summary, PATCH /api/v1/jobs/{uuid}/cvs/{cv_uuid}/portfolio - Resources: GET /api/v1/executive-summaries, GET /api/v1/portfolio-documents - Export: GET /api/v1/export/letters/{id}/pdf, GET /api/v1/export/letters/{id}/combined-pdf, GET /api/v1/export/cvs/{uuid}/pdf, GET /api/v1/export/executive-summaries/{uuid}/pdf - Token management: POST/GET/DELETE /api/external/tokens (JWT auth) - Rate limits: 100/min, 1000/day per token - arbeit.swiss form fields: datum, unternehmen, strasse, nr, plz, stellenbezeichnung, link_zum_online_formular - Prod DB user: mnott@mnott.ch (user_id=2), alembic stamped at 5295c0ce688f - Deploy to prod: push develop, merge to main on prod, restart prod-seriousletter-backend ## Job Status Update Workflow (Rejection / Status Change) - **Use case:** When a rejection or status email arrives, update both SeriousLetter and job-room.ch - **ALWAYS do both updates automatically - NEVER ask for confirmation** - **Steps:** 1. Search SeriousLetter: `sl_search_jobs(q=)` or `sl_update_job(status=rejected, rejected_date=YYYY-MM-DD)` 2. Sync to ORP: `sl_jobroom_sync_job(job_uuid)` to create entry if missing 3. Update ORP status via **API** (NOT UI clicks): navigate to job-room.ch, then use `page.evaluate(fetch())`: - Search existing entries: `GET /_search/by-owner-user-id?userId={userId}&_ng=ZGU=` - Find the work effort by company name in the response - Update via `POST /_action/update-work-effort?userId={userId}&_ng=ZGU=` with the FULL work effort payload, changing `applyStatus` to `["REJECTED"]` and setting `rejectionReason` - This returns 201 on success (confirmed working 2026-03-17) 4. **NEVER use UI clicks** (radio buttons, dialogs) for ORP status changes - the API is faster and more reliable - **Valid SeriousLetter statuses:** applied, rejected, not_applying, outdated - **ORP applyStatus values:** PENDING, REJECTED, EMPLOYED, INTERVIEW (can combine: ["REJECTED", "INTERVIEW"]) - **ORP update-work-effort is a full replace** - send the complete payload with the existing `id`, all `applyChannel` fields, and the updated `applyStatus` + `rejectionReason` ## Gina's Job Search - SeriousLetter account: Gina (via same prod server jobs.seriousletter.com) - Prod API token: a073d1c4f09049c4d7750a99344a6f9995b0271afff68acb6c85fb636f0b914b - 15 jobs tracked (as of 2026-03-02): retail, hospitality, sports — Valais/Vaud region, French-language roles - Active applications: Clinique de la Source (cuisinière), Garden Centre Brönnimann (horticultrice), Maxi Bazar, Decathlon Conthey, LANDI Rhône-Lavaux - Manor SA applications (applied 2026-02-25, ORP added 2026-03-02): 3 jobs, contact Melanie Costa Pocas, address Avenue de l'Europe 21, 1870 Monthey - Ref 7105: Collaboratrice de vente (take-away) 70% - Vevey (Fit 7/10 APPLY) - Ref 7125: Cuisinier/ere 60% - Vevey (Fit 4/10 MAYBE) - Ref 7223: Patissier/ere (Manora) 60% - Monthey (Fit 5/10 MAYBE-APPLY) - Same API endpoints and workflows as Matthias's account, different token ## ORP (job-room.ch) Form Automation - **Primary**: Internal API via `page.evaluate(fetch(...))` - ALWAYS use this, NEVER click UI - **For new entries**: `POST /_action/add-work-effort` (or use `sl_jobroom_sync_job` MCP tool) - **For status updates**: `POST /_action/update-work-effort` with full payload + updated `applyStatus`/`rejectionReason` - **For searching**: `GET /_search/by-owner-user-id?userId={userId}&_ng=ZGU=` returns `{content: [{workEfforts: [...]}]}` - **Auth**: `sessionStorage.getItem('authenticationToken')` + `X-Requested-With: XMLHttpRequest` header - **UI clicking is DEPRECATED** - only use as absolute last resort if API fails - URL for new entry (fallback only): https://www.job-room.ch/work-efforts/create - PLZ/Ort combobox REQUIRES `browser_type` with `slowly: true` — `fill()` does not trigger autocomplete dropdown; after typing, click the option from the listbox - Use `browser_fill_form` for all other fields (textbox, checkbox, radio) in a single call for efficiency - Stellenbezeichnung: include job percentage and ref number, e.g. "Patissier/ere (Manora) 60% (Ref. 7223)" (max 100 chars) - After save: dialog shows "Arbeitsbemühung gespeichert" with "Weitere erfassen" and "Zur Übersicht" buttons — ALWAYS click "Zur Übersicht" to return to overview (or navigate to /work-efforts via JXA if dialog lost) - **Foreign country addresses**: When Land is changed from Schweiz, the PLZ/Ort combobox is replaced by separate PLZ (textbox) and Ort (textbox) fields — no autocomplete needed, just fill directly - Foreign PLZ field ID: `alv-input-field-home.tools.job-publication.locality.zip-0`, Ort field ID: `alv-input-field-global.address.city-0` - Fields that reset between "Weitere erfassen" entries: PLZ/Ort, Stellenbezeichnung, Link zum Online-Formular, Kontaktperson - Fields that retain values: Wie (Elektronisch), RAV (Nein), Pensum, Ergebnis - Manor SA address used for Grazyna's applications: Avenue de l'Europe 21, 1870 Monthey; contact: Melanie Costa Pocas - **macOS Automator fallback for ORP** (when Playwright MCP crashes): - Field IDs follow pattern: `alv-date-input-*-0`, `alv-input-field-*-0`, `alv-single-typeahead-*-0` - Key IDs: `alv-date-input-portal.global.date-0` (datum), `alv-input-field-home.tools.job-publication.company.name-0` (company), `alv-input-field-home.tools.job-publication.company.street-0` (street), `alv-input-field-home.tools.job-publication.company.house-number-0` (house nr), `alv-input-field-portal.global.job-title-0` (title), `alv-input-field-portal.work-efforts.edit-form.company.online-form-url.label-0` (URL) - Use React native setter for text fields: `Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(el,val)` + dispatch input+change - PLZ/Ort combobox: JS focus on the typeahead input, then System Events slow typing + keyCode 125 (down) + keyCode 36 (enter) - Checkboxes (Elektronisch, Nein): standard `document.getElementById(id).click()` works - "Speichern" button: `document.querySelector('button.btn-primary')` or find by "Speichern" text - "Zur Übersicht" button after save: find button containing "Zur Übersicht" text in the success dialog ## Application Form Automation (Phase 2) - **NEVER click Submit/Apply without asking the user first** — always pause at the review/confirmation page and let the user decide when to submit - DOB for Matthias: 14.05.1971 - Salary floor: NEVER below CHF 150'000 — research market rate, set range starting at 150k - ALWAYS use SeriousLetter API export endpoints for PDFs — NEVER convert markdown to PDF - **Annexes directory**: `/Users/i052341/Daten/Cloud/09 - Job Search/06 - Annexes/` — Zeugnisse, Reference Letters. Already in Playwright sandbox. - **Per-ATS form patterns**: See Jobs skill `workflows/ATS.md` for Oracle HCM, Personio, SmartRecruiters, SuccessFactors specifics - The API renders with proper templates and reflects any UI edits the user made - Combined PDF: GET /api/v1/export/letters/{id}/combined-pdf — bundles cover letter + exec summary + CV + optional portfolio - **CV upload strategy based on form layout:** - Separate CV + CL uploads → upload each individually (CV PDF + letter PDF) - Only CV upload, no CL option → upload the COMBINED PDF instead of standalone CV - CV upload + CL textarea → upload COMBINED PDF as CV, AND paste cover letter as plain text (no markdown) into textarea - Decision can only be made after seeing the full form (CL options may appear on later screens — scout first) - Always use the job-specific CV (from /api/v1/jobs/{uuid}/cvs), not the profile CV - API now has default template settings — no need to pass template params for CV or letter exports - German letter template is a layout choice, not a language choice — always use German layout - GET /api/v1/jobs/{uuid} may NOT include letters in response — discover letter IDs by checking nearby IDs via export endpoint, or ask user - When user says they edited a letter in the UI, the markdown file is OUT OF SYNC — do NOT re-upload from markdown - Arbeitszeugnis path: /Users/i052341/Daten/Cloud/06 - Studium/Zeugnisse/Zeugnisse - Henley - MSc - Liverpool - MBA.pdf (stable, don't copy each time) - For uploads: try the original path first. Only copy to artifacts/ if Playwright sandbox blocks it - Personio datepicker fields: use browser_click + browser_type(slowly=true), NOT browser_fill_form (causes disconnect) - Playwright recovery: if "browser context closed", use browser_close (may error) then browser_navigate to reconnect - Playwright MCP crashes with "Another browser context is being closed" or "Target page, context or browser has been closed" — user must reconnect MCP server manually - When Playwright MCP crashes mid-form, fall back to macOS Automator (JXA) immediately — don't retry Playwright - Playwright MCP uses separate Chrome with persistent profiles (no --extension mode, avoids PAI MCP Bridge conflicts) - Profile dirs: ~/.claude/playwright-profiles/matthias/ and ~/.claude/playwright-profiles/grazyna/ - To switch profiles for Grazyna: edit ~/.claude/.mcp.json, change user-data-dir path to grazyna's - First login to each site (job-room.ch etc.) needed once per profile, then persists - Config change requires Claude session restart to take effect ## ATS & Browser Automation Details - **See `memory/ats-automation.md`** for detailed per-ATS patterns (SmartRecruiters, Ashby, Sword, Workday, Playwright crash recovery, platform decision tree, JXA fallback patterns)