{"id":"e8940f4b-fead-4977-bd42-87c24bb3c289","entityType":"agent","slug":"clawhub-chenyuan99-job-application-manager","name":"Job Application Manager","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chenyuan99-job-application-manager","canonicalPath":"/agent/clawhub-chenyuan99-job-application-manager","generatedAt":"2026-10-11T05:32:08.659Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-11T02:49:59.557Z","emptyReason":null},"description":"Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations Skill: Job Application Manager Owner: chenyuan99 Summary: Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations Tags: latest:1.0.11 Version history: v1.0.11 | 2026-05-30T14:34:42.130Z | user Released via CI v1.0.10 | 2026-05-30T14:20:35.948Z | user Released via CI v1.0.9 | 2026-05-30T14:13:39.448Z | user Released via CI v1","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.2K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s17ej20pbgf1t4n63yvq27gzh183fzvq:job-application-manager","sourceUrl":"https://clawhub.ai/chenyuan99/job-application-manager","homepage":"https://clawhub.ai/chenyuan99/skills/job-application-manager","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chenyuan99/job-application-manager","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chenyuan99/skills/job-application-manager","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":61,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations Skill: Job"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T02:49:59.557Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T02:49:59.557Z","emptyReason":null},"stars":null,"forks":null,"downloads":1183,"packageName":null,"latestVersion":"1.0.11","tractionLabel":"1.2K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T02:49:59.542Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T02:49:59.557Z","lastCrawledAt":"2026-10-11T02:49:59.542Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T02:49:59.542Z","lastVerifiedAt":null,"highlights":[{"version":"1.0.11","createdAt":"2026-05-30T14:34:42.130Z","changelog":"Released via CI","fileCount":3,"zipByteSize":13820},{"version":"1.0.10","createdAt":"2026-05-30T14:20:35.948Z","changelog":"Released via CI","fileCount":3,"zipByteSize":11651},{"version":"1.0.9","createdAt":"2026-05-30T14:13:39.448Z","changelog":"Released via CI","fileCount":3,"zipByteSize":10007},{"version":"1.0.8","createdAt":"2026-05-21T20:00:53.657Z","changelog":"- Updated the skill description to clarify that statuses are updated in Notion or SQLite tracker, not just synced. - No changes to core workflow or configuration; documentation wording improved. - No functional changes to backend, triggers, or workflow logic.","fileCount":3,"zipByteSize":6614},{"version":"1.0.7","createdAt":"2026-05-21T19:57:39.307Z","changelog":"job-application-manager 1.0.7 - Updated documentation for clarity and conciseness in SKILL.md. - Improved the description and summary to better communicate the skill’s functionality and value. - No changes to core functionality or workflow logic. - Version number in SKILL.md remains 0.1.9.","fileCount":2,"zipByteSize":5443},{"version":"1.0.6","createdAt":"2026-05-21T19:51:28.838Z","changelog":"# Job Application Manager — Gmail & Notion Sync v1.0.6 - Updated skill name and description in SKILL.md for clarity and improved discoverability. - No functional changes; update focuses on documentation and metadata consistency.","fileCount":2,"zipByteSize":5446},{"version":"1.0.5","createdAt":"2026-05-21T19:48:24.023Z","changelog":"Released via CI","fileCount":2,"zipByteSize":5432},{"version":"1.0.4","createdAt":"2026-05-21T19:43:59.508Z","changelog":"Released via CI","fileCount":2,"zipByteSize":5147}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17ej20pbgf1t4n63yvq27gzh183fzvq:job-application-manager","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chenyuan99-job-application-manager/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chenyuan99-job-application-manager/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chenyuan99-job-application-manager/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chenyuan99-job-application-manager/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chenyuan99-job-application-manager/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chenyuan99-job-application-manager/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T05:32:08.655Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chenyuan99-job-application-manager/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chenyuan99-job-application-manager/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chenyuan99-job-application-manager/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chenyuan99-job-application-manager/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-11T02:49:59.557Z","emptyReason":null},"readme":"Skill: Job Application Manager\n\nOwner: chenyuan99\n\nSummary: Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations\n\nTags: latest:1.0.11\n\nVersion history:\n\nv1.0.11 | 2026-05-30T14:34:42.130Z | user\n\nReleased via CI\n\nv1.0.10 | 2026-05-30T14:20:35.948Z | user\n\nReleased via CI\n\nv1.0.9 | 2026-05-30T14:13:39.448Z | user\n\nReleased via CI\n\nv1.0.8 | 2026-05-21T20:00:53.657Z | auto\n\n- Updated the skill description to clarify that statuses are updated in Notion or SQLite tracker, not just synced.\n- No changes to core workflow or configuration; documentation wording improved.\n- No functional changes to backend, triggers, or workflow logic.\n\nv1.0.7 | 2026-05-21T19:57:39.307Z | auto\n\njob-application-manager 1.0.7\n\n- Updated documentation for clarity and conciseness in SKILL.md.\n- Improved the description and summary to better communicate the skill’s functionality and value.\n- No changes to core functionality or workflow logic.\n- Version number in SKILL.md remains 0.1.9.\n\nv1.0.6 | 2026-05-21T19:51:28.838Z | auto\n\n# Job Application Manager — Gmail & Notion Sync v1.0.6\n\n- Updated skill name and description in SKILL.md for clarity and improved discoverability.\n- No functional changes; update focuses on documentation and metadata consistency.\n\nv1.0.5 | 2026-05-21T19:48:24.023Z | user\n\nReleased via CI\n\nv1.0.4 | 2026-05-21T19:43:59.508Z | user\n\nReleased via CI\n\nv1.0.3 | 2026-05-21T19:42:06.482Z | user\n\nReleased via CI\n\nv1.0.2 | 2026-05-18T03:04:30.491Z | user\n\nAlign skill versions with CLI v0.1.9\n\nv1.0.1 | 2026-05-18T02:30:10.442Z | user\n\nAdd tracker add/update/get; application-manager now uses CLI instead of raw sqlite3\n\nv1.0.0 | 2026-05-18T01:01:44.461Z | auto\n\nApplication Manager 1.0.0\n\n- Initial release: sync job application statuses from Gmail into Notion or a local SQLite database.\n- Supports setup flows for Notion or SQLite backends, with prompts to collect user info as needed.\n- Maps Gmail senders and labels to companies and uses status mapping from common email phrases.\n- Automates deduplication and updating/creating application records in your chosen tracker.\n- Provides a structured workflow from reading the user profile to reporting sync results.\n- Includes detailed configuration instructions for first-time setup and backend-specific options.\n\nArchive index:\n\nArchive v1.0.11: 3 files, 13820 bytes\n\nFiles: skill-card.md (2081b), SKILL.md (35286b), _meta.json (143b)\n\nFile v1.0.11:SKILL.md\n\n---\nname: Job Application Manager — Gmail & Notion Sync\ndescription: Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations\nsummary: >\n  Use this skill to sync your job applications automatically from Gmail\n  into Notion or a local SQLite database. It scans your inbox for emails\n  from companies you have applied to, classifies each message as an offer,\n  rejection, or interview invitation, and syncs the application status into\n  your tracker without any manual effort. Supports Gmail label filters and\n  sender-pattern matching for accurate company detection. Works with both\n  Notion (cloud career tracker) and SQLite (local, no account required).\n  Deduplicates entries so running it multiple times is safe. When syncing\n  a specific company, it also enriches the Notion page with a timeline,\n  conversation notes, key contacts, and preparation suggestions. Detects\n  stale applications (no email activity in 30+ days) and flags them for\n  follow-up. Extracts scheduled interview dates and optionally creates\n  Google Calendar events. Auto-extracts ATS job links from email footers\n  and infers tags (Referral, Remote, Urgent) from email content. Prints\n  a pipeline funnel summary after every full sync. Uses a company alias\n  table and edit-distance matching to prevent duplicate rows when company\n  names vary (e.g. \"Meta\" vs \"Meta Platforms\"). Supports a bulk\n  re-enrichment mode that retrospectively adds timeline, contacts, and\n  prep notes to all existing Notion pages.\nversion: 0.5.0\nauthor: Yuan Chen\nrepository: https://github.com/chenyuan99/swelist\nkeywords:\n  - gmail\n  - notion\n  - job-applications\n  - application-tracker\n  - career-management\n  - offer-detection\n  - interview-tracker\n  - rejection-tracker\n  - job-status\n  - sync\n  - automation\n  - sqlite\n  - email-parsing\n  - career-tracker\ntags:\n  - career\n  - productivity\n  - gmail\n  - notion\n  - job-search\ncategory: career\nmetadata:\n  openclaw:\n    emoji: \"📋\"\n    requires:\n      bins: []\n      env: []\n      mcp:\n        - name: claude_ai_Gmail\n          reason: Search and read job application emails\n          optional: false\n        - name: claude_ai_Notion\n          reason: Create and update application entries in Notion\n          optional: true\n        - name: claude_ai_Google_Calendar\n          reason: Create calendar events for scheduled interviews\n          optional: true\n    config:\n      - key: tracker_backend\n        description: Storage backend — notion or sqlite\n        default: notion\n      - key: sqlite_db_path\n        description: Path to SQLite database (sqlite backend only)\n        default: ~/.offerplus/applications.db\n---\n\n# Application Manager\n\n## When to Use This Skill\n\nTrigger when the user asks to:\n\n- Sync or refresh job application emails from Gmail (\"sync my applications\", \"check my job emails\", \"refresh my tracker\")\n- Update the status of one or more specific applications (\"mark Amazon as rejected\", \"I got an offer from Stripe\", \"update my application status\")\n- Add new applications discovered in Gmail (\"add this application to my tracker\", \"log this job email\")\n- Check what emails arrived from a specific company (\"did Google email me back?\", \"any updates from Meta?\")\n- Track offers, rejections, or interview invitations automatically (\"did I hear back from anyone?\", \"what's my interview pipeline?\")\n- Sync applications into Notion (\"add to my Notion tracker\", \"update my Notion job board\")\n- Sync applications into a local SQLite database (\"add to my sqlite tracker\", \"update my local tracker\")\n- Export or review their full application pipeline (\"show my application pipeline\", \"what's the status of all my applications?\")\n- Update a specific company's page with conversation details, contacts, or prep notes\n- Flag stale applications that haven't had activity in 30+ days (\"what's gone quiet?\", \"flag stale apps\", \"what needs follow-up?\")\n- Re-enrich all existing Notion pages with timeline, contacts, and prep notes (\"re-enrich all pages\", \"update all pages\", \"add prep notes to all my applications\")\n\nKeywords: `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`\n\n---\n\n## Setup (first-time use)\n\n**Always read `profile.md` first.** It contains the user's tracker backend choice,\nNotion database ID or SQLite path, Gmail label IDs, and career email.\n\nResolve these fields before continuing (collect from user + write back to `profile.md`):\n\n1. **Tracker backend** — `Integrations > Tracker Backend`\n   - If blank: ask the user — \"Do you use Notion or would you prefer a local SQLite file?\"\n   - Set to `notion` or `sqlite`\n\n2. **If notion** — resolve `Integrations > Notion > Career tracker database ID`\n   - If missing: ask user to open their Notion career tracker and copy the URL.\n     The ID is the UUID in `notion.so/<workspace>/<DATABASE_ID>?v=...`\n   - Derive: `NOTION_DB_ID`, `COLLECTION_URL = collection://<NOTION_DB_ID>`\n\n3. **If sqlite** — resolve `Integrations > Tracker Backend > SQLite DB path`\n   - Default: `~/.offerplus/applications.db`\n   - On first use, initialize the DB: `swelist tracker init --db <path>`\n     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')));\"`\n\n4. **Gmail label IDs** — `Integrations > Gmail Labels` table\n   - If missing: run `mcp__claude_ai_Gmail__list_labels`, show the list, ask the user\n     which labels are job-related, fill in the table in `profile.md`.\n\n5. **Career email** — `Personal Info > Career email`\n   - If missing: ask the user which email address receives job application emails.\n\n---\n\n## Config: Company → Gmail\n\nPopulate from the user's own labels (`mcp__claude_ai_Gmail__list_labels`):\n\n| Company key | Gmail sender pattern | Gmail label ID | Label name |\n|---|---|---|---|\n| `amazon` | `noreply@mail.amazon.jobs` | _(run list_labels)_ | e.g. amazon |\n| `linkedin` | `jobs-noreply@linkedin.com` | _(run list_labels)_ | e.g. linkedin |\n| `google` | `@google.com` | — | — |\n| `meta` | `@meta.com` | — | — |\n| `_any_` | — | — | — |\n\nAdd rows for any other companies the user has labeled. If no labels exist, rely on sender pattern alone.\n\n---\n\n## Config: Status Mapping\n\nMap email signals → pipeline status value (pick the **first** match).\nThe pipeline is ordered from early to late stage; never move a status *backwards*.\n\n| Priority | Signal (case-insensitive) | Status |\n|---|---|---|\n| 1 | \"offer\" OR \"congratulations\" OR \"pleased to inform\" OR \"we'd like to extend\" OR \"offer letter\" | `Offer` |\n| 2 | \"on-site\" OR \"final round\" OR \"technical interview\" OR \"virtual interview\" OR \"interview loop\" | `Interviewing` |\n| 3 | \"schedule\" OR \"next steps\" OR \"move forward\" OR \"hiring manager\" OR \"phone interview\" OR \"video call\" | `Interviewing` |\n| 4 | \"online assessment\" OR \"coding challenge\" OR \"hackerrank\" OR \"codility\" OR \"take-home\" OR \"assessment link\" | `OA` |\n| 5 | \"recruiter\" OR \"phone screen\" OR \"initial screen\" OR \"introductory call\" OR \"talent acquisition\" | `Recruiter screen` |\n| 6 | \"unable to move forward\" OR \"not selected\" OR \"no longer considering\" OR \"other candidates\" OR \"position has been filled\" OR \"not moving forward\" | `Rejected` |\n| 7 | \"application received\" OR \"thank you for applying\" OR \"keep track\" OR \"under review\" OR \"successfully submitted\" | `Applied / Received` |\n| 8 | (no email found, only a job listing) | `Applied / Received` |\n\nIf multiple threads exist for the same role, use the **most recent** email's status.\n\n---\n\n## Config: Next Action Mapping\n\nAuto-set `Next action` when creating or updating a Notion page:\n\n| Status | Next action |\n|---|---|\n| `Applied / Received` | `Waiting` |\n| `OA` | `Prep` |\n| `Recruiter screen` | `Prep` |\n| `Interviewing` | `Prep` |\n| `Offer` | `Send availability` |\n| `Rejected` | _(leave blank / clear)_ |\n| `Withdrawn` | _(leave blank / clear)_ |\n\n---\n\n## Config: Link Extraction\n\nScan the full email body (not just the snippet) for ATS application URLs.\nUse the **first** match from the priority list; stop once found.\n\n| Priority | Platform | URL pattern to match |\n|---|---|---|\n| 1 | Greenhouse | `boards.greenhouse.io/` OR `grnh.se/` |\n| 2 | Lever | `jobs.lever.co/` |\n| 3 | Workday | `apply.workday.com/` OR `myworkdayjobs.com/` |\n| 4 | Ashby | `jobs.ashbyhq.com/` |\n| 5 | SmartRecruiters | `jobs.smartrecruiters.com/` |\n| 6 | LinkedIn job posting | `linkedin.com/jobs/view/` |\n| 7 | Indeed job posting | `indeed.com/viewjob` OR `indeed.com/rc/clk` |\n| 8 | Any URL containing `/jobs/` or `/careers/` + company domain | e.g. `stripe.com/jobs/` |\n\nRules:\n- Strip tracking parameters (`?gh_src=`, `?utm_*`, etc.) before saving.\n- If multiple ATS URLs found in one email, prefer the one matching the priority list first.\n- If no ATS URL found, set `link = null`; do not write the field.\n- Never fabricate or guess URLs.\n\n---\n\n## Config: Tag Inference\n\nInfer `Tags` values from the email body using these rules (all case-insensitive).\n**Only write tags that already exist in the Notion Tags options list** — check first, omit if not present.\n\n| Tag | Signals to look for |\n|---|---|\n| `Referral` | \"referred by\", \"referral from\", \"your referral\", \"employee referral\", or application link contains `ref=` / `referral` |\n| `Remote` | \"remote\", \"fully remote\", \"work from home\", \"distributed team\", \"location: remote\", \"anywhere\" |\n| `Urgent` | \"respond by [date]\", \"deadline\", \"respond within [N] days\", \"as soon as possible\", \"ASAP\" |\n| `Visa` | \"visa sponsorship\", \"H-1B\", \"OPT\", \"CPT\", \"work authorization provided\" |\n\nPresent inferred tags to the user before writing when running interactively:\n> \"Inferred tags: [Referral, Remote] — write to Notion? (y/n/edit)\"\n\nWhen running non-interactively (bulk sync), write them silently and note in the report.\n\n---\n\n## Config: Company Aliases\n\nUsed in Step 3 to match email-extracted company names against existing tracker entries.\nCheck aliases **before** running edit-distance; aliases are authoritative.\n\n| Canonical name | Known aliases |\n|---|---|\n| `Meta` | Meta Platforms, Meta Platforms Inc., Facebook, Instagram |\n| `Google` | Google LLC, Alphabet, Google DeepMind, Google Cloud, YouTube |\n| `Amazon` | Amazon.com, Amazon Web Services, AWS |\n| `Microsoft` | Microsoft Corporation, Azure |\n| `Apple` | Apple Inc. |\n| `Netflix` | Netflix Inc. |\n| `Uber` | Uber Technologies, Uber Technologies Inc. |\n| `Lyft` | Lyft Inc. |\n| `Stripe` | Stripe Inc., Stripe Payments |\n| `Airbnb` | Airbnb Inc. |\n| `Coinbase` | Coinbase Global, Coinbase Inc. |\n| `Figma` | Figma Inc. |\n| `Notion` | Notion Labs, Notion Labs Inc. |\n| `OpenAI` | OpenAI Inc., OpenAI LP |\n| `Anthropic` | Anthropic PBC |\n\nAdd rows to this table whenever the user encounters a new company variant and confirms a merge.\nWrite updated rows back to `profile.md` under a `Company Aliases` table so they persist across runs.\n\n**Edit-distance fallback** (when no alias matches):\n\n```\nFUNCTION fuzzy_match(extracted_name, existing_names[]):\n  candidates = []\n  FOR each existing_name in existing_names:\n    dist = levenshtein(extracted_name.lower(), existing_name.lower())\n    IF dist <= 2 OR one_is_prefix_of_other(extracted_name, existing_name):\n      candidates.append((existing_name, dist))\n  RETURN sorted(candidates, by=dist)[:3]   # top 3 closest\n```\n\nIf candidates found: present to user — \"Found similar entry: '{existing}'. Is this the same company as '{extracted}'? (y/n)\"\n- Yes → use existing page; write alias to profile.md for future runs\n- No → treat as new company; create new row\n\n---\n\n## Config: Notion Database\n\n_(skip if tracker_backend is sqlite)_\n\n| Field | Value |\n|---|---|\n| Collection URL | `collection://<NOTION_DB_ID>` |\n| Parent ID (for creates) | `<NOTION_DB_ID>` |\n\nExpected schema (updated pipeline schema):\n\n| Property | Type | Allowed values / notes |\n|---|---|---|\n| `Name` | title | `\"Company — Role Title\"` |\n| `status` | select | `Applied / Received`, `OA`, `Recruiter screen`, `Interviewing`, `Offer`, `Rejected`, `Withdrawn` |\n| `Company` | multi-select | Company name (e.g. `Google`, `Amazon`) |\n| `Tags` | multi-select | Labels like `Referral`, `Remote`, `NYC`, `Top choice`, `Visa`, `OA`, `Onsite` |\n| `Applied on` | date | ISO date when first applied |\n| `Last touch` | date | Date of most recent email in the thread |\n| `Interview date` | date | Parsed date of the next scheduled interview (if any) |\n| `Next action` | select | `Waiting`, `Prep`, `Follow up`, `Send availability` |\n| `Link` | url | Job posting URL or Greenhouse/application portal link |\n\n**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:\n\n| Old value | Maps to in old schema |\n|---|---|\n| `Applied / Received` | `In progress` |\n| `OA` | `In progress` |\n| `Recruiter screen` | `In progress` |\n| `Interviewing` | `In progress` |\n| `Offer` | `Done` |\n| `Rejected` | `Rejected` |\n| `Withdrawn` | `Rejected` |\n\n---\n\n## Config: SQLite Schema\n\n_(skip if tracker_backend is notion)_\n\n**Current (v0.5.0) schema:**\n\n```sql\nCREATE TABLE IF NOT EXISTS applications (\n  name           TEXT PRIMARY KEY,   -- \"Company — Role Title\"\n  status         TEXT NOT NULL,      -- pipeline status (see Status Mapping)\n  job_id         TEXT,\n  company        TEXT,\n  applied_on     TEXT,               -- ISO date YYYY-MM-DD\n  last_touch     TEXT,               -- ISO date of most recent email\n  interview_date TEXT,               -- ISO date of next scheduled interview\n  next_action    TEXT,               -- Waiting | Prep | Follow up | Send availability\n  link           TEXT,               -- job posting or portal URL (ATS-extracted)\n  tags           TEXT,               -- comma-separated inferred tags e.g. \"Referral,Remote\"\n  notes          TEXT,\n  updated_at     TEXT DEFAULT (datetime('now'))\n);\n```\n\n**Migration from v0.2.x** (run once if the DB already exists):\n\n```sql\nALTER TABLE applications ADD COLUMN company        TEXT;\nALTER TABLE applications ADD COLUMN last_touch     TEXT;\nALTER TABLE applications ADD COLUMN interview_date TEXT;\nALTER TABLE applications ADD COLUMN next_action    TEXT;\nALTER TABLE applications ADD COLUMN link           TEXT;\nALTER TABLE applications ADD COLUMN tags           TEXT;\n```\n\nRun via: `sqlite3 <DB_PATH> < migration.sql`\nOr inline: `sqlite3 ~/.offerplus/applications.db \"ALTER TABLE applications ADD COLUMN last_touch TEXT; ...\"`\n\nAt Step 0, check whether `last_touch` column exists (`PRAGMA table_info(applications)`) and run the migration automatically if not.\n\n---\n\n## Workflow\n\n```\nINPUT: company (optional), date_range (default: newer_than:6m)\n\nSTEP 0  Load profile\n  Read profile.md → extract tracker_backend, CAREER_EMAIL, label table\n  IF tracker_backend == \"notion\":  resolve NOTION_DB_ID, COLLECTION_URL\n  IF tracker_backend == \"sqlite\":\n    resolve DB_PATH; run init if DB does not exist\n    check PRAGMA table_info(applications) for last_touch column\n    IF missing: run migration SQL (see Config: SQLite Schema)\n  IF any required field blank: collect from user → write to profile.md → continue\n  IF company given AND not in label table:\n    run list_labels → confirm with user → append to profile.md label table\n\nSTEP 1  Search Gmail\n  IF company given:\n    query = build_query(company)          # see Query Builder below\n  ELSE:\n    query = 'subject:\"application\" OR subject:\"your application\" OR subject:\"interview\" newer_than:6m'\n  threads = gmail_search(query, max_results=20)\n  FOR each thread WHERE snippet is ambiguous:\n    fetch full thread via gmail_get_thread(thread_id)\n\nSTEP 2  Parse threads → applications[]\n  FOR each thread:\n    company_name   = extract_company(thread)\n    role_title     = extract_role(thread)\n    status         = map_status(thread)         # use Status Mapping table above\n    next_action    = map_next_action(status)    # use Next Action Mapping table above\n    date           = most_recent_message_date(thread)\n    applied_date   = earliest_message_date(thread)\n    job_id         = extract_job_id(thread)     # if present in email\n    interview_date = extract_interview_date(thread)\n                     # scan body for patterns: \"June 9\", \"Monday June 9 at 2pm PT\",\n                     # \"scheduled for <date>\", \"your interview is on <date>\"\n                     # normalise to ISO date YYYY-MM-DD; set null if not found\n    link           = extract_link(thread)\n                     # match ATS URL patterns (see Config: Link Extraction)\n                     # strip tracking params; null if not found\n    suggested_tags = infer_tags(thread)\n                     # apply Tag Inference rules; returns list e.g. [\"Referral\",\"Remote\"]\n                     # empty list if no signals found\n    page_name      = f\"{company_name} — {role_title}\"\n    APPEND { page_name, company_name, status, next_action, date, applied_date,\n             job_id, interview_date, link, suggested_tags, thread_id }\n\nSTEP 3  Deduplicate (with fuzzy company matching)\n  IF tracker_backend == \"notion\":\n    all_pages = notion_fetch(COLLECTION_URL)   # cache for reuse in Steps 6+7\n    existing_names = [p.name for p in all_pages]\n    FOR each application:\n      # 1. exact match\n      existing = find_exact(application.page_name, existing_names)\n      IF not found:\n        # 2. alias match on company portion\n        canonical = resolve_alias(application.company_name)  # Config: Company Aliases\n        existing = find_exact(canonical + \" — \" + role_title, existing_names)\n      IF not found:\n        # 3. edit-distance match on company name portion\n        company_candidates = fuzzy_match(application.company_name,\n                                         [extract_company(n) for n in existing_names])\n        IF company_candidates:\n          ask user to confirm merge (show top match + distance)\n          IF confirmed: existing = candidate; write alias to profile.md\n      IF found: set existing_status, action = \"update\" or \"skip\"\n      ELSE:     action = \"create\"\n\n  IF tracker_backend == \"sqlite\":\n    existing_rows = sqlite3 DB_PATH \"SELECT name, company, status FROM applications\"\n    FOR each application:\n      # 1. exact match\n      result = find_exact(application.page_name, existing_rows)\n      IF not found:\n        # 2. alias match\n        canonical = resolve_alias(application.company_name)\n        result = find_exact(canonical + \" — \" + role_title, existing_rows)\n      IF not found:\n        # 3. edit-distance match on company column\n        company_candidates = fuzzy_match(application.company_name,\n                                         [r.company for r in existing_rows if r.company])\n        IF company_candidates:\n          ask user to confirm merge\n          IF confirmed: result = candidate; write alias to profile.md\n      IF result is not null: set existing_status, action = \"update\" or \"skip\"\n      ELSE:                   action = \"create\"\n\nSTEP 4  Apply changes\n  IF tracker_backend == \"notion\":\n    FOR each create/update:\n      confirm_tags = filter suggested_tags to only values in existing Notion Tags options\n      IF interactive AND confirm_tags not empty:\n        prompt user to confirm / edit tag suggestions\n    creates → notion_create_pages(batch) with all fields incl. Interview date, Link, Tags\n    updates → notion_update_page per entry with status, Last touch, Next action,\n              Interview date (if extracted), Link (if extracted, don't overwrite existing),\n              Tags (merge with any already on page; add new confirmed ones only)\n\n  IF tracker_backend == \"sqlite\":\n    FOR each create:\n      sqlite3 DB_PATH \"INSERT OR IGNORE INTO applications\n        (name,status,company,job_id,applied_on,last_touch,interview_date,next_action,link,tags)\n        VALUES (?,?,?,?,?,?,?,?,?,?)\"\n    FOR each update:\n      sqlite3 DB_PATH \"UPDATE applications SET status=?, last_touch=?,\n        next_action=?, interview_date=COALESCE(?,interview_date),\n        link=COALESCE(?,link), tags=COALESCE(?,tags),\n        updated_at=datetime('now') WHERE name=?\"\n\nSTEP 5  Enrich page content (Notion only, when company is given OR when status changed)\n  FOR each application that was created or updated (and tracker is notion):\n    fetch full thread via gmail_get_thread(thread_id)\n    extract:\n      timeline[]       = list of { date, event_summary } sorted chronologically\n      key_contacts[]   = list of { name, email, role } from From/Cc headers\n      conversation[]   = key quotes or summaries from each email in thread\n      prep_suggestions = generate 3-5 bullet prep suggestions based on status + role\n    Build structured page content:\n      ## Timeline\n      | Date | Event |\n      ...\n      ## Key Contacts\n      | Name | Email | Role |\n      ...\n      ## Conversation Notes\n      [bullet summary of each email in thread]\n      ## Preparation\n      [3-5 tailored bullet points based on company + role + stage]\n    notion_update_page(page_id, content=structured_content)\n\n    IF interview_date is not null AND Google Calendar MCP is available:\n      ask user: \"Create a calendar event for <company> interview on <date>?\"\n      IF yes:\n        google_calendar_create_event(\n          summary  = \"<company> — <role> Interview\",\n          start    = interview_date + \"T09:00:00\",  # use extracted time if available\n          end      = interview_date + \"T10:00:00\",\n          description = \"Application tracked in 2026 Career Notion database.\"\n        )\n\nSTEP 6  Staleness detection\n  stale_threshold = today - 30 days\n  IF tracker_backend == \"notion\":\n    # all_pages already fetched and cached in Step 3 — no second fetch needed\n    stale = [p for p in all_pages\n             if p.status NOT IN (\"Rejected\", \"Withdrawn\", \"Offer\")\n             AND (p.last_touch < stale_threshold OR p.last_touch is null)\n             AND p.next_action != \"Follow up\"]\n    FOR each stale page:\n      notion_update_page(page_id, properties={\"Next action\": \"Follow up\"})\n\n  IF tracker_backend == \"sqlite\":\n    stale = sqlite3 DB_PATH \"SELECT name, status, last_touch FROM applications\n      WHERE status NOT IN ('Rejected','Withdrawn','Offer')\n      AND (date(COALESCE(last_touch, updated_at)) < date('now','-30 days'))\n      AND COALESCE(next_action,'') != 'Follow up'\"\n    FOR each stale row:\n      sqlite3 DB_PATH \"UPDATE applications SET next_action='Follow up',\n        updated_at=datetime('now') WHERE name=?\"\n\nSTEP 7  Report\n  IF no specific company was given (full sync):\n    IF tracker_backend == \"notion\":\n      counts = tally status values from all_pages (fetched in Step 6)\n    IF tracker_backend == \"sqlite\":\n      counts = sqlite3 DB_PATH \"SELECT status, COUNT(*) FROM applications GROUP BY status\"\n    print funnel summary line (see Report Format below)\n  print per-application summary (see Report Format below)\n```\n\n---\n\n## Workflow: Bulk Re-enrichment Mode\n\nTriggered when the user says \"re-enrich all pages\", \"add notes to all applications\",\n\"update all pages with prep notes\", or uses the `--enrich-all` flag.\n\nRuns Step 5 enrichment (timeline, key contacts, conversation notes, prep suggestions)\nagainst **all existing** Notion pages, not just the ones touched in the current sync.\nRequires Notion backend. Skip gracefully if tracker_backend is sqlite (no page content).\n\n```\nINPUT: include_closed (default: false) — whether to enrich Rejected/Withdrawn pages\n\nSTEP R0  Load profile (same as main STEP 0)\n\nSTEP R1  Fetch all pages\n  all_pages = notion_fetch(COLLECTION_URL)\n  IF include_closed == false:\n    pages = [p for p in all_pages if p.status NOT IN (\"Rejected\", \"Withdrawn\")]\n  ELSE:\n    pages = all_pages\n  print: \"Found {len(pages)} pages to enrich.\"\n\nSTEP R2  Match pages to Gmail threads\n  FOR each page in pages:\n    company_name = extract_company_from_name(page.name)\n    role_title   = extract_role_from_name(page.name)\n    query        = build_query(company_name) with date_range=\"newer_than:12m\"\n    threads      = gmail_search(query, max_results=5)\n    IF no threads found:\n      skip page; note in report as \"no emails found\"\n      CONTINUE\n    best_thread  = most_recent_thread(threads)\n    full_thread  = gmail_get_thread(best_thread.id)\n    STORE { page, full_thread }\n\nSTEP R3  Enrich each page (same as main STEP 5)\n  FOR each { page, full_thread }:\n    extract timeline, key_contacts, conversation notes, prep_suggestions\n    notion_update_page(page.id, content=structured_content)\n    IF page.interview_date is null:\n      interview_date = extract_interview_date(full_thread)\n      IF found: notion_update_page(page.id, properties={\"Interview date\": interview_date})\n    IF page.link is null:\n      link = extract_link(full_thread)\n      IF found: notion_update_page(page.id, properties={\"Link\": link})\n\nSTEP R4  Report\n  print:\n    Re-enriched: {N} pages\n      ✦ {Company} — {Role}  (+ interview date | + link | full notes)\n    Skipped (no emails): {M} pages\n      · {Company} — {Role}\n```\n\n**Rate-limiting note:** Enriching many pages will issue many API calls. Pause 1 second\nbetween Gmail thread fetches to avoid hitting rate limits. Process pages in batches\nof 10 and report progress after each batch.\n\n---\n\n## Query Builder\n\n```\nFUNCTION build_query(company_key, date_range=\"newer_than:6m\"):\n  cfg = COMPANY_CONFIG[company_key]\n  parts = []\n  IF cfg.sender:   parts.append(f\"from:{cfg.sender}\")\n  IF cfg.label_id: parts.append(f\"label:{cfg.label_id}\")\n  subject_terms = 'subject:application OR subject:interview OR subject:offer OR subject:position OR subject:assessment OR subject:next steps'\n  RETURN f'({\" OR \".join(parts)}) ({subject_terms}) {date_range}'\n```\n\nExamples:\n- Amazon with label → `(from:noreply@mail.amazon.jobs OR label:<label_id>) (subject:application OR ...) newer_than:6m`\n- Unknown company → fall back to `from:@<company>.com` or company name in subject\n\n---\n\n## Storage API Calls\n\n### Notion — Create (batch)\n```json\n{\n  \"data_source_id\": \"<NOTION_DB_ID>\",\n  \"pages\": [\n    {\n      \"properties\": {\n        \"Name\": \"Amazon — SDE, AWS\",\n        \"status\": \"Applied / Received\",\n        \"Company\": [\"Amazon\"],\n        \"Tags\": [\"Referral\"],\n        \"Applied on\": \"2026-05-17\",\n        \"Last touch\": \"2026-05-17\",\n        \"Interview date\": null,\n        \"Next action\": \"Waiting\",\n        \"Link\": \"https://boards.greenhouse.io/amazon/jobs/12345\"\n      },\n      \"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\"\n    }\n  ]\n}\n```\n\n### Notion — Update (single, status + dates + next action)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": {\n    \"status\": \"Interviewing\",\n    \"Last touch\": \"2026-05-28\",\n    \"Interview date\": \"2026-06-09\",\n    \"Next action\": \"Prep\"\n  },\n  \"content_updates\": []\n}\n```\n\n### Notion — Update (single, staleness flag)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": {\n    \"Next action\": \"Follow up\"\n  },\n  \"content_updates\": []\n}\n```\n\n### Notion — Update (single, full page enrichment)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": {\n    \"status\": \"Interviewing\",\n    \"Last touch\": \"2026-05-28\",\n    \"Next action\": \"Prep\"\n  },\n  \"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\"\n}\n```\n\n### Notion — Search (dedup)\n```json\n{\n  \"query\": \"Amazon — Software Development Engineer\",\n  \"data_source_url\": \"collection://<NOTION_DB_ID>\",\n  \"filters\": {}\n}\n```\n\n### SQLite — Create\n```bash\nswelist tracker add \"Amazon — SDE, AWS\" \\\n  --status \"Applied / Received\" \\\n  --job-id 10414382 \\\n  --applied-on 2026-05-17 \\\n  --db <DB_PATH>\n```\n\n### SQLite — Update\n```bash\nswelist tracker update \"Amazon — SDE, AWS\" \\\n  --status \"Interviewing\" \\\n  --db <DB_PATH>\n```\n\n### SQLite — Search (dedup)\n```bash\nswelist tracker get \"Amazon — SDE, AWS\" --db <DB_PATH>\n# Returns JSON object if found, null if not found. Exit code 1 when not found.\n```\n\n### SQLite — List all\n```bash\nswelist tracker list --db <DB_PATH>\nswelist tracker export --format json --db <DB_PATH>\n```\n\n### SQLite — Funnel summary query\n```bash\nsqlite3 ~/.offerplus/applications.db \\\n  \"SELECT status, COUNT(*) AS n FROM applications GROUP BY status ORDER BY n DESC;\"\n```\n\n### SQLite — Staleness query\n```bash\nsqlite3 ~/.offerplus/applications.db \\\n  \"SELECT name, status, COALESCE(last_touch, updated_at) AS last_activity\n   FROM applications\n   WHERE status NOT IN ('Rejected','Withdrawn','Offer')\n   AND date(COALESCE(last_touch, updated_at)) < date('now','-30 days')\n   ORDER BY last_activity ASC;\"\n```\n\n### Google Calendar — Create interview event\n```json\n{\n  \"summary\": \"Applied Intuition — Software Engineer Interview\",\n  \"start\": { \"dateTime\": \"2026-06-09T14:00:00\", \"timeZone\": \"America/Los_Angeles\" },\n  \"end\":   { \"dateTime\": \"2026-06-09T15:00:00\", \"timeZone\": \"America/Los_Angeles\" },\n  \"description\": \"Application tracked in 2026 Career Notion database.\"\n}\n```\n\n---\n\n## Naming Convention\n\n```\n\"{Company} — {Role Title}\"\n```\n\n| Good | Bad |\n|---|---|\n| `Amazon — Software Development Engineer, AWS` | `SDE at Amazon` |\n| `Google — Software Engineer II, Early Career` | `Google SWE` |\n| `Goldman Sachs — Software Engineer - Associate` | `Goldman` |\n\nRules:\n- Em dash (`—`), not hyphen\n- Company name in title case\n- Role title verbatim from the job posting or email subject when possible\n\n---\n\n## Report Format\n\n```\nPipeline: {N} Applied · {N} OA · {N} Interviewing · {N} Offer · {N} Rejected\n  (omit pipeline line when syncing a single company)\n\nSynced {N} application(s) on {date}  [{backend}: notion|sqlite]\n\nCreated:\n  ✦ {Company} — {Role} → {status}  [next: {next_action}]\n    🔗 {link}  (omit if not found)\n    🏷  Tags: {tag1}, {tag2}  (omit if none inferred)\n\nUpdated:\n  ↑ {Company} — {Role} → {new_status}  (was: {old_status})  [next: {next_action}]\n  + Page enriched: timeline, key contacts, prep notes added\n  📅 Interview date: {date}  (calendar event created / skipped)\n  🔗 Link extracted: {link}  (omit if not found)\n  🏷  Tags added: {tag1}, {tag2}  (omit if none)\n\nSkipped (already up to date):\n  · {Company} — {Role} → {status}\n\nFlagged as stale (no activity > 30 days → Follow up):\n  ⏰ {Company} — {Role}  [last touch: {date}]\n\nMerged (fuzzy company match confirmed):\n  ⟳ \"{extracted}\" → matched to \"{existing}\"  [edit distance: {n}]\n\nErrors:\n  ✕ {description of issue}\n```\n\n---\n\n## Edge Cases\n\n| Situation | Handling |\n|---|---|\n| Multiple threads for same role | Use most recent thread's status |\n| Role title not in email | Use job ID or \"Role\" as placeholder; note it in content/notes |\n| Company exists under a different name variant | Search by company name alone first, prompt user to confirm merge |\n| Email snippet enough to determine status | Skip full thread fetch for status; still fetch for page enrichment when company is given |\n| `Company` or `Tags` value not in Notion options list | Omit the field; do not create new options automatically |\n| Page name collision (same company, same title, different role) | Add disambiguator: `Amazon — SDE, AWS (Job ID 12345)` |\n| Notion DB ID or schema unknown | Fetch database URL with `notion-fetch` to inspect schema first |\n| Notion DB uses old 4-status schema | Map pipeline status to old values (see Config: Notion Database), notify user to upgrade |\n| SQLite DB does not exist | Run `swelist tracker init` or the CREATE TABLE statement before Step 3 |\n| SQLite DB path missing from profile.md | Use default `~/.offerplus/applications.db`; confirm with user |\n| Status would move backwards (e.g. Interviewing → Applied) | Keep existing status; only update Last touch date |\n| Key contacts not extractable from thread | Omit key contacts section; do not hallucinate names or emails |\n| No prep suggestions applicable | Omit Preparation section rather than generating generic advice |\n| Interview date ambiguous (e.g. \"sometime next week\") | Set interview_date to null; note ambiguity in conversation notes |\n| Interview date already in the past | Still write it to the page; do not create a calendar event for past dates |\n| Google Calendar MCP not connected | Skip calendar event creation; note it in the report |\n| SQLite DB exists but missing new columns (v0.2.x) | Run migration SQL automatically at Step 0 before proceeding |\n| Staleness check finds 0 stale rows | Omit the stale section from the report entirely |\n| next_action already \"Follow up\" | Skip staleness update for that row; it's already flagged |\n| ATS URL found but already saved in Link property | Do not overwrite existing value; preserve what was there |\n| Multiple ATS URLs in one email (e.g. Greenhouse + LinkedIn) | Use highest-priority match per Link Extraction table |\n| URL contains tracking params (`?gh_src=`, `?utm_*`) | Strip before saving; store clean canonical URL only |\n| Inferred tag not in Notion Tags options list | Silently omit; never create new Notion options |\n| Inferred tag is already on the page | Skip; do not duplicate |\n| Funnel counts fetched from Notion but all_pages is empty | Skip pipeline line; note \"0 applications tracked\" |\n| Funnel query when syncing a single company | Skip pipeline summary entirely |\n| Fuzzy match finds a candidate but user declines merge | Treat as new company; create new row; do not add alias |\n| Fuzzy match distance is 1 (e.g. \"Gogle\" vs \"Google\") | Still prompt user — never auto-merge without confirmation |\n| Two existing rows both match fuzzily (ambiguous) | Show top 2 candidates; let user pick or decline all |\n| Alias resolved but role title doesn't match | Do not merge — same company, different role is a different application |\n| Bulk re-enrichment on SQLite backend | Not supported; print \"Page enrichment requires Notion backend\" and exit |\n| Bulk re-enrichment: page has no matching Gmail thread | Skip; add to \"no emails found\" section of report |\n| Bulk re-enrichment: page already has rich content | Overwrite with fresh extraction — user triggered this explicitly |\n| Bulk re-enrichment: more than 50 pages to enrich | Warn user of expected time (~2 min per 10 pages); ask to confirm before proceeding |\n\nFile v1.0.11:_meta.json\n\n{\n  \"ownerId\": \"kn78p1g6xzcqm9fyegreqrft0n80bazp\",\n  \"slug\": \"job-application-manager\",\n  \"version\": \"1.0.11\",\n  \"publishedAt\": 1780151682130\n}\n\nFile v1.0.11:skill-card.md\n\n## Description:\n\nSyncs job application emails from Gmail and updates statuses in a Notion or SQLite tracker, detecting offers, rejections, interview invitations, and follow-up needs.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chenyuan99](https://clawhub.ai/user/chenyuan99)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal users use this skill to keep a job application tracker current from Gmail activity, with optional writes to Notion, a local SQLite database, and Google Calendar interview events.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill may read sensitive job-related Gmail threads and write extracted details to a tracker.\n\nMitigation: Use it only with explicit consent for Gmail access and tracker writes, and review the data it will sync before installation.\n\nRisk: SQLite persistence can be unsafe when the database path is user-supplied, profile-loaded, or passed through shell interpolation.\n\nMitigation: Prefer the Notion backend or a fixed trusted SQLite path; validate and pass SQLite paths safely without shell interpolation.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/chenyuan99/skills/job-application-manager)\n- [Publisher profile](https://clawhub.ai/user/chenyuan99)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown reports, structured tracker content, JSON-like API payloads, and shell command snippets]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May read job-related Gmail threads and write extracted application details to Notion, SQLite, or Google Calendar when configured.]\n\n## Skill Version(s):\n\n1.0.11 (source: ClawHub release metadata; artifact frontmatter version 0.5.0)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.0.10: 3 files, 11651 bytes\n\nFiles: skill-card.md (2448b), SKILL.md (28261b), _meta.json (143b)\n\nFile v1.0.10:SKILL.md\n\n---\nname: Job Application Manager — Gmail & Notion Sync\ndescription: Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations\nsummary: >\n  Use this skill to sync your job applications automatically from Gmail\n  into Notion or a local SQLite database. It scans your inbox for emails\n  from companies you have applied to, classifies each message as an offer,\n  rejection, or interview invitation, and syncs the application status into\n  your tracker without any manual effort. Supports Gmail label filters and\n  sender-pattern matching for accurate company detection. Works with both\n  Notion (cloud career tracker) and SQLite (local, no account required).\n  Deduplicates entries so running it multiple times is safe. When syncing\n  a specific company, it also enriches the Notion page with a timeline,\n  conversation notes, key contacts, and preparation suggestions. Detects\n  stale applications (no email activity in 30+ days) and flags them for\n  follow-up. Extracts scheduled interview dates and optionally creates\n  Google Calendar events. Auto-extracts ATS job links from email footers\n  and infers tags (Referral, Remote, Urgent) from email content. Prints\n  a pipeline funnel summary after every full sync.\nversion: 0.4.0\nauthor: Yuan Chen\nrepository: https://github.com/chenyuan99/swelist\nkeywords:\n  - gmail\n  - notion\n  - job-applications\n  - application-tracker\n  - career-management\n  - offer-detection\n  - interview-tracker\n  - rejection-tracker\n  - job-status\n  - sync\n  - automation\n  - sqlite\n  - email-parsing\n  - career-tracker\ntags:\n  - career\n  - productivity\n  - gmail\n  - notion\n  - job-search\ncategory: career\nmetadata:\n  openclaw:\n    emoji: \"📋\"\n    requires:\n      bins: []\n      env: []\n      mcp:\n        - name: claude_ai_Gmail\n          reason: Search and read job application emails\n          optional: false\n        - name: claude_ai_Notion\n          reason: Create and update application entries in Notion\n          optional: true\n        - name: claude_ai_Google_Calendar\n          reason: Create calendar events for scheduled interviews\n          optional: true\n    config:\n      - key: tracker_backend\n        description: Storage backend — notion or sqlite\n        default: notion\n      - key: sqlite_db_path\n        description: Path to SQLite database (sqlite backend only)\n        default: ~/.offerplus/applications.db\n---\n\n# Application Manager\n\n## When to Use This Skill\n\nTrigger when the user asks to:\n\n- Sync or refresh job application emails from Gmail (\"sync my applications\", \"check my job emails\", \"refresh my tracker\")\n- Update the status of one or more specific applications (\"mark Amazon as rejected\", \"I got an offer from Stripe\", \"update my application status\")\n- Add new applications discovered in Gmail (\"add this application to my tracker\", \"log this job email\")\n- Check what emails arrived from a specific company (\"did Google email me back?\", \"any updates from Meta?\")\n- Track offers, rejections, or interview invitations automatically (\"did I hear back from anyone?\", \"what's my interview pipeline?\")\n- Sync applications into Notion (\"add to my Notion tracker\", \"update my Notion job board\")\n- Sync applications into a local SQLite database (\"add to my sqlite tracker\", \"update my local tracker\")\n- Export or review their full application pipeline (\"show my application pipeline\", \"what's the status of all my applications?\")\n- Update a specific company's page with conversation details, contacts, or prep notes\n- Flag stale applications that haven't had activity in 30+ days (\"what's gone quiet?\", \"flag stale apps\", \"what needs follow-up?\")\n\nKeywords: `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`\n\n---\n\n## Setup (first-time use)\n\n**Always read `profile.md` first.** It contains the user's tracker backend choice,\nNotion database ID or SQLite path, Gmail label IDs, and career email.\n\nResolve these fields before continuing (collect from user + write back to `profile.md`):\n\n1. **Tracker backend** — `Integrations > Tracker Backend`\n   - If blank: ask the user — \"Do you use Notion or would you prefer a local SQLite file?\"\n   - Set to `notion` or `sqlite`\n\n2. **If notion** — resolve `Integrations > Notion > Career tracker database ID`\n   - If missing: ask user to open their Notion career tracker and copy the URL.\n     The ID is the UUID in `notion.so/<workspace>/<DATABASE_ID>?v=...`\n   - Derive: `NOTION_DB_ID`, `COLLECTION_URL = collection://<NOTION_DB_ID>`\n\n3. **If sqlite** — resolve `Integrations > Tracker Backend > SQLite DB path`\n   - Default: `~/.offerplus/applications.db`\n   - On first use, initialize the DB: `swelist tracker init --db <path>`\n     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')));\"`\n\n4. **Gmail label IDs** — `Integrations > Gmail Labels` table\n   - If missing: run `mcp__claude_ai_Gmail__list_labels`, show the list, ask the user\n     which labels are job-related, fill in the table in `profile.md`.\n\n5. **Career email** — `Personal Info > Career email`\n   - If missing: ask the user which email address receives job application emails.\n\n---\n\n## Config: Company → Gmail\n\nPopulate from the user's own labels (`mcp__claude_ai_Gmail__list_labels`):\n\n| Company key | Gmail sender pattern | Gmail label ID | Label name |\n|---|---|---|---|\n| `amazon` | `noreply@mail.amazon.jobs` | _(run list_labels)_ | e.g. amazon |\n| `linkedin` | `jobs-noreply@linkedin.com` | _(run list_labels)_ | e.g. linkedin |\n| `google` | `@google.com` | — | — |\n| `meta` | `@meta.com` | — | — |\n| `_any_` | — | — | — |\n\nAdd rows for any other companies the user has labeled. If no labels exist, rely on sender pattern alone.\n\n---\n\n## Config: Status Mapping\n\nMap email signals → pipeline status value (pick the **first** match).\nThe pipeline is ordered from early to late stage; never move a status *backwards*.\n\n| Priority | Signal (case-insensitive) | Status |\n|---|---|---|\n| 1 | \"offer\" OR \"congratulations\" OR \"pleased to inform\" OR \"we'd like to extend\" OR \"offer letter\" | `Offer` |\n| 2 | \"on-site\" OR \"final round\" OR \"technical interview\" OR \"virtual interview\" OR \"interview loop\" | `Interviewing` |\n| 3 | \"schedule\" OR \"next steps\" OR \"move forward\" OR \"hiring manager\" OR \"phone interview\" OR \"video call\" | `Interviewing` |\n| 4 | \"online assessment\" OR \"coding challenge\" OR \"hackerrank\" OR \"codility\" OR \"take-home\" OR \"assessment link\" | `OA` |\n| 5 | \"recruiter\" OR \"phone screen\" OR \"initial screen\" OR \"introductory call\" OR \"talent acquisition\" | `Recruiter screen` |\n| 6 | \"unable to move forward\" OR \"not selected\" OR \"no longer considering\" OR \"other candidates\" OR \"position has been filled\" OR \"not moving forward\" | `Rejected` |\n| 7 | \"application received\" OR \"thank you for applying\" OR \"keep track\" OR \"under review\" OR \"successfully submitted\" | `Applied / Received` |\n| 8 | (no email found, only a job listing) | `Applied / Received` |\n\nIf multiple threads exist for the same role, use the **most recent** email's status.\n\n---\n\n## Config: Next Action Mapping\n\nAuto-set `Next action` when creating or updating a Notion page:\n\n| Status | Next action |\n|---|---|\n| `Applied / Received` | `Waiting` |\n| `OA` | `Prep` |\n| `Recruiter screen` | `Prep` |\n| `Interviewing` | `Prep` |\n| `Offer` | `Send availability` |\n| `Rejected` | _(leave blank / clear)_ |\n| `Withdrawn` | _(leave blank / clear)_ |\n\n---\n\n## Config: Link Extraction\n\nScan the full email body (not just the snippet) for ATS application URLs.\nUse the **first** match from the priority list; stop once found.\n\n| Priority | Platform | URL pattern to match |\n|---|---|---|\n| 1 | Greenhouse | `boards.greenhouse.io/` OR `grnh.se/` |\n| 2 | Lever | `jobs.lever.co/` |\n| 3 | Workday | `apply.workday.com/` OR `myworkdayjobs.com/` |\n| 4 | Ashby | `jobs.ashbyhq.com/` |\n| 5 | SmartRecruiters | `jobs.smartrecruiters.com/` |\n| 6 | LinkedIn job posting | `linkedin.com/jobs/view/` |\n| 7 | Indeed job posting | `indeed.com/viewjob` OR `indeed.com/rc/clk` |\n| 8 | Any URL containing `/jobs/` or `/careers/` + company domain | e.g. `stripe.com/jobs/` |\n\nRules:\n- Strip tracking parameters (`?gh_src=`, `?utm_*`, etc.) before saving.\n- If multiple ATS URLs found in one email, prefer the one matching the priority list first.\n- If no ATS URL found, set `link = null`; do not write the field.\n- Never fabricate or guess URLs.\n\n---\n\n## Config: Tag Inference\n\nInfer `Tags` values from the email body using these rules (all case-insensitive).\n**Only write tags that already exist in the Notion Tags options list** — check first, omit if not present.\n\n| Tag | Signals to look for |\n|---|---|\n| `Referral` | \"referred by\", \"referral from\", \"your referral\", \"employee referral\", or application link contains `ref=` / `referral` |\n| `Remote` | \"remote\", \"fully remote\", \"work from home\", \"distributed team\", \"location: remote\", \"anywhere\" |\n| `Urgent` | \"respond by [date]\", \"deadline\", \"respond within [N] days\", \"as soon as possible\", \"ASAP\" |\n| `Visa` | \"visa sponsorship\", \"H-1B\", \"OPT\", \"CPT\", \"work authorization provided\" |\n\nPresent inferred tags to the user before writing when running interactively:\n> \"Inferred tags: [Referral, Remote] — write to Notion? (y/n/edit)\"\n\nWhen running non-interactively (bulk sync), write them silently and note in the report.\n\n---\n\n## Config: Notion Database\n\n_(skip if tracker_backend is sqlite)_\n\n| Field | Value |\n|---|---|\n| Collection URL | `collection://<NOTION_DB_ID>` |\n| Parent ID (for creates) | `<NOTION_DB_ID>` |\n\nExpected schema (updated pipeline schema):\n\n| Property | Type | Allowed values / notes |\n|---|---|---|\n| `Name` | title | `\"Company — Role Title\"` |\n| `status` | select | `Applied / Received`, `OA`, `Recruiter screen`, `Interviewing`, `Offer`, `Rejected`, `Withdrawn` |\n| `Company` | multi-select | Company name (e.g. `Google`, `Amazon`) |\n| `Tags` | multi-select | Labels like `Referral`, `Remote`, `NYC`, `Top choice`, `Visa`, `OA`, `Onsite` |\n| `Applied on` | date | ISO date when first applied |\n| `Last touch` | date | Date of most recent email in the thread |\n| `Interview date` | date | Parsed date of the next scheduled interview (if any) |\n| `Next action` | select | `Waiting`, `Prep`, `Follow up`, `Send availability` |\n| `Link` | url | Job posting URL or Greenhouse/application portal link |\n\n**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:\n\n| Old value | Maps to in old schema |\n|---|---|\n| `Applied / Received` | `In progress` |\n| `OA` | `In progress` |\n| `Recruiter screen` | `In progress` |\n| `Interviewing` | `In progress` |\n| `Offer` | `Done` |\n| `Rejected` | `Rejected` |\n| `Withdrawn` | `Rejected` |\n\n---\n\n## Config: SQLite Schema\n\n_(skip if tracker_backend is notion)_\n\n**Current (v0.3.0) schema:**\n\n```sql\nCREATE TABLE IF NOT EXISTS applications (\n  name           TEXT PRIMARY KEY,   -- \"Company — Role Title\"\n  status         TEXT NOT NULL,      -- pipeline status (see Status Mapping)\n  job_id         TEXT,\n  company        TEXT,\n  applied_on     TEXT,               -- ISO date YYYY-MM-DD\n  last_touch     TEXT,               -- ISO date of most recent email\n  interview_date TEXT,               -- ISO date of next scheduled interview\n  next_action    TEXT,               -- Waiting | Prep | Follow up | Send availability\n  link           TEXT,               -- job posting or portal URL (ATS-extracted)\n  tags           TEXT,               -- comma-separated inferred tags e.g. \"Referral,Remote\"\n  notes          TEXT,\n  updated_at     TEXT DEFAULT (datetime('now'))\n);\n```\n\n**Migration from v0.2.x** (run once if the DB already exists):\n\n```sql\nALTER TABLE applications ADD COLUMN company        TEXT;\nALTER TABLE applications ADD COLUMN last_touch     TEXT;\nALTER TABLE applications ADD COLUMN interview_date TEXT;\nALTER TABLE applications ADD COLUMN next_action    TEXT;\nALTER TABLE applications ADD COLUMN link           TEXT;\nALTER TABLE applications ADD COLUMN tags           TEXT;\n```\n\nRun via: `sqlite3 <DB_PATH> < migration.sql`\nOr inline: `sqlite3 ~/.offerplus/applications.db \"ALTER TABLE applications ADD COLUMN last_touch TEXT; ...\"`\n\nAt Step 0, check whether `last_touch` column exists (`PRAGMA table_info(applications)`) and run the migration automatically if not.\n\n---\n\n## Workflow\n\n```\nINPUT: company (optional), date_range (default: newer_than:6m)\n\nSTEP 0  Load profile\n  Read profile.md → extract tracker_backend, CAREER_EMAIL, label table\n  IF tracker_backend == \"notion\":  resolve NOTION_DB_ID, COLLECTION_URL\n  IF tracker_backend == \"sqlite\":\n    resolve DB_PATH; run init if DB does not exist\n    check PRAGMA table_info(applications) for last_touch column\n    IF missing: run migration SQL (see Config: SQLite Schema)\n  IF any required field blank: collect from user → write to profile.md → continue\n  IF company given AND not in label table:\n    run list_labels → confirm with user → append to profile.md label table\n\nSTEP 1  Search Gmail\n  IF company given:\n    query = build_query(company)          # see Query Builder below\n  ELSE:\n    query = 'subject:\"application\" OR subject:\"your application\" OR subject:\"interview\" newer_than:6m'\n  threads = gmail_search(query, max_results=20)\n  FOR each thread WHERE snippet is ambiguous:\n    fetch full thread via gmail_get_thread(thread_id)\n\nSTEP 2  Parse threads → applications[]\n  FOR each thread:\n    company_name   = extract_company(thread)\n    role_title     = extract_role(thread)\n    status         = map_status(thread)         # use Status Mapping table above\n    next_action    = map_next_action(status)    # use Next Action Mapping table above\n    date           = most_recent_message_date(thread)\n    applied_date   = earliest_message_date(thread)\n    job_id         = extract_job_id(thread)     # if present in email\n    interview_date = extract_interview_date(thread)\n                     # scan body for patterns: \"June 9\", \"Monday June 9 at 2pm PT\",\n                     # \"scheduled for <date>\", \"your interview is on <date>\"\n                     # normalise to ISO date YYYY-MM-DD; set null if not found\n    link           = extract_link(thread)\n                     # match ATS URL patterns (see Config: Link Extraction)\n                     # strip tracking params; null if not found\n    suggested_tags = infer_tags(thread)\n                     # apply Tag Inference rules; returns list e.g. [\"Referral\",\"Remote\"]\n                     # empty list if no signals found\n    page_name      = f\"{company_name} — {role_title}\"\n    APPEND { page_name, company_name, status, next_action, date, applied_date,\n             job_id, interview_date, link, suggested_tags, thread_id }\n\nSTEP 3  Deduplicate\n  IF tracker_backend == \"notion\":\n    FOR each application:\n      existing = notion_search(query=application.page_name,\n                               data_source_url=COLLECTION_URL)\n      IF found: set existing_status, action = \"update\" or \"skip\"\n      ELSE:     action = \"create\"\n\n  IF tracker_backend == \"sqlite\":\n    FOR each application:\n      result = swelist tracker get \"<page_name>\" --db DB_PATH\n      IF result is not null: set existing_status, action = \"update\" or \"skip\"\n      ELSE:                   action = \"create\"\n\nSTEP 4  Apply changes\n  IF tracker_backend == \"notion\":\n    FOR each create/update:\n      confirm_tags = filter suggested_tags to only values in existing Notion Tags options\n      IF interactive AND confirm_tags not empty:\n        prompt user to confirm / edit tag suggestions\n    creates → notion_create_pages(batch) with all fields incl. Interview date, Link, Tags\n    updates → notion_update_page per entry with status, Last touch, Next action,\n              Interview date (if extracted), Link (if extracted, don't overwrite existing),\n              Tags (merge with any already on page; add new confirmed ones only)\n\n  IF tracker_backend == \"sqlite\":\n    FOR each create:\n      sqlite3 DB_PATH \"INSERT OR IGNORE INTO applications\n        (name,status,company,job_id,applied_on,last_touch,interview_date,next_action,link,tags)\n        VALUES (?,?,?,?,?,?,?,?,?,?)\"\n    FOR each update:\n      sqlite3 DB_PATH \"UPDATE applications SET status=?, last_touch=?,\n        next_action=?, interview_date=COALESCE(?,interview_date),\n        link=COALESCE(?,link), tags=COALESCE(?,tags),\n        updated_at=datetime('now') WHERE name=?\"\n\nSTEP 5  Enrich page content (Notion only, when company is given OR when status changed)\n  FOR each application that was created or updated (and tracker is notion):\n    fetch full thread via gmail_get_thread(thread_id)\n    extract:\n      timeline[]       = list of { date, event_summary } sorted chronologically\n      key_contacts[]   = list of { name, email, role } from From/Cc headers\n      conversation[]   = key quotes or summaries from each email in thread\n      prep_suggestions = generate 3-5 bullet prep suggestions based on status + role\n    Build structured page content:\n      ## Timeline\n      | Date | Event |\n      ...\n      ## Key Contacts\n      | Name | Email | Role |\n      ...\n      ## Conversation Notes\n      [bullet summary of each email in thread]\n      ## Preparation\n      [3-5 tailored bullet points based on company + role + stage]\n    notion_update_page(page_id, content=structured_content)\n\n    IF interview_date is not null AND Google Calendar MCP is available:\n      ask user: \"Create a calendar event for <company> interview on <date>?\"\n      IF yes:\n        google_calendar_create_event(\n          summary  = \"<company> — <role> Interview\",\n          start    = interview_date + \"T09:00:00\",  # use extracted time if available\n          end      = interview_date + \"T10:00:00\",\n          description = \"Application tracked in 2026 Career Notion database.\"\n        )\n\nSTEP 6  Staleness detection\n  stale_threshold = today - 30 days\n  IF tracker_backend == \"notion\":\n    all_pages = notion_fetch(COLLECTION_URL)\n    stale = [p for p in all_pages\n             if p.status NOT IN (\"Rejected\", \"Withdrawn\", \"Offer\")\n             AND (p.last_touch < stale_threshold OR p.last_touch is null)\n             AND p.next_action != \"Follow up\"]\n    FOR each stale page:\n      notion_update_page(page_id, properties={\"Next action\": \"Follow up\"})\n\n  IF tracker_backend == \"sqlite\":\n    stale = sqlite3 DB_PATH \"SELECT name, status, last_touch FROM applications\n      WHERE status NOT IN ('Rejected','Withdrawn','Offer')\n      AND (date(COALESCE(last_touch, updated_at)) < date('now','-30 days'))\n      AND COALESCE(next_action,'') != 'Follow up'\"\n    FOR each stale row:\n      sqlite3 DB_PATH \"UPDATE applications SET next_action='Follow up',\n        updated_at=datetime('now') WHERE name=?\"\n\nSTEP 7  Report\n  IF no specific company was given (full sync):\n    IF tracker_backend == \"notion\":\n      counts = tally status values from all_pages (fetched in Step 6)\n    IF tracker_backend == \"sqlite\":\n      counts = sqlite3 DB_PATH \"SELECT status, COUNT(*) FROM applications GROUP BY status\"\n    print funnel summary line (see Report Format below)\n  print per-application summary (see Report Format below)\n```\n\n---\n\n## Query Builder\n\n```\nFUNCTION build_query(company_key, date_range=\"newer_than:6m\"):\n  cfg = COMPANY_CONFIG[company_key]\n  parts = []\n  IF cfg.sender:   parts.append(f\"from:{cfg.sender}\")\n  IF cfg.label_id: parts.append(f\"label:{cfg.label_id}\")\n  subject_terms = 'subject:application OR subject:interview OR subject:offer OR subject:position OR subject:assessment OR subject:next steps'\n  RETURN f'({\" OR \".join(parts)}) ({subject_terms}) {date_range}'\n```\n\nExamples:\n- Amazon with label → `(from:noreply@mail.amazon.jobs OR label:<label_id>) (subject:application OR ...) newer_than:6m`\n- Unknown company → fall back to `from:@<company>.com` or company name in subject\n\n---\n\n## Storage API Calls\n\n### Notion — Create (batch)\n```json\n{\n  \"data_source_id\": \"<NOTION_DB_ID>\",\n  \"pages\": [\n    {\n      \"properties\": {\n        \"Name\": \"Amazon — SDE, AWS\",\n        \"status\": \"Applied / Received\",\n        \"Company\": [\"Amazon\"],\n        \"Tags\": [\"Referral\"],\n        \"Applied on\": \"2026-05-17\",\n        \"Last touch\": \"2026-05-17\",\n        \"Interview date\": null,\n        \"Next action\": \"Waiting\",\n        \"Link\": \"https://boards.greenhouse.io/amazon/jobs/12345\"\n      },\n      \"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\"\n    }\n  ]\n}\n```\n\n### Notion — Update (single, status + dates + next action)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": {\n    \"status\": \"Interviewing\",\n    \"Last touch\": \"2026-05-28\",\n    \"Interview date\": \"2026-06-09\",\n    \"Next action\": \"Prep\"\n  },\n  \"content_updates\": []\n}\n```\n\n### Notion — Update (single, staleness flag)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": {\n    \"Next action\": \"Follow up\"\n  },\n  \"content_updates\": []\n}\n```\n\n### Notion — Update (single, full page enrichment)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": {\n    \"status\": \"Interviewing\",\n    \"Last touch\": \"2026-05-28\",\n    \"Next action\": \"Prep\"\n  },\n  \"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\"\n}\n```\n\n### Notion — Search (dedup)\n```json\n{\n  \"query\": \"Amazon — Software Development Engineer\",\n  \"data_source_url\": \"collection://<NOTION_DB_ID>\",\n  \"filters\": {}\n}\n```\n\n### SQLite — Create\n```bash\nswelist tracker add \"Amazon — SDE, AWS\" \\\n  --status \"Applied / Received\" \\\n  --job-id 10414382 \\\n  --applied-on 2026-05-17 \\\n  --db <DB_PATH>\n```\n\n### SQLite — Update\n```bash\nswelist tracker update \"Amazon — SDE, AWS\" \\\n  --status \"Interviewing\" \\\n  --db <DB_PATH>\n```\n\n### SQLite — Search (dedup)\n```bash\nswelist tracker get \"Amazon — SDE, AWS\" --db <DB_PATH>\n# Returns JSON object if found, null if not found. Exit code 1 when not found.\n```\n\n### SQLite — List all\n```bash\nswelist tracker list --db <DB_PATH>\nswelist tracker export --format json --db <DB_PATH>\n```\n\n### SQLite — Funnel summary query\n```bash\nsqlite3 ~/.offerplus/applications.db \\\n  \"SELECT status, COUNT(*) AS n FROM applications GROUP BY status ORDER BY n DESC;\"\n```\n\n### SQLite — Staleness query\n```bash\nsqlite3 ~/.offerplus/applications.db \\\n  \"SELECT name, status, COALESCE(last_touch, updated_at) AS last_activity\n   FROM applications\n   WHERE status NOT IN ('Rejected','Withdrawn','Offer')\n   AND date(COALESCE(last_touch, updated_at)) < date('now','-30 days')\n   ORDER BY last_activity ASC;\"\n```\n\n### Google Calendar — Create interview event\n```json\n{\n  \"summary\": \"Applied Intuition — Software Engineer Interview\",\n  \"start\": { \"dateTime\": \"2026-06-09T14:00:00\", \"timeZone\": \"America/Los_Angeles\" },\n  \"end\":   { \"dateTime\": \"2026-06-09T15:00:00\", \"timeZone\": \"America/Los_Angeles\" },\n  \"description\": \"Application tracked in 2026 Career Notion database.\"\n}\n```\n\n---\n\n## Naming Convention\n\n```\n\"{Company} — {Role Title}\"\n```\n\n| Good | Bad |\n|---|---|\n| `Amazon — Software Development Engineer, AWS` | `SDE at Amazon` |\n| `Google — Software Engineer II, Early Career` | `Google SWE` |\n| `Goldman Sachs — Software Engineer - Associate` | `Goldman` |\n\nRules:\n- Em dash (`—`), not hyphen\n- Company name in title case\n- Role title verbatim from the job posting or email subject when possible\n\n---\n\n## Report Format\n\n```\nPipeline: {N} Applied · {N} OA · {N} Interviewing · {N} Offer · {N} Rejected\n  (omit pipeline line when syncing a single company)\n\nSynced {N} application(s) on {date}  [{backend}: notion|sqlite]\n\nCreated:\n  ✦ {Company} — {Role} → {status}  [next: {next_action}]\n    🔗 {link}  (omit if not found)\n    🏷  Tags: {tag1}, {tag2}  (omit if none inferred)\n\nUpdated:\n  ↑ {Company} — {Role} → {new_status}  (was: {old_status})  [next: {next_action}]\n  + Page enriched: timeline, key contacts, prep notes added\n  📅 Interview date: {date}  (calendar event created / skipped)\n  🔗 Link extracted: {link}  (omit if not found)\n  🏷  Tags added: {tag1}, {tag2}  (omit if none)\n\nSkipped (already up to date):\n  · {Company} — {Role} → {status}\n\nFlagged as stale (no activity > 30 days → Follow up):\n  ⏰ {Company} — {Role}  [last touch: {date}]\n\nErrors:\n  ✕ {description of issue}\n```\n\n---\n\n## Edge Cases\n\n| Situation | Handling |\n|---|---|\n| Multiple threads for same role | Use most recent thread's status |\n| Role title not in email | Use job ID or \"Role\" as placeholder; note it in content/notes |\n| Company exists under a different name variant | Search by company name alone first, prompt user to confirm merge |\n| Email snippet enough to determine status | Skip full thread fetch for status; still fetch for page enrichment when company is given |\n| `Company` or `Tags` value not in Notion options list | Omit the field; do not create new options automatically |\n| Page name collision (same company, same title, different role) | Add disambiguator: `Amazon — SDE, AWS (Job ID 12345)` |\n| Notion DB ID or schema unknown | Fetch database URL with `notion-fetch` to inspect schema first |\n| Notion DB uses old 4-status schema | Map pipeline status to old values (see Config: Notion Database), notify user to upgrade |\n| SQLite DB does not exist | Run `swelist tracker init` or the CREATE TABLE statement before Step 3 |\n| SQLite DB path missing from profile.md | Use default `~/.offerplus/applications.db`; confirm with user |\n| Status would move backwards (e.g. Interviewing → Applied) | Keep existing status; only update Last touch date |\n| Key contacts not extractable from thread | Omit key contacts section; do not hallucinate names or emails |\n| No prep suggestions applicable | Omit Preparation section rather than generating generic advice |\n| Interview date ambiguous (e.g. \"sometime next week\") | Set interview_date to null; note ambiguity in conversation notes |\n| Interview date already in the past | Still write it to the page; do not create a calendar event for past dates |\n| Google Calendar MCP not connected | Skip calendar event creation; note it in the report |\n| SQLite DB exists but missing new columns (v0.2.x) | Run migration SQL automatically at Step 0 before proceeding |\n| Staleness check finds 0 stale rows | Omit the stale section from the report entirely |\n| next_action already \"Follow up\" | Skip staleness update for that row; it's already flagged |\n| ATS URL found but already saved in Link property | Do not overwrite existing value; preserve what was there |\n| Multiple ATS URLs in one email (e.g. Greenhouse + LinkedIn) | Use highest-priority match per Link Extraction table |\n| URL contains tracking params (`?gh_src=`, `?utm_*`) | Strip before saving; store clean canonical URL only |\n| Inferred tag not in Notion Tags options list | Silently omit; never create new Notion options |\n| Inferred tag is already on the page | Skip; do not duplicate |\n| Funnel counts fetched from Notion but all_pages is empty | Skip pipeline line; note \"0 applications tracked\" |\n| Funnel query when syncing a single company | Skip pipeline summary entirely |\n\nFile v1.0.10:_meta.json\n\n{\n  \"ownerId\": \"kn78p1g6xzcqm9fyegreqrft0n80bazp\",\n  \"slug\": \"job-application-manager\",\n  \"version\": \"1.0.10\",\n  \"publishedAt\": 1780150835948\n}\n\nFile v1.0.10:skill-card.md\n\n## Description: <br>\nSyncs job application emails from Gmail and updates statuses in a Notion or SQLite tracker, including offers, rejections, and interview invitations. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[chenyuan99](https://clawhub.ai/user/chenyuan99) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nExternal users use this skill to keep a job-application pipeline current by reading job-search emails, classifying application status, and syncing structured records to Notion or a local SQLite database. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill reads sensitive Gmail content while identifying job-application updates. <br>\nMitigation: Grant access only to the Gmail account and labels needed for job-search tracking, and review the first sync report before relying on bulk automation. <br>\nRisk: Extracted application details may be written to Notion, Google Calendar, SQLite, or local profile files. <br>\nMitigation: Verify the configured Notion database, Calendar connection, SQLite path, and profile settings before allowing writes. <br>\nRisk: Email classification or date extraction can misclassify a status, interview date, tag, or next action. <br>\nMitigation: Review proposed tracker updates, especially offers, rejections, interview dates, stale follow-up flags, and inferred tags. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/chenyuan99/job-application-manager) <br>\n- [Publisher profile](https://clawhub.ai/user/chenyuan99) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, shell commands, configuration, API calls] <br>\n**Output Format:** [Markdown reports with structured tracker updates, optional shell commands, and Notion, Gmail, Calendar, or SQLite operations.] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May create or update Notion pages, SQLite records, Google Calendar events, profile settings, and sync summaries based on user-approved configuration.] <br>\n\n## Skill Version(s): <br>\n1.0.10 (source: ClawHub release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.0.9: 3 files, 10007 bytes\n\nFiles: skill-card.md (2470b), SKILL.md (23551b), _meta.json (142b)\n\nFile v1.0.9:SKILL.md\n\n---\nname: Job Application Manager — Gmail & Notion Sync\ndescription: Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations\nsummary: >\n  Use this skill to sync your job applications automatically from Gmail\n  into Notion or a local SQLite database. It scans your inbox for emails\n  from companies you have applied to, classifies each message as an offer,\n  rejection, or interview invitation, and syncs the application status into\n  your tracker without any manual effort. Supports Gmail label filters and\n  sender-pattern matching for accurate company detection. Works with both\n  Notion (cloud career tracker) and SQLite (local, no account required).\n  Deduplicates entries so running it multiple times is safe. When syncing\n  a specific company, it also enriches the Notion page with a timeline,\n  conversation notes, key contacts, and preparation suggestions. Detects\n  stale applications (no email activity in 30+ days) and flags them for\n  follow-up. Extracts scheduled interview dates and optionally creates\n  Google Calendar events.\nversion: 0.3.0\nauthor: Yuan Chen\nrepository: https://github.com/chenyuan99/swelist\nkeywords:\n  - gmail\n  - notion\n  - job-applications\n  - application-tracker\n  - career-management\n  - offer-detection\n  - interview-tracker\n  - rejection-tracker\n  - job-status\n  - sync\n  - automation\n  - sqlite\n  - email-parsing\n  - career-tracker\ntags:\n  - career\n  - productivity\n  - gmail\n  - notion\n  - job-search\ncategory: career\nmetadata:\n  openclaw:\n    emoji: \"📋\"\n    requires:\n      bins: []\n      env: []\n      mcp:\n        - name: claude_ai_Gmail\n          reason: Search and read job application emails\n          optional: false\n        - name: claude_ai_Notion\n          reason: Create and update application entries in Notion\n          optional: true\n        - name: claude_ai_Google_Calendar\n          reason: Create calendar events for scheduled interviews\n          optional: true\n    config:\n      - key: tracker_backend\n        description: Storage backend — notion or sqlite\n        default: notion\n      - key: sqlite_db_path\n        description: Path to SQLite database (sqlite backend only)\n        default: ~/.offerplus/applications.db\n---\n\n# Application Manager\n\n## When to Use This Skill\n\nTrigger when the user asks to:\n\n- Sync or refresh job application emails from Gmail (\"sync my applications\", \"check my job emails\", \"refresh my tracker\")\n- Update the status of one or more specific applications (\"mark Amazon as rejected\", \"I got an offer from Stripe\", \"update my application status\")\n- Add new applications discovered in Gmail (\"add this application to my tracker\", \"log this job email\")\n- Check what emails arrived from a specific company (\"did Google email me back?\", \"any updates from Meta?\")\n- Track offers, rejections, or interview invitations automatically (\"did I hear back from anyone?\", \"what's my interview pipeline?\")\n- Sync applications into Notion (\"add to my Notion tracker\", \"update my Notion job board\")\n- Sync applications into a local SQLite database (\"add to my sqlite tracker\", \"update my local tracker\")\n- Export or review their full application pipeline (\"show my application pipeline\", \"what's the status of all my applications?\")\n- Update a specific company's page with conversation details, contacts, or prep notes\n- Flag stale applications that haven't had activity in 30+ days (\"what's gone quiet?\", \"flag stale apps\", \"what needs follow-up?\")\n\nKeywords: `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`\n\n---\n\n## Setup (first-time use)\n\n**Always read `profile.md` first.** It contains the user's tracker backend choice,\nNotion database ID or SQLite path, Gmail label IDs, and career email.\n\nResolve these fields before continuing (collect from user + write back to `profile.md`):\n\n1. **Tracker backend** — `Integrations > Tracker Backend`\n   - If blank: ask the user — \"Do you use Notion or would you prefer a local SQLite file?\"\n   - Set to `notion` or `sqlite`\n\n2. **If notion** — resolve `Integrations > Notion > Career tracker database ID`\n   - If missing: ask user to open their Notion career tracker and copy the URL.\n     The ID is the UUID in `notion.so/<workspace>/<DATABASE_ID>?v=...`\n   - Derive: `NOTION_DB_ID`, `COLLECTION_URL = collection://<NOTION_DB_ID>`\n\n3. **If sqlite** — resolve `Integrations > Tracker Backend > SQLite DB path`\n   - Default: `~/.offerplus/applications.db`\n   - On first use, initialize the DB: `swelist tracker init --db <path>`\n     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')));\"`\n\n4. **Gmail label IDs** — `Integrations > Gmail Labels` table\n   - If missing: run `mcp__claude_ai_Gmail__list_labels`, show the list, ask the user\n     which labels are job-related, fill in the table in `profile.md`.\n\n5. **Career email** — `Personal Info > Career email`\n   - If missing: ask the user which email address receives job application emails.\n\n---\n\n## Config: Company → Gmail\n\nPopulate from the user's own labels (`mcp__claude_ai_Gmail__list_labels`):\n\n| Company key | Gmail sender pattern | Gmail label ID | Label name |\n|---|---|---|---|\n| `amazon` | `noreply@mail.amazon.jobs` | _(run list_labels)_ | e.g. amazon |\n| `linkedin` | `jobs-noreply@linkedin.com` | _(run list_labels)_ | e.g. linkedin |\n| `google` | `@google.com` | — | — |\n| `meta` | `@meta.com` | — | — |\n| `_any_` | — | — | — |\n\nAdd rows for any other companies the user has labeled. If no labels exist, rely on sender pattern alone.\n\n---\n\n## Config: Status Mapping\n\nMap email signals → pipeline status value (pick the **first** match).\nThe pipeline is ordered from early to late stage; never move a status *backwards*.\n\n| Priority | Signal (case-insensitive) | Status |\n|---|---|---|\n| 1 | \"offer\" OR \"congratulations\" OR \"pleased to inform\" OR \"we'd like to extend\" OR \"offer letter\" | `Offer` |\n| 2 | \"on-site\" OR \"final round\" OR \"technical interview\" OR \"virtual interview\" OR \"interview loop\" | `Interviewing` |\n| 3 | \"schedule\" OR \"next steps\" OR \"move forward\" OR \"hiring manager\" OR \"phone interview\" OR \"video call\" | `Interviewing` |\n| 4 | \"online assessment\" OR \"coding challenge\" OR \"hackerrank\" OR \"codility\" OR \"take-home\" OR \"assessment link\" | `OA` |\n| 5 | \"recruiter\" OR \"phone screen\" OR \"initial screen\" OR \"introductory call\" OR \"talent acquisition\" | `Recruiter screen` |\n| 6 | \"unable to move forward\" OR \"not selected\" OR \"no longer considering\" OR \"other candidates\" OR \"position has been filled\" OR \"not moving forward\" | `Rejected` |\n| 7 | \"application received\" OR \"thank you for applying\" OR \"keep track\" OR \"under review\" OR \"successfully submitted\" | `Applied / Received` |\n| 8 | (no email found, only a job listing) | `Applied / Received` |\n\nIf multiple threads exist for the same role, use the **most recent** email's status.\n\n---\n\n## Config: Next Action Mapping\n\nAuto-set `Next action` when creating or updating a Notion page:\n\n| Status | Next action |\n|---|---|\n| `Applied / Received` | `Waiting` |\n| `OA` | `Prep` |\n| `Recruiter screen` | `Prep` |\n| `Interviewing` | `Prep` |\n| `Offer` | `Send availability` |\n| `Rejected` | _(leave blank / clear)_ |\n| `Withdrawn` | _(leave blank / clear)_ |\n\n---\n\n## Config: Notion Database\n\n_(skip if tracker_backend is sqlite)_\n\n| Field | Value |\n|---|---|\n| Collection URL | `collection://<NOTION_DB_ID>` |\n| Parent ID (for creates) | `<NOTION_DB_ID>` |\n\nExpected schema (updated pipeline schema):\n\n| Property | Type | Allowed values / notes |\n|---|---|---|\n| `Name` | title | `\"Company — Role Title\"` |\n| `status` | select | `Applied / Received`, `OA`, `Recruiter screen`, `Interviewing`, `Offer`, `Rejected`, `Withdrawn` |\n| `Company` | multi-select | Company name (e.g. `Google`, `Amazon`) |\n| `Tags` | multi-select | Labels like `Referral`, `Remote`, `NYC`, `Top choice`, `Visa`, `OA`, `Onsite` |\n| `Applied on` | date | ISO date when first applied |\n| `Last touch` | date | Date of most recent email in the thread |\n| `Interview date` | date | Parsed date of the next scheduled interview (if any) |\n| `Next action` | select | `Waiting`, `Prep`, `Follow up`, `Send availability` |\n| `Link` | url | Job posting URL or Greenhouse/application portal link |\n\n**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:\n\n| Old value | Maps to in old schema |\n|---|---|\n| `Applied / Received` | `In progress` |\n| `OA` | `In progress` |\n| `Recruiter screen` | `In progress` |\n| `Interviewing` | `In progress` |\n| `Offer` | `Done` |\n| `Rejected` | `Rejected` |\n| `Withdrawn` | `Rejected` |\n\n---\n\n## Config: SQLite Schema\n\n_(skip if tracker_backend is notion)_\n\n**Current (v0.3.0) schema:**\n\n```sql\nCREATE TABLE IF NOT EXISTS applications (\n  name           TEXT PRIMARY KEY,   -- \"Company — Role Title\"\n  status         TEXT NOT NULL,      -- pipeline status (see Status Mapping)\n  job_id         TEXT,\n  company        TEXT,\n  applied_on     TEXT,               -- ISO date YYYY-MM-DD\n  last_touch     TEXT,               -- ISO date of most recent email\n  interview_date TEXT,               -- ISO date of next scheduled interview\n  next_action    TEXT,               -- Waiting | Prep | Follow up | Send availability\n  link           TEXT,               -- job posting or portal URL\n  notes          TEXT,\n  updated_at     TEXT DEFAULT (datetime('now'))\n);\n```\n\n**Migration from v0.2.x** (run once if the DB already exists):\n\n```sql\nALTER TABLE applications ADD COLUMN company        TEXT;\nALTER TABLE applications ADD COLUMN last_touch     TEXT;\nALTER TABLE applications ADD COLUMN interview_date TEXT;\nALTER TABLE applications ADD COLUMN next_action    TEXT;\nALTER TABLE applications ADD COLUMN link           TEXT;\n```\n\nRun via: `sqlite3 <DB_PATH> < migration.sql`\nOr inline: `sqlite3 ~/.offerplus/applications.db \"ALTER TABLE applications ADD COLUMN last_touch TEXT; ...\"`\n\nAt Step 0, check whether `last_touch` column exists (`PRAGMA table_info(applications)`) and run the migration automatically if not.\n\n---\n\n## Workflow\n\n```\nINPUT: company (optional), date_range (default: newer_than:6m)\n\nSTEP 0  Load profile\n  Read profile.md → extract tracker_backend, CAREER_EMAIL, label table\n  IF tracker_backend == \"notion\":  resolve NOTION_DB_ID, COLLECTION_URL\n  IF tracker_backend == \"sqlite\":\n    resolve DB_PATH; run init if DB does not exist\n    check PRAGMA table_info(applications) for last_touch column\n    IF missing: run migration SQL (see Config: SQLite Schema)\n  IF any required field blank: collect from user → write to profile.md → continue\n  IF company given AND not in label table:\n    run list_labels → confirm with user → append to profile.md label table\n\nSTEP 1  Search Gmail\n  IF company given:\n    query = build_query(company)          # see Query Builder below\n  ELSE:\n    query = 'subject:\"application\" OR subject:\"your application\" OR subject:\"interview\" newer_than:6m'\n  threads = gmail_search(query, max_results=20)\n  FOR each thread WHERE snippet is ambiguous:\n    fetch full thread via gmail_get_thread(thread_id)\n\nSTEP 2  Parse threads → applications[]\n  FOR each thread:\n    company_name   = extract_company(thread)\n    role_title     = extract_role(thread)\n    status         = map_status(thread)         # use Status Mapping table above\n    next_action    = map_next_action(status)    # use Next Action Mapping table above\n    date           = most_recent_message_date(thread)\n    applied_date   = earliest_message_date(thread)\n    job_id         = extract_job_id(thread)     # if present in email\n    interview_date = extract_interview_date(thread)\n                     # scan body for patterns: \"June 9\", \"Monday June 9 at 2pm PT\",\n                     # \"scheduled for <date>\", \"your interview is on <date>\"\n                     # normalise to ISO date YYYY-MM-DD; set null if not found\n    page_name      = f\"{company_name} — {role_title}\"\n    APPEND { page_name, company_name, status, next_action, date, applied_date,\n             job_id, interview_date, thread_id }\n\nSTEP 3  Deduplicate\n  IF tracker_backend == \"notion\":\n    FOR each application:\n      existing = notion_search(query=application.page_name,\n                               data_source_url=COLLECTION_URL)\n      IF found: set existing_status, action = \"update\" or \"skip\"\n      ELSE:     action = \"create\"\n\n  IF tracker_backend == \"sqlite\":\n    FOR each application:\n      result = swelist tracker get \"<page_name>\" --db DB_PATH\n      IF result is not null: set existing_status, action = \"update\" or \"skip\"\n      ELSE:                   action = \"create\"\n\nSTEP 4  Apply changes\n  IF tracker_backend == \"notion\":\n    creates → notion_create_pages(batch) with all new fields incl. Interview date\n    updates → notion_update_page per entry with status, Last touch, Next action,\n              Interview date (if extracted)\n\n  IF tracker_backend == \"sqlite\":\n    FOR each create:\n      sqlite3 DB_PATH \"INSERT OR IGNORE INTO applications\n        (name,status,company,job_id,applied_on,last_touch,interview_date,next_action)\n        VALUES (?,?,?,?,?,?,?,?)\"\n    FOR each update:\n      sqlite3 DB_PATH \"UPDATE applications SET status=?, last_touch=?,\n        next_action=?, interview_date=COALESCE(?,interview_date),\n        updated_at=datetime('now') WHERE name=?\"\n\nSTEP 5  Enrich page content (Notion only, when company is given OR when status changed)\n  FOR each application that was created or updated (and tracker is notion):\n    fetch full thread via gmail_get_thread(thread_id)\n    extract:\n      timeline[]       = list of { date, event_summary } sorted chronologically\n      key_contacts[]   = list of { name, email, role } from From/Cc headers\n      conversation[]   = key quotes or summaries from each email in thread\n      prep_suggestions = generate 3-5 bullet prep suggestions based on status + role\n    Build structured page content:\n      ## Timeline\n      | Date | Event |\n      ...\n      ## Key Contacts\n      | Name | Email | Role |\n      ...\n      ## Conversation Notes\n      [bullet summary of each email in thread]\n      ## Preparation\n      [3-5 tailored bullet points based on company + role + stage]\n    notion_update_page(page_id, content=structured_content)\n\n    IF interview_date is not null AND Google Calendar MCP is available:\n      ask user: \"Create a calendar event for <company> interview on <date>?\"\n      IF yes:\n        google_calendar_create_event(\n          summary  = \"<company> — <role> Interview\",\n          start    = interview_date + \"T09:00:00\",  # use extracted time if available\n          end      = interview_date + \"T10:00:00\",\n          description = \"Application tracked in 2026 Career Notion database.\"\n        )\n\nSTEP 6  Staleness detection\n  stale_threshold = today - 30 days\n  IF tracker_backend == \"notion\":\n    all_pages = notion_fetch(COLLECTION_URL)\n    stale = [p for p in all_pages\n             if p.status NOT IN (\"Rejected\", \"Withdrawn\", \"Offer\")\n             AND (p.last_touch < stale_threshold OR p.last_touch is null)\n             AND p.next_action != \"Follow up\"]\n    FOR each stale page:\n      notion_update_page(page_id, properties={\"Next action\": \"Follow up\"})\n\n  IF tracker_backend == \"sqlite\":\n    stale = sqlite3 DB_PATH \"SELECT name, status, last_touch FROM applications\n      WHERE status NOT IN ('Rejected','Withdrawn','Offer')\n      AND (date(COALESCE(last_touch, updated_at)) < date('now','-30 days'))\n      AND COALESCE(next_action,'') != 'Follow up'\"\n    FOR each stale row:\n      sqlite3 DB_PATH \"UPDATE applications SET next_action='Follow up',\n        updated_at=datetime('now') WHERE name=?\"\n\nSTEP 7  Report\n  print summary (see Report Format below)\n```\n\n---\n\n## Query Builder\n\n```\nFUNCTION build_query(company_key, date_range=\"newer_than:6m\"):\n  cfg = COMPANY_CONFIG[company_key]\n  parts = []\n  IF cfg.sender:   parts.append(f\"from:{cfg.sender}\")\n  IF cfg.label_id: parts.append(f\"label:{cfg.label_id}\")\n  subject_terms = 'subject:application OR subject:interview OR subject:offer OR subject:position OR subject:assessment OR subject:next steps'\n  RETURN f'({\" OR \".join(parts)}) ({subject_terms}) {date_range}'\n```\n\nExamples:\n- Amazon with label → `(from:noreply@mail.amazon.jobs OR label:<label_id>) (subject:application OR ...) newer_than:6m`\n- Unknown company → fall back to `from:@<company>.com` or company name in subject\n\n---\n\n## Storage API Calls\n\n### Notion — Create (batch)\n```json\n{\n  \"data_source_id\": \"<NOTION_DB_ID>\",\n  \"pages\": [\n    {\n      \"properties\": {\n        \"Name\": \"Amazon — SDE, AWS\",\n        \"status\": \"Applied / Received\",\n        \"Company\": [\"Amazon\"],\n        \"Applied on\": \"2026-05-17\",\n        \"Last touch\": \"2026-05-17\",\n        \"Interview date\": null,\n        \"Next action\": \"Waiting\"\n      },\n      \"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\"\n    }\n  ]\n}\n```\n\n### Notion — Update (single, status + dates + next action)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": {\n    \"status\": \"Interviewing\",\n    \"Last touch\": \"2026-05-28\",\n    \"Interview date\": \"2026-06-09\",\n    \"Next action\": \"Prep\"\n  },\n  \"content_updates\": []\n}\n```\n\n### Notion — Update (single, staleness flag)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": {\n    \"Next action\": \"Follow up\"\n  },\n  \"content_updates\": []\n}\n```\n\n### Notion — Update (single, full page enrichment)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": {\n    \"status\": \"Interviewing\",\n    \"Last touch\": \"2026-05-28\",\n    \"Next action\": \"Prep\"\n  },\n  \"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\"\n}\n```\n\n### Notion — Search (dedup)\n```json\n{\n  \"query\": \"Amazon — Software Development Engineer\",\n  \"data_source_url\": \"collection://<NOTION_DB_ID>\",\n  \"filters\": {}\n}\n```\n\n### SQLite — Create\n```bash\nswelist tracker add \"Amazon — SDE, AWS\" \\\n  --status \"Applied / Received\" \\\n  --job-id 10414382 \\\n  --applied-on 2026-05-17 \\\n  --db <DB_PATH>\n```\n\n### SQLite — Update\n```bash\nswelist tracker update \"Amazon — SDE, AWS\" \\\n  --status \"Interviewing\" \\\n  --db <DB_PATH>\n```\n\n### SQLite — Search (dedup)\n```bash\nswelist tracker get \"Amazon — SDE, AWS\" --db <DB_PATH>\n# Returns JSON object if found, null if not found. Exit code 1 when not found.\n```\n\n### SQLite — List all\n```bash\nswelist tracker list --db <DB_PATH>\nswelist tracker export --format json --db <DB_PATH>\n```\n\n### SQLite — Staleness query\n```bash\nsqlite3 ~/.offerplus/applications.db \\\n  \"SELECT name, status, COALESCE(last_touch, updated_at) AS last_activity\n   FROM applications\n   WHERE status NOT IN ('Rejected','Withdrawn','Offer')\n   AND date(COALESCE(last_touch, updated_at)) < date('now','-30 days')\n   ORDER BY last_activity ASC;\"\n```\n\n### Google Calendar — Create interview event\n```json\n{\n  \"summary\": \"Applied Intuition — Software Engineer Interview\",\n  \"start\": { \"dateTime\": \"2026-06-09T14:00:00\", \"timeZone\": \"America/Los_Angeles\" },\n  \"end\":   { \"dateTime\": \"2026-06-09T15:00:00\", \"timeZone\": \"America/Los_Angeles\" },\n  \"description\": \"Application tracked in 2026 Career Notion database.\"\n}\n```\n\n---\n\n## Naming Convention\n\n```\n\"{Company} — {Role Title}\"\n```\n\n| Good | Bad |\n|---|---|\n| `Amazon — Software Development Engineer, AWS` | `SDE at Amazon` |\n| `Google — Software Engineer II, Early Career` | `Google SWE` |\n| `Goldman Sachs — Software Engineer - Associate` | `Goldman` |\n\nRules:\n- Em dash (`—`), not hyphen\n- Company name in title case\n- Role title verbatim from the job posting or email subject when possible\n\n---\n\n## Report Format\n\n```\nSynced {N} application(s) on {date}  [{backend}: notion|sqlite]\n\nCreated:\n  ✦ {Company} — {Role} → {status}  [next: {next_action}]\n\nUpdated:\n  ↑ {Company} — {Role} → {new_status}  (was: {old_status})  [next: {next_action}]\n  + Page enriched: timeline, key contacts, prep notes added\n  📅 Interview date: {date}  (calendar event created / skipped)\n\nSkipped (already up to date):\n  · {Company} — {Role} → {status}\n\nFlagged as stale (no activity > 30 days → Follow up):\n  ⏰ {Company} — {Role}  [last touch: {date}]\n\nErrors:\n  ✕ {description of issue}\n```\n\n---\n\n## Edge Cases\n\n| Situation | Handling |\n|---|---|\n| Multiple threads for same role | Use most recent thread's status |\n| Role title not in email | Use job ID or \"Role\" as placeholder; note it in content/notes |\n| Company exists under a different name variant | Search by company name alone first, prompt user to confirm merge |\n| Email snippet enough to determine status | Skip full thread fetch for status; still fetch for page enrichment when company is given |\n| `Company` or `Tags` value not in Notion options list | Omit the field; do not create new options automatically |\n| Page name collision (same company, same title, different role) | Add disambiguator: `Amazon — SDE, AWS (Job ID 12345)` |\n| Notion DB ID or schema unknown | Fetch database URL with `notion-fetch` to inspect schema first |\n| Notion DB uses old 4-status schema | Map pipeline status to old values (see Config: Notion Database), notify user to upgrade |\n| SQLite DB does not exist | Run `swelist tracker init` or the CREATE TABLE statement before Step 3 |\n| SQLite DB path missing from profile.md | Use default `~/.offerplus/applications.db`; confirm with user |\n| Status would move backwards (e.g. Interviewing → Applied) | Keep existing status; only update Last touch date |\n| Key contacts not extractable from thread | Omit key contacts section; do not hallucinate names or emails |\n| No prep suggestions applicable | Omit Preparation section rather than generating generic advice |\n| Interview date ambiguous (e.g. \"sometime next week\") | Set interview_date to null; note ambiguity in conversation notes |\n| Interview date already in the past | Still write it to the page; do not create a calendar event for past dates |\n| Google Calendar MCP not connected | Skip calendar event creation; note it in the report |\n| SQLite DB exists but missing new columns (v0.2.x) | Run migration SQL automatically at Step 0 before proceeding |\n| Staleness check finds 0 stale rows | Omit the stale section from the report entirely |\n| next_action already \"Follow up\" | Skip staleness update for that row; it's already flagged |\n\nFile v1.0.9:_meta.json\n\n{\n  \"ownerId\": \"kn78p1g6xzcqm9fyegreqrft0n80bazp\",\n  \"slug\": \"job-application-manager\",\n  \"version\": \"1.0.9\",\n  \"publishedAt\": 1780150419448\n}\n\nFile v1.0.9:skill-card.md\n\n## Description: <br>\nSyncs job application emails from Gmail and updates statuses in a Notion or SQLite tracker, detecting offers, rejections, and interview invitations. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[chenyuan99](https://clawhub.ai/user/chenyuan99) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nJob seekers use this skill to keep an application tracker current from Gmail activity. It can classify application emails, update Notion or SQLite records, enrich Notion pages with timelines and notes, flag stale applications, and optionally create interview calendar events. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill reads job-related Gmail messages, which may expose sensitive personal and employment information. <br>\nMitigation: Confirm the Gmail labels and search scope before first use, and limit syncs to job-related messages. <br>\nRisk: The skill can write to Notion, SQLite, Google Calendar, and profile.md, which may create or change personal records. <br>\nMitigation: Preview proposed creates and updates before applying them, and confirm the SQLite path and target Notion database. <br>\nRisk: The security evidence notes under-declared local command use. <br>\nMitigation: Review shell commands and database migrations before execution, especially commands that initialize or alter SQLite data. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/chenyuan99/job-application-manager) <br>\n- [ClawHub publisher profile](https://clawhub.ai/user/chenyuan99) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration, API calls, Guidance] <br>\n**Output Format:** [Markdown summaries with structured tracker updates, optional shell commands, and API-call payloads] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May update Gmail-derived application status data in Notion, SQLite, Google Calendar, and profile.md when configured by the user.] <br>\n\n## Skill Version(s): <br>\n1.0.9 (source: server release metadata; source skill frontmatter states 0.3.0) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.0.8: 3 files, 6614 bytes\n\nFiles: skill-card.md (2221b), SKILL.md (12962b), _meta.json (142b)\n\nFile v1.0.8:SKILL.md\n\n---\nname: Job Application Manager — Gmail & Notion Sync\ndescription: Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations\nsummary: >\n  Use this skill to sync your job applications automatically from Gmail\n  into Notion or a local SQLite database. It scans your inbox for emails\n  from companies you have applied to, classifies each message as an offer,\n  rejection, or interview invitation, and syncs the application status into\n  your tracker without any manual effort. Supports Gmail label filters and\n  sender-pattern matching for accurate company detection. Works with both\n  Notion (cloud career tracker) and SQLite (local, no account required).\n  Deduplicates entries so running it multiple times is safe. Ideal for\n  keeping a job pipeline, career dashboard, or application spreadsheet\n  up to date directly from your email inbox.\nversion: 0.1.9\nauthor: Yuan Chen\nrepository: https://github.com/chenyuan99/swelist\nkeywords:\n  - gmail\n  - notion\n  - job-applications\n  - application-tracker\n  - career-management\n  - offer-detection\n  - interview-tracker\n  - rejection-tracker\n  - job-status\n  - sync\n  - automation\n  - sqlite\n  - email-parsing\n  - career-tracker\ntags:\n  - career\n  - productivity\n  - gmail\n  - notion\n  - job-search\ncategory: career\nmetadata:\n  openclaw:\n    emoji: \"📋\"\n    requires:\n      bins: []\n      env: []\n      mcp:\n        - name: claude_ai_Gmail\n          reason: Search and read job application emails\n          optional: false\n        - name: claude_ai_Notion\n          reason: Create and update application entries in Notion\n          optional: true\n    config:\n      - key: tracker_backend\n        description: Storage backend — notion or sqlite\n        default: notion\n      - key: sqlite_db_path\n        description: Path to SQLite database (sqlite backend only)\n        default: ~/.offerplus/applications.db\n---\n\n# Application Manager\n\n## When to Use This Skill\n\nTrigger when the user asks to:\n\n- Sync or refresh job application emails from Gmail (\"sync my applications\", \"check my job emails\", \"refresh my tracker\")\n- Update the status of one or more specific applications (\"mark Amazon as rejected\", \"I got an offer from Stripe\", \"update my application status\")\n- Add new applications discovered in Gmail (\"add this application to my tracker\", \"log this job email\")\n- Check what emails arrived from a specific company (\"did Google email me back?\", \"any updates from Meta?\")\n- Track offers, rejections, or interview invitations automatically (\"did I hear back from anyone?\", \"what's my interview pipeline?\")\n- Sync applications into Notion (\"add to my Notion tracker\", \"update my Notion job board\")\n- Sync applications into a local SQLite database (\"add to my sqlite tracker\", \"update my local tracker\")\n- Export or review their full application pipeline (\"show my application pipeline\", \"what's the status of all my applications?\")\n\nKeywords: `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`\n\n---\n\n## Setup (first-time use)\n\n**Always read `profile.md` first.** It contains the user's tracker backend choice,\nNotion database ID or SQLite path, Gmail label IDs, and career email.\n\nResolve these fields before continuing (collect from user + write back to `profile.md`):\n\n1. **Tracker backend** — `Integrations > Tracker Backend`\n   - If blank: ask the user — \"Do you use Notion or would you prefer a local SQLite file?\"\n   - Set to `notion` or `sqlite`\n\n2. **If notion** — resolve `Integrations > Notion > Career tracker database ID`\n   - If missing: ask user to open their Notion career tracker and copy the URL.\n     The ID is the UUID in `notion.so/<workspace>/<DATABASE_ID>?v=...`\n   - Derive: `NOTION_DB_ID`, `COLLECTION_URL = collection://<NOTION_DB_ID>`\n\n3. **If sqlite** — resolve `Integrations > Tracker Backend > SQLite DB path`\n   - Default: `~/.offerplus/applications.db`\n   - On first use, initialize the DB: `swelist tracker init --db <path>`\n     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')));\"`\n\n4. **Gmail label IDs** — `Integrations > Gmail Labels` table\n   - If missing: run `mcp__claude_ai_Gmail__list_labels`, show the list, ask the user\n     which labels are job-related, fill in the table in `profile.md`.\n\n5. **Career email** — `Personal Info > Career email`\n   - If missing: ask the user which email address receives job application emails.\n\n---\n\n## Config: Company → Gmail\n\nPopulate from the user's own labels (`mcp__claude_ai_Gmail__list_labels`):\n\n| Company key | Gmail sender pattern | Gmail label ID | Label name |\n|---|---|---|---|\n| `amazon` | `noreply@mail.amazon.jobs` | _(run list_labels)_ | e.g. amazon |\n| `linkedin` | `jobs-noreply@linkedin.com` | _(run list_labels)_ | e.g. linkedin |\n| `google` | `@google.com` | — | — |\n| `meta` | `@meta.com` | — | — |\n| `_any_` | — | — | — |\n\nAdd rows for any other companies the user has labeled. If no labels exist, rely on sender pattern alone.\n\n---\n\n## Config: Status Mapping\n\nMap email signals → status value (pick the **first** match):\n\n| Priority | Signal (case-insensitive) | Status |\n|---|---|---|\n| 1 | \"offer\" OR \"congratulations\" OR \"pleased to inform\" OR \"we'd like to extend\" | `Done` |\n| 2 | \"interview\" OR \"move forward\" OR \"next steps\" OR \"schedule\" OR \"hiring manager\" | `In progress` |\n| 3 | \"unable to move forward\" OR \"not selected\" OR \"no longer considering\" OR \"other candidates\" OR \"position has been filled\" | `Rejected` |\n| 4 | \"application received\" OR \"thank you for applying\" OR \"keep track\" OR \"under review\" | `In progress` |\n| 5 | (no email found, only a job listing) | `Not started` |\n\nIf multiple threads exist for the same role, use the **most recent** email's status.\n\n---\n\n## Config: Notion Database\n\n_(skip if tracker_backend is sqlite)_\n\n| Field | Value |\n|---|---|\n| Collection URL | `collection://<NOTION_DB_ID>` |\n| Parent ID (for creates) | `<NOTION_DB_ID>` |\n\nExpected schema:\n\n| Property | Type | Allowed values |\n|---|---|---|\n| `Name` | title | `\"Company — Role Title\"` |\n| `status` | select | `Not started`, `In progress`, `Rejected`, `Done` |\n| `Tags` | multi-select | Only use values already in the options list |\n\n---\n\n## Config: SQLite Schema\n\n_(skip if tracker_backend is notion)_\n\n```sql\nCREATE TABLE IF NOT EXISTS applications (\n  name        TEXT PRIMARY KEY,   -- \"Company — Role Title\"\n  status      TEXT NOT NULL,      -- Not started | In progress | Rejected | Done\n  job_id      TEXT,\n  applied_on  TEXT,               -- ISO date YYYY-MM-DD\n  notes       TEXT,\n  updated_at  TEXT DEFAULT (datetime('now'))\n);\n```\n\n---\n\n## Workflow\n\n```\nINPUT: company (optional), date_range (default: newer_than:6m)\n\nSTEP 0  Load profile\n  Read profile.md → extract tracker_backend, CAREER_EMAIL, label table\n  IF tracker_backend == \"notion\":  resolve NOTION_DB_ID, COLLECTION_URL\n  IF tracker_backend == \"sqlite\":  resolve DB_PATH; run init if DB does not exist\n  IF any required field blank: collect from user → write to profile.md → continue\n  IF company given AND not in label table:\n    run list_labels → confirm with user → append to profile.md label table\n\nSTEP 1  Search Gmail\n  IF company given:\n    query = build_query(company)          # see Query Builder below\n  ELSE:\n    query = 'subject:\"application\" OR subject:\"your application\" OR subject:\"interview\" newer_than:6m'\n  threads = gmail_search(query, max_results=20)\n  FOR each thread WHERE snippet is ambiguous:\n    fetch full thread via gmail_get_thread(thread_id)\n\nSTEP 2  Parse threads → applications[]\n  FOR each thread:\n    company_name = extract_company(thread)\n    role_title   = extract_role(thread)\n    status       = map_status(thread)       # use Status Mapping table above\n    date         = most_recent_message_date(thread)\n    job_id       = extract_job_id(thread)   # if present in email\n    page_name    = f\"{company_name} — {role_title}\"\n    APPEND { page_name, status, date, job_id, thread_id }\n\nSTEP 3  Deduplicate\n  IF tracker_backend == \"notion\":\n    FOR each application:\n      existing = notion_search(query=application.page_name,\n                               data_source_url=COLLECTION_URL)\n      IF found: set existing_status, action = \"update\" or \"skip\"\n      ELSE:     action = \"create\"\n\n  IF tracker_backend == \"sqlite\":\n    FOR each application:\n      result = swelist tracker get \"<page_name>\" --db DB_PATH\n      IF result is not null: set existing_status, action = \"update\" or \"skip\"\n      ELSE:                   action = \"create\"\n\nSTEP 4  Apply changes\n  IF tracker_backend == \"notion\":\n    creates → notion_create_pages(batch)\n    updates → notion_update_page per entry\n\n  IF tracker_backend == \"sqlite\":\n    FOR each create:\n      swelist tracker add \"<name>\" --status <status> --job-id <id> --applied-on <date> --db DB_PATH\n    FOR each update:\n      swelist tracker update \"<name>\" --status <status> --db DB_PATH\n\nSTEP 5  Report\n  print summary (see Report Format below)\n```\n\n---\n\n## Query Builder\n\n```\nFUNCTION build_query(company_key, date_range=\"newer_than:6m\"):\n  cfg = COMPANY_CONFIG[company_key]\n  parts = []\n  IF cfg.sender:   parts.append(f\"from:{cfg.sender}\")\n  IF cfg.label_id: parts.append(f\"label:{cfg.label_id}\")\n  subject_terms = 'subject:application OR subject:interview OR subject:offer OR subject:position'\n  RETURN f'({\" OR \".join(parts)}) ({subject_terms}) {date_range}'\n```\n\nExamples:\n- Amazon with label → `(from:noreply@mail.amazon.jobs OR label:<label_id>) (subject:application OR ...) newer_than:6m`\n- Unknown company → fall back to `from:@<company>.com` or company name in subject\n\n---\n\n## Storage API Calls\n\n### Notion — Create (batch)\n```json\n{\n  \"data_source_id\": \"<NOTION_DB_ID>\",\n  \"pages\": [\n    {\n      \"properties\": { \"Name\": \"Amazon — SDE, AWS\", \"status\": \"In progress\" },\n      \"content\": \"Applied: <date>\"\n    }\n  ]\n}\n```\n\n### Notion — Update (single)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": { \"status\": \"Rejected\" },\n  \"content_updates\": []\n}\n```\n\n### Notion — Search (dedup)\n```json\n{\n  \"query\": \"Amazon — Software Development Engineer\",\n  \"data_source_url\": \"collection://<NOTION_DB_ID>\",\n  \"filters\": {}\n}\n```\n\n### SQLite — Create\n```bash\nswelist tracker add \"Amazon — SDE, AWS\" \\\n  --status \"In progress\" \\\n  --job-id 10414382 \\\n  --applied-on 2026-05-17 \\\n  --db <DB_PATH>\n```\n\n### SQLite — Update\n```bash\nswelist tracker update \"Amazon — SDE, AWS\" \\\n  --status \"Rejected\" \\\n  --db <DB_PATH>\n```\n\n### SQLite — Search (dedup)\n```bash\nswelist tracker get \"Amazon — SDE, AWS\" --db <DB_PATH>\n# Returns JSON object if found, null if not found. Exit code 1 when not found.\n```\n\n### SQLite — List all\n```bash\nswelist tracker list --db <DB_PATH>\nswelist tracker export --format json --db <DB_PATH>\n```\n\n---\n\n## Naming Convention\n\n```\n\"{Company} — {Role Title}\"\n```\n\n| Good | Bad |\n|---|---|\n| `Amazon — Software Development Engineer, AWS` | `SDE at Amazon` |\n| `Google — Software Engineer II, Early Career` | `Google SWE` |\n| `Goldman Sachs — Software Engineer - Associate` | `Goldman` |\n\nRules:\n- Em dash (`—`), not hyphen\n- Company name in title case\n- Role title verbatim from the job posting or email subject when possible\n\n---\n\n## Report Format\n\n```\nSynced {N} application(s) on {date}  [{backend}: notion|sqlite]\n\nCreated:\n  ✦ {Company} — {Role} → {status}\n\nUpdated:\n  ↑ {Company} — {Role} → {new_status}  (was: {old_status})\n\nSkipped (already up to date):\n  · {Company} — {Role} → {status}\n\nErrors:\n  ✕ {description of issue}\n```\n\n---\n\n## Edge Cases\n\n| Situation | Handling |\n|---|---|\n| Multiple threads for same role | Use most recent thread's status |\n| Role title not in email | Use job ID or \"Role\" as placeholder; note it in content/notes |\n| Company exists under a different name variant | Search by company name alone first, prompt user to confirm merge |\n| Email snippet enough to determine status | Skip full thread fetch to reduce API calls |\n| `Tags` value not in Notion options list | Omit `Tags`; do not create new options |\n| Page name collision (same company, same title, different role) | Add disambiguator: `Amazon — SDE, AWS (Job ID 12345)` |\n| Notion DB ID or schema unknown | Fetch database URL with `notion-fetch` to inspect schema first |\n| SQLite DB does not exist | Run `swelist tracker init` or the CREATE TABLE statement before Step 3 |\n| SQLite DB path missing from profile.md | Use default `~/.offerplus/applications.db`; confirm with user |\n\nFile v1.0.8:_meta.json\n\n{\n  \"ownerId\": \"kn78p1g6xzcqm9fyegreqrft0n80bazp\",\n  \"slug\": \"job-application-manager\",\n  \"version\": \"1.0.8\",\n  \"publishedAt\": 1779393653657\n}\n\nFile v1.0.8:skill-card.md\n\n## Description: <br>\nSyncs job application emails from Gmail and updates statuses in a Notion or SQLite tracker, detecting offers, rejections, and interview invitations. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[chenyuan99](https://clawhub.ai/user/chenyuan99) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nJob seekers and career-focused users use this skill to scan job-related Gmail messages, classify application updates, and keep a Notion or local SQLite application tracker current. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill reads job-related Gmail messages and updates a Notion or SQLite application tracker. <br>\nMitigation: Install only after confirming the Gmail labels or sender patterns, tracker destination, and intended storage backend. <br>\nRisk: Email classification can incorrectly change an application's status. <br>\nMitigation: Review the sync summary and correct any offer, rejection, or interview status before relying on the tracker. <br>\nRisk: Setup details such as Gmail label IDs, tracker destination, and local database path may be retained in profile.md. <br>\nMitigation: Decide whether profile.md should retain setup details for future runs and remove sensitive details when persistence is not desired. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/chenyuan99/job-application-manager) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Guidance, Markdown, API Calls, Shell commands, Configuration] <br>\n**Output Format:** [Markdown guidance with inline JSON, SQL, and shell command examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Produces application sync summaries and tracker update instructions for Gmail, Notion, and SQLite workflows.] <br>\n\n## Skill Version(s): <br>\n1.0.8 (source: server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.0.7: 2 files, 5443 bytes\n\nFiles: SKILL.md (12948b), _meta.json (142b)\n\nFile v1.0.7:SKILL.md\n\n---\nname: Job Application Manager — Gmail & Notion Sync\ndescription: Syncs job application emails from Gmail into your Notion or SQLite tracker — auto-detects offers, rejections, and interview invitations\nsummary: >\n  Use this skill to sync your job applications automatically from Gmail\n  into Notion or a local SQLite database. It scans your inbox for emails\n  from companies you have applied to, classifies each message as an offer,\n  rejection, or interview invitation, and syncs the application status into\n  your tracker without any manual effort. Supports Gmail label filters and\n  sender-pattern matching for accurate company detection. Works with both\n  Notion (cloud career tracker) and SQLite (local, no account required).\n  Deduplicates entries so running it multiple times is safe. Ideal for\n  keeping a job pipeline, career dashboard, or application spreadsheet\n  up to date directly from your email inbox.\nversion: 0.1.9\nauthor: Yuan Chen\nrepository: https://github.com/chenyuan99/swelist\nkeywords:\n  - gmail\n  - notion\n  - job-applications\n  - application-tracker\n  - career-management\n  - offer-detection\n  - interview-tracker\n  - rejection-tracker\n  - job-status\n  - sync\n  - automation\n  - sqlite\n  - email-parsing\n  - career-tracker\ntags:\n  - career\n  - productivity\n  - gmail\n  - notion\n  - job-search\ncategory: career\nmetadata:\n  openclaw:\n    emoji: \"📋\"\n    requires:\n      bins: []\n      env: []\n      mcp:\n        - name: claude_ai_Gmail\n          reason: Search and read job application emails\n          optional: false\n        - name: claude_ai_Notion\n          reason: Create and update application entries in Notion\n          optional: true\n    config:\n      - key: tracker_backend\n        description: Storage backend — notion or sqlite\n        default: notion\n      - key: sqlite_db_path\n        description: Path to SQLite database (sqlite backend only)\n        default: ~/.offerplus/applications.db\n---\n\n# Application Manager\n\n## When to Use This Skill\n\nTrigger when the user asks to:\n\n- Sync or refresh job application emails from Gmail (\"sync my applications\", \"check my job emails\", \"refresh my tracker\")\n- Update the status of one or more specific applications (\"mark Amazon as rejected\", \"I got an offer from Stripe\", \"update my application status\")\n- Add new applications discovered in Gmail (\"add this application to my tracker\", \"log this job email\")\n- Check what emails arrived from a specific company (\"did Google email me back?\", \"any updates from Meta?\")\n- Track offers, rejections, or interview invitations automatically (\"did I hear back from anyone?\", \"what's my interview pipeline?\")\n- Sync applications into Notion (\"add to my Notion tracker\", \"update my Notion job board\")\n- Sync applications into a local SQLite database (\"add to my sqlite tracker\", \"update my local tracker\")\n- Export or review their full application pipeline (\"show my application pipeline\", \"what's the status of all my applications?\")\n\nKeywords: `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`\n\n---\n\n## Setup (first-time use)\n\n**Always read `profile.md` first.** It contains the user's tracker backend choice,\nNotion database ID or SQLite path, Gmail label IDs, and career email.\n\nResolve these fields before continuing (collect from user + write back to `profile.md`):\n\n1. **Tracker backend** — `Integrations > Tracker Backend`\n   - If blank: ask the user — \"Do you use Notion or would you prefer a local SQLite file?\"\n   - Set to `notion` or `sqlite`\n\n2. **If notion** — resolve `Integrations > Notion > Career tracker database ID`\n   - If missing: ask user to open their Notion career tracker and copy the URL.\n     The ID is the UUID in `notion.so/<workspace>/<DATABASE_ID>?v=...`\n   - Derive: `NOTION_DB_ID`, `COLLECTION_URL = collection://<NOTION_DB_ID>`\n\n3. **If sqlite** — resolve `Integrations > Tracker Backend > SQLite DB path`\n   - Default: `~/.offerplus/applications.db`\n   - On first use, initialize the DB: `swelist tracker init --db <path>`\n     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')));\"`\n\n4. **Gmail label IDs** — `Integrations > Gmail Labels` table\n   - If missing: run `mcp__claude_ai_Gmail__list_labels`, show the list, ask the user\n     which labels are job-related, fill in the table in `profile.md`.\n\n5. **Career email** — `Personal Info > Career email`\n   - If missing: ask the user which email address receives job application emails.\n\n---\n\n## Config: Company → Gmail\n\nPopulate from the user's own labels (`mcp__claude_ai_Gmail__list_labels`):\n\n| Company key | Gmail sender pattern | Gmail label ID | Label name |\n|---|---|---|---|\n| `amazon` | `noreply@mail.amazon.jobs` | _(run list_labels)_ | e.g. amazon |\n| `linkedin` | `jobs-noreply@linkedin.com` | _(run list_labels)_ | e.g. linkedin |\n| `google` | `@google.com` | — | — |\n| `meta` | `@meta.com` | — | — |\n| `_any_` | — | — | — |\n\nAdd rows for any other companies the user has labeled. If no labels exist, rely on sender pattern alone.\n\n---\n\n## Config: Status Mapping\n\nMap email signals → status value (pick the **first** match):\n\n| Priority | Signal (case-insensitive) | Status |\n|---|---|---|\n| 1 | \"offer\" OR \"congratulations\" OR \"pleased to inform\" OR \"we'd like to extend\" | `Done` |\n| 2 | \"interview\" OR \"move forward\" OR \"next steps\" OR \"schedule\" OR \"hiring manager\" | `In progress` |\n| 3 | \"unable to move forward\" OR \"not selected\" OR \"no longer considering\" OR \"other candidates\" OR \"position has been filled\" | `Rejected` |\n| 4 | \"application received\" OR \"thank you for applying\" OR \"keep track\" OR \"under review\" | `In progress` |\n| 5 | (no email found, only a job listing) | `Not started` |\n\nIf multiple threads exist for the same role, use the **most recent** email's status.\n\n---\n\n## Config: Notion Database\n\n_(skip if tracker_backend is sqlite)_\n\n| Field | Value |\n|---|---|\n| Collection URL | `collection://<NOTION_DB_ID>` |\n| Parent ID (for creates) | `<NOTION_DB_ID>` |\n\nExpected schema:\n\n| Property | Type | Allowed values |\n|---|---|---|\n| `Name` | title | `\"Company — Role Title\"` |\n| `status` | select | `Not started`, `In progress`, `Rejected`, `Done` |\n| `Tags` | multi-select | Only use values already in the options list |\n\n---\n\n## Config: SQLite Schema\n\n_(skip if tracker_backend is notion)_\n\n```sql\nCREATE TABLE IF NOT EXISTS applications (\n  name        TEXT PRIMARY KEY,   -- \"Company — Role Title\"\n  status      TEXT NOT NULL,      -- Not started | In progress | Rejected | Done\n  job_id      TEXT,\n  applied_on  TEXT,               -- ISO date YYYY-MM-DD\n  notes       TEXT,\n  updated_at  TEXT DEFAULT (datetime('now'))\n);\n```\n\n---\n\n## Workflow\n\n```\nINPUT: company (optional), date_range (default: newer_than:6m)\n\nSTEP 0  Load profile\n  Read profile.md → extract tracker_backend, CAREER_EMAIL, label table\n  IF tracker_backend == \"notion\":  resolve NOTION_DB_ID, COLLECTION_URL\n  IF tracker_backend == \"sqlite\":  resolve DB_PATH; run init if DB does not exist\n  IF any required field blank: collect from user → write to profile.md → continue\n  IF company given AND not in label table:\n    run list_labels → confirm with user → append to profile.md label table\n\nSTEP 1  Search Gmail\n  IF company given:\n    query = build_query(company)          # see Query Builder below\n  ELSE:\n    query = 'subject:\"application\" OR subject:\"your application\" OR subject:\"interview\" newer_than:6m'\n  threads = gmail_search(query, max_results=20)\n  FOR each thread WHERE snippet is ambiguous:\n    fetch full thread via gmail_get_thread(thread_id)\n\nSTEP 2  Parse threads → applications[]\n  FOR each thread:\n    company_name = extract_company(thread)\n    role_title   = extract_role(thread)\n    status       = map_status(thread)       # use Status Mapping table above\n    date         = most_recent_message_date(thread)\n    job_id       = extract_job_id(thread)   # if present in email\n    page_name    = f\"{company_name} — {role_title}\"\n    APPEND { page_name, status, date, job_id, thread_id }\n\nSTEP 3  Deduplicate\n  IF tracker_backend == \"notion\":\n    FOR each application:\n      existing = notion_search(query=application.page_name,\n                               data_source_url=COLLECTION_URL)\n      IF found: set existing_status, action = \"update\" or \"skip\"\n      ELSE:     action = \"create\"\n\n  IF tracker_backend == \"sqlite\":\n    FOR each application:\n      result = swelist tracker get \"<page_name>\" --db DB_PATH\n      IF result is not null: set existing_status, action = \"update\" or \"skip\"\n      ELSE:                   action = \"create\"\n\nSTEP 4  Apply changes\n  IF tracker_backend == \"notion\":\n    creates → notion_create_pages(batch)\n    updates → notion_update_page per entry\n\n  IF tracker_backend == \"sqlite\":\n    FOR each create:\n      swelist tracker add \"<name>\" --status <status> --job-id <id> --applied-on <date> --db DB_PATH\n    FOR each update:\n      swelist tracker update \"<name>\" --status <status> --db DB_PATH\n\nSTEP 5  Report\n  print summary (see Report Format below)\n```\n\n---\n\n## Query Builder\n\n```\nFUNCTION build_query(company_key, date_range=\"newer_than:6m\"):\n  cfg = COMPANY_CONFIG[company_key]\n  parts = []\n  IF cfg.sender:   parts.append(f\"from:{cfg.sender}\")\n  IF cfg.label_id: parts.append(f\"label:{cfg.label_id}\")\n  subject_terms = 'subject:application OR subject:interview OR subject:offer OR subject:position'\n  RETURN f'({\" OR \".join(parts)}) ({subject_terms}) {date_range}'\n```\n\nExamples:\n- Amazon with label → `(from:noreply@mail.amazon.jobs OR label:<label_id>) (subject:application OR ...) newer_than:6m`\n- Unknown company → fall back to `from:@<company>.com` or company name in subject\n\n---\n\n## Storage API Calls\n\n### Notion — Create (batch)\n```json\n{\n  \"data_source_id\": \"<NOTION_DB_ID>\",\n  \"pages\": [\n    {\n      \"properties\": { \"Name\": \"Amazon — SDE, AWS\", \"status\": \"In progress\" },\n      \"content\": \"Applied: <date>\"\n    }\n  ]\n}\n```\n\n### Notion — Update (single)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": { \"status\": \"Rejected\" },\n  \"content_updates\": []\n}\n```\n\n### Notion — Search (dedup)\n```json\n{\n  \"query\": \"Amazon — Software Development Engineer\",\n  \"data_source_url\": \"collection://<NOTION_DB_ID>\",\n  \"filters\": {}\n}\n```\n\n### SQLite — Create\n```bash\nswelist tracker add \"Amazon — SDE, AWS\" \\\n  --status \"In progress\" \\\n  --job-id 10414382 \\\n  --applied-on 2026-05-17 \\\n  --db <DB_PATH>\n```\n\n### SQLite — Update\n```bash\nswelist tracker update \"Amazon — SDE, AWS\" \\\n  --status \"Rejected\" \\\n  --db <DB_PATH>\n```\n\n### SQLite — Search (dedup)\n```bash\nswelist tracker get \"Amazon — SDE, AWS\" --db <DB_PATH>\n# Returns JSON object if found, null if not found. Exit code 1 when not found.\n```\n\n### SQLite — List all\n```bash\nswelist tracker list --db <DB_PATH>\nswelist tracker export --format json --db <DB_PATH>\n```\n\n---\n\n## Naming Convention\n\n```\n\"{Company} — {Role Title}\"\n```\n\n| Good | Bad |\n|---|---|\n| `Amazon — Software Development Engineer, AWS` | `SDE at Amazon` |\n| `Google — Software Engineer II, Early Career` | `Google SWE` |\n| `Goldman Sachs — Software Engineer - Associate` | `Goldman` |\n\nRules:\n- Em dash (`—`), not hyphen\n- Company name in title case\n- Role title verbatim from the job posting or email subject when possible\n\n---\n\n## Report Format\n\n```\nSynced {N} application(s) on {date}  [{backend}: notion|sqlite]\n\nCreated:\n  ✦ {Company} — {Role} → {status}\n\nUpdated:\n  ↑ {Company} — {Role} → {new_status}  (was: {old_status})\n\nSkipped (already up to date):\n  · {Company} — {Role} → {status}\n\nErrors:\n  ✕ {description of issue}\n```\n\n---\n\n## Edge Cases\n\n| Situation | Handling |\n|---|---|\n| Multiple threads for same role | Use most recent thread's status |\n| Role title not in email | Use job ID or \"Role\" as placeholder; note it in content/notes |\n| Company exists under a different name variant | Search by company name alone first, prompt user to confirm merge |\n| Email snippet enough to determine status | Skip full thread fetch to reduce API calls |\n| `Tags` value not in Notion options list | Omit `Tags`; do not create new options |\n| Page name collision (same company, same title, different role) | Add disambiguator: `Amazon — SDE, AWS (Job ID 12345)` |\n| Notion DB ID or schema unknown | Fetch database URL with `notion-fetch` to inspect schema first |\n| SQLite DB does not exist | Run `swelist tracker init` or the CREATE TABLE statement before Step 3 |\n| SQLite DB path missing from profile.md | Use default `~/.offerplus/applications.db`; confirm with user |\n\nFile v1.0.7:_meta.json\n\n{\n  \"ownerId\": \"kn78p1g6xzcqm9fyegreqrft0n80bazp\",\n  \"slug\": \"job-application-manager\",\n  \"version\": \"1.0.7\",\n  \"publishedAt\": 1779393459307\n}\n\nArchive v1.0.6: 2 files, 5446 bytes\n\nFiles: SKILL.md (12942b), _meta.json (142b)\n\nFile v1.0.6:SKILL.md\n\n---\nname: Job Application Manager — Gmail & Notion Sync\ndescription: Reads your Gmail to find job application emails and updates your Notion or SQLite tracker with offer, rejection, and interview statuses automatically\nsummary: >\n  Use this skill to keep your job application tracker up to date without\n  any manual effort. It searches your Gmail for emails from companies you\n  have applied to, determines whether each email signals an offer,\n  rejection, or interview invitation, and syncs the status into your\n  Notion database or local SQLite file. Handles any company's application\n  emails using sender patterns and subject-line signals. Supports both\n  Notion (cloud) and SQLite (local, no account required) as storage\n  backends. Deduplicates entries so running it multiple times is safe.\n  Great for keeping a career tracker, job pipeline, or application\n  spreadsheet automatically updated from your inbox.\nversion: 0.1.9\nauthor: Yuan Chen\nrepository: https://github.com/chenyuan99/swelist\nkeywords:\n  - gmail\n  - notion\n  - job-applications\n  - application-tracker\n  - career-management\n  - offer-detection\n  - interview-tracker\n  - rejection-tracker\n  - job-status\n  - sync\n  - automation\n  - sqlite\n  - email-parsing\n  - career-tracker\ntags:\n  - career\n  - productivity\n  - gmail\n  - notion\n  - job-search\ncategory: career\nmetadata:\n  openclaw:\n    emoji: \"📋\"\n    requires:\n      bins: []\n      env: []\n      mcp:\n        - name: claude_ai_Gmail\n          reason: Search and read job application emails\n          optional: false\n        - name: claude_ai_Notion\n          reason: Create and update application entries in Notion\n          optional: true\n    config:\n      - key: tracker_backend\n        description: Storage backend — notion or sqlite\n        default: notion\n      - key: sqlite_db_path\n        description: Path to SQLite database (sqlite backend only)\n        default: ~/.offerplus/applications.db\n---\n\n# Application Manager\n\n## When to Use This Skill\n\nTrigger when the user asks to:\n\n- Sync or refresh job application emails from Gmail (\"sync my applications\", \"check my job emails\", \"refresh my tracker\")\n- Update the status of one or more specific applications (\"mark Amazon as rejected\", \"I got an offer from Stripe\", \"update my application status\")\n- Add new applications discovered in Gmail (\"add this application to my tracker\", \"log this job email\")\n- Check what emails arrived from a specific company (\"did Google email me back?\", \"any updates from Meta?\")\n- Track offers, rejections, or interview invitations automatically (\"did I hear back from anyone?\", \"what's my interview pipeline?\")\n- Sync applications into Notion (\"add to my Notion tracker\", \"update my Notion job board\")\n- Sync applications into a local SQLite database (\"add to my sqlite tracker\", \"update my local tracker\")\n- Export or review their full application pipeline (\"show my application pipeline\", \"what's the status of all my applications?\")\n\nKeywords: `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`\n\n---\n\n## Setup (first-time use)\n\n**Always read `profile.md` first.** It contains the user's tracker backend choice,\nNotion database ID or SQLite path, Gmail label IDs, and career email.\n\nResolve these fields before continuing (collect from user + write back to `profile.md`):\n\n1. **Tracker backend** — `Integrations > Tracker Backend`\n   - If blank: ask the user — \"Do you use Notion or would you prefer a local SQLite file?\"\n   - Set to `notion` or `sqlite`\n\n2. **If notion** — resolve `Integrations > Notion > Career tracker database ID`\n   - If missing: ask user to open their Notion career tracker and copy the URL.\n     The ID is the UUID in `notion.so/<workspace>/<DATABASE_ID>?v=...`\n   - Derive: `NOTION_DB_ID`, `COLLECTION_URL = collection://<NOTION_DB_ID>`\n\n3. **If sqlite** — resolve `Integrations > Tracker Backend > SQLite DB path`\n   - Default: `~/.offerplus/applications.db`\n   - On first use, initialize the DB: `swelist tracker init --db <path>`\n     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')));\"`\n\n4. **Gmail label IDs** — `Integrations > Gmail Labels` table\n   - If missing: run `mcp__claude_ai_Gmail__list_labels`, show the list, ask the user\n     which labels are job-related, fill in the table in `profile.md`.\n\n5. **Career email** — `Personal Info > Career email`\n   - If missing: ask the user which email address receives job application emails.\n\n---\n\n## Config: Company → Gmail\n\nPopulate from the user's own labels (`mcp__claude_ai_Gmail__list_labels`):\n\n| Company key | Gmail sender pattern | Gmail label ID | Label name |\n|---|---|---|---|\n| `amazon` | `noreply@mail.amazon.jobs` | _(run list_labels)_ | e.g. amazon |\n| `linkedin` | `jobs-noreply@linkedin.com` | _(run list_labels)_ | e.g. linkedin |\n| `google` | `@google.com` | — | — |\n| `meta` | `@meta.com` | — | — |\n| `_any_` | — | — | — |\n\nAdd rows for any other companies the user has labeled. If no labels exist, rely on sender pattern alone.\n\n---\n\n## Config: Status Mapping\n\nMap email signals → status value (pick the **first** match):\n\n| Priority | Signal (case-insensitive) | Status |\n|---|---|---|\n| 1 | \"offer\" OR \"congratulations\" OR \"pleased to inform\" OR \"we'd like to extend\" | `Done` |\n| 2 | \"interview\" OR \"move forward\" OR \"next steps\" OR \"schedule\" OR \"hiring manager\" | `In progress` |\n| 3 | \"unable to move forward\" OR \"not selected\" OR \"no longer considering\" OR \"other candidates\" OR \"position has been filled\" | `Rejected` |\n| 4 | \"application received\" OR \"thank you for applying\" OR \"keep track\" OR \"under review\" | `In progress` |\n| 5 | (no email found, only a job listing) | `Not started` |\n\nIf multiple threads exist for the same role, use the **most recent** email's status.\n\n---\n\n## Config: Notion Database\n\n_(skip if tracker_backend is sqlite)_\n\n| Field | Value |\n|---|---|\n| Collection URL | `collection://<NOTION_DB_ID>` |\n| Parent ID (for creates) | `<NOTION_DB_ID>` |\n\nExpected schema:\n\n| Property | Type | Allowed values |\n|---|---|---|\n| `Name` | title | `\"Company — Role Title\"` |\n| `status` | select | `Not started`, `In progress`, `Rejected`, `Done` |\n| `Tags` | multi-select | Only use values already in the options list |\n\n---\n\n## Config: SQLite Schema\n\n_(skip if tracker_backend is notion)_\n\n```sql\nCREATE TABLE IF NOT EXISTS applications (\n  name        TEXT PRIMARY KEY,   -- \"Company — Role Title\"\n  status      TEXT NOT NULL,      -- Not started | In progress | Rejected | Done\n  job_id      TEXT,\n  applied_on  TEXT,               -- ISO date YYYY-MM-DD\n  notes       TEXT,\n  updated_at  TEXT DEFAULT (datetime('now'))\n);\n```\n\n---\n\n## Workflow\n\n```\nINPUT: company (optional), date_range (default: newer_than:6m)\n\nSTEP 0  Load profile\n  Read profile.md → extract tracker_backend, CAREER_EMAIL, label table\n  IF tracker_backend == \"notion\":  resolve NOTION_DB_ID, COLLECTION_URL\n  IF tracker_backend == \"sqlite\":  resolve DB_PATH; run init if DB does not exist\n  IF any required field blank: collect from user → write to profile.md → continue\n  IF company given AND not in label table:\n    run list_labels → confirm with user → append to profile.md label table\n\nSTEP 1  Search Gmail\n  IF company given:\n    query = build_query(company)          # see Query Builder below\n  ELSE:\n    query = 'subject:\"application\" OR subject:\"your application\" OR subject:\"interview\" newer_than:6m'\n  threads = gmail_search(query, max_results=20)\n  FOR each thread WHERE snippet is ambiguous:\n    fetch full thread via gmail_get_thread(thread_id)\n\nSTEP 2  Parse threads → applications[]\n  FOR each thread:\n    company_name = extract_company(thread)\n    role_title   = extract_role(thread)\n    status       = map_status(thread)       # use Status Mapping table above\n    date         = most_recent_message_date(thread)\n    job_id       = extract_job_id(thread)   # if present in email\n    page_name    = f\"{company_name} — {role_title}\"\n    APPEND { page_name, status, date, job_id, thread_id }\n\nSTEP 3  Deduplicate\n  IF tracker_backend == \"notion\":\n    FOR each application:\n      existing = notion_search(query=application.page_name,\n                               data_source_url=COLLECTION_URL)\n      IF found: set existing_status, action = \"update\" or \"skip\"\n      ELSE:     action = \"create\"\n\n  IF tracker_backend == \"sqlite\":\n    FOR each application:\n      result = swelist tracker get \"<page_name>\" --db DB_PATH\n      IF result is not null: set existing_status, action = \"update\" or \"skip\"\n      ELSE:                   action = \"create\"\n\nSTEP 4  Apply changes\n  IF tracker_backend == \"notion\":\n    creates → notion_create_pages(batch)\n    updates → notion_update_page per entry\n\n  IF tracker_backend == \"sqlite\":\n    FOR each create:\n      swelist tracker add \"<name>\" --status <status> --job-id <id> --applied-on <date> --db DB_PATH\n    FOR each update:\n      swelist tracker update \"<name>\" --status <status> --db DB_PATH\n\nSTEP 5  Report\n  print summary (see Report Format below)\n```\n\n---\n\n## Query Builder\n\n```\nFUNCTION build_query(company_key, date_range=\"newer_than:6m\"):\n  cfg = COMPANY_CONFIG[company_key]\n  parts = []\n  IF cfg.sender:   parts.append(f\"from:{cfg.sender}\")\n  IF cfg.label_id: parts.append(f\"label:{cfg.label_id}\")\n  subject_terms = 'subject:application OR subject:interview OR subject:offer OR subject:position'\n  RETURN f'({\" OR \".join(parts)}) ({subject_terms}) {date_range}'\n```\n\nExamples:\n- Amazon with label → `(from:noreply@mail.amazon.jobs OR label:<label_id>) (subject:application OR ...) newer_than:6m`\n- Unknown company → fall back to `from:@<company>.com` or company name in subject\n\n---\n\n## Storage API Calls\n\n### Notion — Create (batch)\n```json\n{\n  \"data_source_id\": \"<NOTION_DB_ID>\",\n  \"pages\": [\n    {\n      \"properties\": { \"Name\": \"Amazon — SDE, AWS\", \"status\": \"In progress\" },\n      \"content\": \"Applied: <date>\"\n    }\n  ]\n}\n```\n\n### Notion — Update (single)\n```json\n{\n  \"page_id\": \"<page-id>\",\n  \"command\": \"update_properties\",\n  \"properties\": { \"status\": \"Rejected\" },\n  \"content_updates\": []\n}\n```\n\n### Notion — Search (dedup)\n```json\n{\n  \"query\": \"Amazon — Software Development Engineer\",\n  \"data_source_url\": \"collection://<NOTION_DB_ID>\",\n  \"filters\": {}\n}\n```\n\n### SQLite — Create\n```bash\nswelist tracker add \"Amazon — SDE, AWS\" \\\n  --status \"In progress\" \\\n  --job-id 10414382 \\\n  --applied-on 2026-05-17 \\\n  --db <DB_PATH>\n```\n\n### SQLite — Update\n```bash\nswelist tracker update \"Amazon — SDE, AWS\" \\\n  --status \"Rejected\" \\\n  --db <DB_PATH>\n```\n\n### SQLite — Search (dedup)\n```bash\nswelist tracker get \"Amazon — SDE, AWS\" --db <DB_PATH>\n# Returns JSON object if found, null if not found. Exit code 1 when not found.\n```\n\n### SQLite — List all\n```bash\nswelist tracker list --db <DB_PATH>\nswelist tracker export --format json --db <DB_PATH>\n```\n\n---\n\n## Naming Convention\n\n```\n\"{Company} — {Role Title}\"\n```\n\n| Good | Bad |\n|---|---|\n| `Amazon — Software Development Engineer, AWS` | `SDE at Amazon` |\n| `Google — Software Engineer II, Early Career` | `Google SWE` |\n| `Goldman Sachs — Software Engineer - Associate` | `Goldman` |\n\nRules:\n- Em dash (`—`), not hyphen\n- Company name in title case\n- Role title verbatim from the job posting or email subject when possible\n\n---\n\n## Report Format\n\n```\nSynced {N} application(s) on {date}  [{backend}: notion|sqlite]\n\nCreated:\n  ✦ {Company} — {Role} → {status}\n\nUpdated:\n  ↑ {Company} — {Role} → {new_status}  (was: {old_status})\n\nSkipped (already up to date):\n  · {Company} — {Role} → {status}\n\nErrors:\n  ✕ {description of issue}\n```\n\n---\n\n## Edge Cases\n\n| Situation | Handling |\n|---|---|\n| Multiple threads for same role | Use most recent thread's status |\n| Role title not in email | Use job ID or \"Role\" as placeholder; note it in content/notes |\n| Company exists under a different name variant | Search by company name alone first, prompt user to confirm merge |\n| Email snippet enough to determine status | Skip full thread fetch to reduce API calls |\n| `Tags` value not in Notion options list | Omit `Tags`; do not create new options |\n| Page name collision (same company, same title, different role) | Add disambiguator: `Amazon — SDE, AWS (Job ID 12345)` |\n| Notion DB ID or schema unknown | Fetch database URL with `notion-fetch` to inspect schema first |\n| SQLite DB does not exist | Run `swelist tracker init` or the CREATE TABLE statement before Step 3 |\n| SQLite DB path missing from profile.md | Use default `~/.offerplus/applications.db`; confirm with user |\n\nFile v1.0.6:_meta.json\n\n{\n  \"ownerId\": \"kn78p1g6xzcqm9fyegreqrft0n80bazp\",\n  \"slug\": \"job-application-manager\",\n  \"version\": \"1.0.6\",\n  \"publishedAt\": 1779393088838\n}\n\nArchive v1.0.5: 2 files, 5432 bytes\n\nFiles: SKILL.md (12914b), _meta.json (142b)\n\nFile v1.0.5:SKILL.md\n\n---\nname: Application Manager\ndescription: Reads your Gmail to find job application emails and updates your Notion or SQLite tracker with offer, rejection, and interview statuses automatically\nsummary: >\n  Use this skill to keep your job application tracker up to date without\n  any manual effort. It searches your Gmail for emails from companies you\n  have applied to, determines whether each email signals an offer,\n  rejection, or interview invitation, and syncs the status into your\n  Notion database or local SQLite file. Handles any company's application\n  emails using sender patterns and subject-line signals. Supports both\n  Notion (cloud) and SQLite (local, no account required) as storage\n  backends. Deduplicates entries so running it multiple times is safe.\n  Great for keeping a career tracker, job pipeline, or application\n  spreadsheet automatically updated from your inbox.\nversion: 0.1.9\nauthor: Yuan Chen\nrepository: https://github.com/chenyuan99/swelist\nkeywords:\n  - gmail\n  - notion\n  - job-applications\n  - application-tracker\n  - career-management\n  - offer-detection\n  - interview-tracker\n  - rejection-tracker\n  - job-status\n  - sync\n  - automation\n  - sqlite\n  - email-parsing\n  - career-tracker\ntags:\n  - career\n  - productivity\n  - gmail\n  - notion\n  - job-search\ncategory: career\nmetadata:\n  openclaw:\n    emoji: \"📋\"\n    requires:\n      bins: []\n      env: []\n      mcp:\n        - name: claude_ai_Gmail\n          reason: Search and read job application emails\n          optional: false\n        - name: claude_ai_Notion\n          reason: Create and update application entries in Notion\n          optional: true\n    config:\n      - key: tracker_backend\n        description: Storage backend — notion or sqlite\n        default: notion\n      - key: sqlite_db_path\n        description: Path to SQLite database (sqlite backend only)\n        default: ~/.offerplus/applications.db\n---\n\n# Application Manager\n\n## When to Use This Skill\n\nTrigger when the user asks to:\n\n- Sync or refresh job application emails from Gmail (\"sync my applications\", \"check my job emails\", \"refresh my tracker\")\n- Update the status of one or more specific applications (\"mark Amazon as rejected\", \"I got an offer from Stripe\", \"update my application status\")\n- Add new applications discovered in Gmail (\"add this application to my tracker\", \"log this job email\")\n- Check what emails arrived from a specific company (\"did Google email me back?\", \"any updates from Meta?\")\n- Track offers, rejections, or interview invitations automatically (\"did I hear back from anyone?\", \"what's my interview pipeline?\")\n- Sync applications into Notion (\"add to my Notion tracker\", \"update my Notion job board\")\n- Sync applications into a local SQLite database (\"add to my sqlite tracker\", \"update my local tracker\")\n- Export or review their full application pipeline (\"show my application pipeline\", \"what's the status of all my applications?\")\n\nKeywords: `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`\n\n---\n\n## Setup (first-time use)\n\n**Always read `profile.md` first.** It contains the user's tracker backend choice,\nNotion database ID or SQLite path, Gmail label IDs, and career email.\n\nResolve these fields before continuing (collect from user + write back to `profile.md`):\n\n1. **Tracker backend** — `Integrations > Tracker Backend`\n   - If blank: ask the user — \"Do you use Notion or would you prefer a local SQLite file?\"\n   - Set to `notion` or `sqlite`\n\n2. **If notion** — resolve `Integrations > Notion > Career tracker database ID`\n   - If missing: ask user to open their Notion career tracker and copy the URL.\n     The ID is the UUID in `notion.so/<workspace>/<DATABASE_ID>?v=...`\n   - Derive: `NOTION_DB_ID`, `COLLECTION_URL = collection://<NOTION_DB_ID>`\n\n3. **If sqlite** — resolve `Integrations > Tracker Backend > SQLite DB path`\n   - Default: `~/.offerplus/applications.db`\n   - On first use, initialize the DB: `swelist tracker init --db <path>`\n     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')));\"`\n\n4. **Gmail label IDs** — `Integrations > Gmail Labels` table\n   - If missing: run `mcp__claude_ai_Gmail__list_labels`, show the list, ask the user\n     which labels are job-related, fill in the table in `profile.md`.\n\n5. **Career email** — `Personal Info > Career email`\n   - If missing: ask the user which email address receives job application emails.\n\n---\n\n## Config: Company → Gmail\n\nPopulate from the use\n\nArchive v1.0.4: 2 files, 5147 bytes\n\nFiles: SKILL.md (12206b), _meta.json (142b)\n\nArchive v1.0.3: 2 files, 5140 bytes\n\nFiles: SKILL.md (12189b), _meta.json (142b)\n\nArchive v1.0.2: 2 files, 4676 bytes\n\nFiles: SKILL.md (10816b), _meta.json (142b)","readmeExcerpt":"Skill: Job Application Manager Owner: chenyuan99 Summary: Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations Tags: latest:1.0.11 Version history: v1.0.11 | 2026-05-30T14:34:42.130Z | user Released via CI v1.0.10 | 2026-05-30T14:20:35.948Z | user Released via CI v1.0.9 | 2026-05-30T14:13:39.448Z | user Released via CI v1","codeSnippets":[],"executableExamples":[{"language":"text","snippet":"FUNCTION fuzzy_match(extracted_name, existing_names[]):\n  candidates = []\n  FOR each existing_name in existing_names:\n    dist = levenshtein(extracted_name.lower(), existing_name.lower())\n    IF dist <= 2 OR one_is_prefix_of_other(extracted_name, existing_name):\n      candidates.append((existing_name, dist))\n  RETURN sorted(candidates, by=dist)[:3]   # top 3 closest"},{"language":"sql","snippet":"CREATE TABLE IF NOT EXISTS applications (\n  name           TEXT PRIMARY KEY,   -- \"Company — Role Title\"\n  status         TEXT NOT NULL,      -- pipeline status (see Status Mapping)\n  job_id         TEXT,\n  company        TEXT,\n  applied_on     TEXT,               -- ISO date YYYY-MM-DD\n  last_touch     TEXT,               -- ISO date of most recent email\n  interview_date TEXT,               -- ISO date of next scheduled interview\n  next_action    TEXT,               -- Waiting | Prep | Follow up | Send availability\n  link           TEXT,               -- job posting or portal URL (ATS-extracted)\n  tags           TEXT,               -- comma-separated inferred tags e.g. \"Referral,Remote\"\n  notes          TEXT,\n  updated_at     TEXT DEFAULT (datetime('now'))\n);"},{"language":"sql","snippet":"ALTER TABLE applications ADD COLUMN company        TEXT;\nALTER TABLE applications ADD COLUMN last_touch     TEXT;\nALTER TABLE applications ADD COLUMN interview_date TEXT;\nALTER TABLE applications ADD COLUMN next_action    TEXT;\nALTER TABLE applications ADD COLUMN link           TEXT;\nALTER TABLE applications ADD COLUMN tags           TEXT;"},{"language":"text","snippet":"INPUT: company (optional), date_range (default: newer_than:6m)\n\nSTEP 0  Load profile\n  Read profile.md → extract tracker_backend, CAREER_EMAIL, label table\n  IF tracker_backend == \"notion\":  resolve NOTION_DB_ID, COLLECTION_URL\n  IF tracker_backend == \"sqlite\":\n    resolve DB_PATH; run init if DB does not exist\n    check PRAGMA table_info(applications) for last_touch column\n    IF missing: run migration SQL (see Config: SQLite Schema)\n  IF any required field blank: collect from user → write to profile.md → continue\n  IF company given AND not in label table:\n    run list_labels → confirm with user → append to profile.md label table\n\nSTEP 1  Search Gmail\n  IF company given:\n    query = build_query(company)          # see Query Builder below\n  ELSE:\n    query = 'subject:\"application\" OR subject:\"your application\" OR subject:\"interview\" newer_than:6m'\n  threads = gmail_search(query, max_results=20)\n  FOR each thread WHERE snippet is ambiguous:\n    fetch full thread via gmail_get_thread(thread_id)\n\nSTEP 2  Parse threads → applications[]\n  FOR each thread:\n    company_name   = extract_company(thread)\n    role_title     = extract_role(thread)\n    status         = map_status(thread)         # use Status Mapping table above\n    next_action    = map_next_action(status)    # use Next Action Mapping table above\n    date           = most_recent_message_date(thread)\n    applied_date   = earliest_message_date(thread)\n    job_id         = extract_job_id(thread)     # if present in email\n    interview_date = extract_interview_date(thread)\n                     # scan body for patterns: \"June 9\", \"Monday June 9 at 2pm PT\",\n                     # \"scheduled for <date>\", \"your interview is on <date>\"\n                     # normalise to ISO date YYYY-MM-DD; set null if not found\n    link           = extract_link(thread)\n                     # match ATS URL patterns (see Config: Link Extraction)\n                     # strip tracking params; null if not found\n    suggested_tags = infer_tag"},{"language":"text","snippet":"INPUT: include_closed (default: false) — whether to enrich Rejected/Withdrawn pages\n\nSTEP R0  Load profile (same as main STEP 0)\n\nSTEP R1  Fetch all pages\n  all_pages = notion_fetch(COLLECTION_URL)\n  IF include_closed == false:\n    pages = [p for p in all_pages if p.status NOT IN (\"Rejected\", \"Withdrawn\")]\n  ELSE:\n    pages = all_pages\n  print: \"Found {len(pages)} pages to enrich.\"\n\nSTEP R2  Match pages to Gmail threads\n  FOR each page in pages:\n    company_name = extract_company_from_name(page.name)\n    role_title   = extract_role_from_name(page.name)\n    query        = build_query(company_name) with date_range=\"newer_than:12m\"\n    threads      = gmail_search(query, max_results=5)\n    IF no threads found:\n      skip page; note in report as \"no emails found\"\n      CONTINUE\n    best_thread  = most_recent_thread(threads)\n    full_thread  = gmail_get_thread(best_thread.id)\n    STORE { page, full_thread }\n\nSTEP R3  Enrich each page (same as main STEP 5)\n  FOR each { page, full_thread }:\n    extract timeline, key_contacts, conversation notes, prep_suggestions\n    notion_update_page(page.id, content=structured_content)\n    IF page.interview_date is null:\n      interview_date = extract_interview_date(full_thread)\n      IF found: notion_update_page(page.id, properties={\"Interview date\": interview_date})\n    IF page.link is null:\n      link = extract_link(full_thread)\n      IF found: notion_update_page(page.id, properties={\"Link\": link})\n\nSTEP R4  Report\n  print:\n    Re-enriched: {N} pages\n      ✦ {Company} — {Role}  (+ interview date | + link | full notes)\n    Skipped (no emails): {M} pages\n      · {Company} — {Role}"},{"language":"text","snippet":"FUNCTION build_query(company_key, date_range=\"newer_than:6m\"):\n  cfg = COMPANY_CONFIG[company_key]\n  parts = []\n  IF cfg.sender:   parts.append(f\"from:{cfg.sender}\")\n  IF cfg.label_id: parts.append(f\"label:{cfg.label_id}\")\n  subject_terms = 'subject:application OR subject:interview OR subject:offer OR subject:position OR subject:assessment OR subject:next steps'\n  RETURN f'({\" OR \".join(parts)}) ({subject_terms}) {date_range}'"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: Job Application Manager — Gmail & Notion Sync\ndescription: Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations\nsummary: >\n  Use this skill to sync your job applications automatically from Gmail\n  into Notion or a local SQLite database. It scans your inbox for emails\n  from companies you have applied to, classifies each message as an offer,\n  rejection, or interview invitation, and syncs the application status into\n  your tracker without any manual effort. Supports Gmail label filters and\n  sender-pattern matching for accurate company detection. Works with both\n  Notion (cloud career tracker) and SQLite (local, no account required).\n  Deduplicates entries so running it multiple times is safe. When syncing\n  a specific company, it also enriches the Notion page with a timeline,\n  conversation notes, key contacts, and preparation suggestions. Detects\n  stale applications (no email activity in 30+ days) and flags them for\n  follow-up. Extracts scheduled interview dates and optionally creates\n  Google Calendar events. Auto-extracts ATS job links from email footers\n  and infers tags (Referral, Remote, Urgent) from email content. Prints\n  a pipeline funnel summary after every full sync. Uses a company alias\n  table and edit-distance matching to prevent duplicate rows when company\n  names vary (e.g. \"Meta\" vs \"Meta Platforms\"). Supports a bulk\n  re-enrichment mode that retrospectively adds timeline, contacts, and\n  prep notes to all existing Notion pages.\nversion: 0.5.0\nauthor: Yuan Chen\nrepository: https://github.com/chenyuan99/swelist\nkeywords:\n  - gmail\n  - notion\n  - job-applications\n  - application-tracker\n  - career-management\n  - offer-detection\n  - interview-tracker\n  - rejection-tracker\n  - job-status\n  - sync\n  - automation\n  - sqlite\n  - email-parsing\n  - career-tracker\ntags:\n  - career\n  - productivity\n  - gmail\n  - notion\n  - job-search\ncategory: career\nmetadata:\n  openclaw:\n    emoji: \"📋\"\n    requires:\n      bins: []\n      env: []\n      mcp:\n        - name: claude_ai_Gmail\n          reason: Search and read job application emails\n          optional: false\n        - name: claude_ai_Notion\n          reason: Create and update application entries in Notion\n          optional: true\n        - name: claude_ai_Google_Calendar\n          reason: Create calendar events for scheduled interviews\n          optional: true\n    config:\n      - key: tracker_backend\n        description: Storage backend — notion or sqlite\n        default: notion\n      - key: sqlite_db_path\n        description: Path to SQLite database (sqlite backend only)\n        default: ~/.offerplus/applications.db\n---\n\n# Application Manager\n\n## When to Use This Skill\n\nTrigger when the user asks to:\n\n- Sync or refresh job application emails from Gmail (\"sync my applications\", \"check my job emails\", \"refresh my tracker\")\n- Update the status of one or more specific applications (\"mark Ama"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn78p1g6xzcqm9fyegreqrft0n80bazp\",\n  \"slug\": \"job-application-manager\",\n  \"version\": \"1.0.11\",\n  \"publishedAt\": 1780151682130\n}"},{"path":"skill-card.md","content":"## Description:\n\nSyncs job application emails from Gmail and updates statuses in a Notion or SQLite tracker, detecting offers, rejections, interview invitations, and follow-up needs.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chenyuan99](https://clawhub.ai/user/chenyuan99)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal users use this skill to keep a job application tracker current from Gmail activity, with optional writes to Notion, a local SQLite database, and Google Calendar interview events.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill may read sensitive job-related Gmail threads and write extracted details to a tracker.\n\nMitigation: Use it only with explicit consent for Gmail access and tracker writes, and review the data it will sync before installation.\n\nRisk: SQLite persistence can be unsafe when the database path is user-supplied, profile-loaded, or passed through shell interpolation.\n\nMitigation: Prefer the Notion backend or a fixed trusted SQLite path; validate and pass SQLite paths safely without shell interpolation.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/chenyuan99/skills/job-application-manager)\n- [Publisher profile](https://clawhub.ai/user/chenyuan99)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown reports, structured tracker content, JSON-like API payloads, and shell command snippets]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May read job-related Gmail threads and write extracted application details to Notion, SQLite, or Google Calendar when configured.]\n\n## Skill Version(s):\n\n1.0.11 (source: ClawHub release metadata; artifact frontmatter version 0.5.0)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations Skill: Job Application Manager Owner: chenyuan99 Summary: Syncs job application emails from Gmail and updates statuses in your Notion or SQLite tracker — detects offers, rejections, and interview invitations Tags: latest:1.0.11 Version history: v1.0.11 | 2026-05-30T14:34:42.130Z | user Released via CI v1.0.10 | 2026-05-30T14:20:35.948Z | user Released via CI v1.0.9 | 2026-05-30T14:13:39.448Z | user Released via CI v1","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1186,"uniquenessScore":50,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T02:49:59.557Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T02:49:59.557Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T05:32:08.659Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}