# Job Application Tracker

> Automatically syncs Gmail and Google Calendar with a Notion database to keep track of the status of job applications the user has submitted. Use this skill whenever the user asks to "sync my job applications," "update my job tracker," "check where my interviews stand," or something similar — even if they don't explicitly mention Notion, Gmail, or Calendar, and even if they don't have a Notion dat…

- **Type:** Skill
- **Install:** `agentstack add skill-daliaabbruciati-job-application-tracker-skill-job-application-tracker-skill`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [daliaabbruciati](https://agentstack.voostack.com/s/daliaabbruciati)
- **Installs:** 0
- **Category:** [Databases](https://agentstack.voostack.com/c/databases)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [daliaabbruciati](https://github.com/daliaabbruciati)
- **Source:** https://github.com/daliaabbruciati/job-application-tracker-SKILL

## Install

```sh
agentstack add skill-daliaabbruciati-job-application-tracker-skill-job-application-tracker-skill
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Job Application Tracker

A skill for keeping a Notion database of job applications up to date, by cross-referencing recent Gmail messages and Google Calendar events. Designed to be usable by anyone, including someone starting from scratch (no Notion database yet, connectors not yet linked).

## Step 0 — Check prerequisites (always do this first)

### 0.1 Connectors
Check whether you have access to **Notion**, **Gmail**, and **Google Calendar** tools. If one or more are missing:
- Use `search_mcp_registry` with relevant keywords (e.g. `["notion"]`, `["gmail", "email"]`, `["calendar"]`).
- Use `suggest_connectors` to let the user connect them.
- Don't move to the next step until the user has at least connected Notion. Gmail and Calendar are both needed for a full scan, but if the user wants to start with just one of the two, that's fine — flag it and proceed with what's available.

### 0.2 Notion database
Ask the user (or search yourself) whether a dedicated job-applications page/database already exists. Try `notion-search` with keywords like "applications", "job tracker", "interviews", "candidature".

**If no database exists:**
Offer to create one yourself, explain what you're about to do, and ask for confirmation before creating pages in the user's workspace. Create a page with an inline database using this minimal schema (it can be enriched if the user wants):

| Property | Type | Notes |
|---|---|---|
| `Company` | title | company or agency name |
| `Role` | text | position applied for |
| `Application Status` | status | options: `No contact`, `Applied`, `HR Contact`, `Intro Call`, `Technical Test`, `Rejected`, `Offer Accepted` |
| `Last Updated` | date | date of the last detected status change |
| `Notes` | text | free-form summary of what happened / next steps |

[CRITICAL ORDERING RULE]: You MUST define and inject these properties into the Notion API payload in the exact top-to-bottom sequence shown in the table above: 1. Company, 2. Role, 3. Application Status, 4. Last Updated, 5. Notes. Notion establishes the visual column order based strictly on the sequence of the creation object; do not scramble or alphabetize them.

Use `notion-create-pages` (or whichever database-creation tool is available) to generate it, then share the link with the user and ask for confirmation that it looks right before starting to populate it.

**If a database already exists but with different column names** (e.g. "Azienda" instead of "Company", "Stage" instead of "Application Status", or no Notes column at all):
- Adapt the matching to the real column names — don't assume they match the ones above. Always read the actual schema with `notion-fetch` on the data source before writing anything.
- If a column you need for useful information is missing (typically `Notes` or a last-updated date), propose adding it with `notion-update-data-source` (`ADD COLUMN ...`) **asking for confirmation before modifying the schema of an existing user database**.
- If the `Application Status` property has different or incomplete options compared to the table above, don't force the existing options: map the classification from Step 5 onto the closest available option, and if important stages are genuinely missing, propose (without imposing) adding them.

### 0.3 First run vs. subsequent syncs
If the database is empty or newly created, every application found will be a new row. If it already contains rows, run the matching described in Step 4 before deciding whether to create or update an entry.

## Execution flow (after Step 0)

### 1. Read the current state of the Notion database
- `notion-fetch` on the page URL/ID to find the inline database and its `data-source-url` (`collection://...`).
- `notion-fetch` on the data source to read the exact schema (property names, available status options) — never assume the names, always read them.
- Try reading existing rows with `notion-query-database-view` or `notion-query-data-sources` (SQL). **Note**: both tools require a Notion Business+ plan with Notion AI: if they fail with `validation_error`, use `notion-search` with `data_source_url` set to the data source to retrieve existing pages, or proceed by browsing the database view directly.

### 2. Scan Gmail (if connected)
Search with `Gmail:search_threads`, a query like:
```
newer_than:7d (application OR interview OR screening OR "intro call" OR "technical assessment" OR recruiter OR position OR offer OR hiring)
```
Adapt the keywords to whatever language(s) the user receives emails in (e.g. add Italian terms like "candidatura", "colloquio", "prova tecnica" if they're also applying to companies in Italy, or other languages as needed).

Default window "7d", or since the last sync date if known. For ambiguous threads, use `Gmail:get_thread` or `Gmail:get_message` to read the full body before classifying — don't rely on the snippet alone.

Discard clearly promotional/irrelevant emails (newsletters, generic platform alerts with no specific application behind them).

### 3. Scan Google Calendar (if connected)
`Google Calendar:list_events` for the current month (`startTime`/`endTime` first-to-last day of the month), including both past and future events. Look for interviews, calls, and screenings in the title/description/attendees.

### 4. Company ↔ Email/Event matching algorithm
For every email or event found:
1. Extract the sender's domain (`name@company.com` → `company`), strip TLDs (`.com`, `.io`, `.co`...) and common corporate suffixes (`inc`, `ltd`, `llc`, `group`, `srl`, `spa`...).
2. Compare the cleaned name against the "company" column of the database (whatever its real name is): match if one contains the other (case-insensitive).
3. **Recruiting platforms/agencies** (LinkedIn, Teamtailor, Greenhouse, Lever, Indeed, staffing agencies like Randstad/Adecco/Robert Half/Akkodis...): the sender's domain is NOT the company. Extract the real company name from the subject or body (e.g. "your application for [Role] at [Company]", or the client name mentioned in the text). If the agency doesn't name a specific client (a general/spontaneous application to the agency itself), use the agency name as the Company and note this in the Notes field.
4. If no matching row is found → this is a new application to create.
5. If a matching row is found → evaluate whether the status or notes need updating (see Step 5).

### 5. Status classification (rules)
Read the full subject + body of the email (or the calendar event description) and classify it according to these rules, from most recent/advanced. The labels below are the "standard" ones proposed in Step 0.2: if the user's database uses different labels, map onto the closest concept.

| "Standard" status | Typical signals |
|---|---|
| Rejected | "we've decided not to move forward", "not the right fit at this time", explicit negative outcome |
| Offer Accepted | signed/accepted job offer, confirmed contract |
| Technical Test | technical assessment, live coding, HackerRank/Codility, "test completed", interview with the engineering team |
| Intro Call | invitation/confirmation of a call or interview (Teams/Meet/Zoom) with a recruiter or hiring manager, "intro chat", "first round" |
| HR Contact | a recruiter has replied/written but without a call scheduled yet, request for more information, discussion about work mode/location |
| Applied | automatic confirmation of application receipt ("thank you for applying", "we've received your resume"), no human contact yet |
| No Contact | application planned but not yet submitted (rarely used in this flow) |

Priority rules:
- If a thread contains multiple stages (e.g. application → then test → then call), always use the **latest stage reached**, not the first one.
- In case of genuine ambiguity between two adjacent statuses, prefer the one supported by the most recent, concrete signal (e.g. a calendar invite beats a vague mention in text).
- If the Notion schema doesn't have a status that accurately represents the situation, use the closest available status and **add the detail in Notes** instead of forcing a new status option into the schema, unless the user explicitly asks to expand the options.

### 6. Write to Notion
- **New application** → `notion-create-pages` with the `data_source_id` as parent. You MUST populate the page properties strictly respecting the established column order in the schema array/object:
  1. `Company`
  2. `Role`
  3. `Application Status`
  4. `Last Updated`
  5. `Notes`
     
  Never omit a property from the creation payload if it belongs to the standard schema, even if empty, to preserve the structural visual order.- **Existing application with a changed status** → `notion-update-page` (`command: update_properties`) on the matching page, updating status, notes, and date.
- Don't touch existing applications if there's no new signal from email/calendar.

### 6.5 Duplicate detection and removal
Before producing the final report, check the full set of rows read in Step 1 (plus anything just created/updated in Step 6) for duplicates: two or more rows referring to the same company (same cleaned name per the matching rules in Step 4).

- For each group of duplicate rows for the same company, keep **only the row with the most recent "Last Updated" date** and treat all the others as duplicates to remove.
  - If two duplicate rows are tied on "Last Updated" (same date), use the underlying page's actual last-edited timestamp (visible in `notion-search` results, or by comparing which row was just written in this run) as a tiebreaker, and keep the more recently edited one.
- **Removal mechanism**: this integration has no tool to permanently delete a Notion page. To "remove" a duplicate:
  1. Rename its title to prefix it clearly, e.g. `[DUPLICATO - da eliminare] ` (adapt the language to the user's), so it's unambiguous and easy to find later.
  2. Use `notion-move-pages` to move it out of the database, to `{"type": "workspace"}` (private page at workspace level). This removes it from the tracked database/view without touching the row you're keeping.
- Never guess which row to keep based on content length or how detailed the notes look — always use the "Last Updated" date (with the edit-timestamp tiebreaker above) as the deciding factor.
- Do this automatically, without asking for confirmation first, since the action is reversible (the page still exists, just moved and relabeled, not deleted) and the criteria are deterministic. Still, always tell the user clearly in the final report which duplicates were found and that they were removed/moved (see Step 7), and mention that true permanent deletion isn't available via this integration — the person can delete the relabeled pages themselves from Notion's trash/sidebar if they want them gone for good.

### 7. Final report
Present the user with a text summary (a markdown table is fine) including:
- Applications added (company, role, status, brief reason)
- Applications updated (previous status → new, reason)
- Duplicates found and removed/moved (company, which row was kept vs. removed and why, i.e. the "Last Updated" dates compared), explicitly telling the user the duplicate records were removed/moved out of the database
- Any emails excluded as irrelevant (brief mention, no need to list them all)
- Any issues encountered (e.g. Notion tools unavailable on the current plan, missing connector, database created from scratch)

## Notes for first-time users
- You don't need everything set up in advance: if the database, connector, or a column is missing, the skill walks you through creating them step by step, asking for confirmation before making any change to your Notion workspace.
- If you're not sure what column names or statuses you want to use, the standard schema proposed in Step 0.2 works fine — you can always rename or expand it later, directly in Notion.
- If you also receive applications/interviews in other languages, mention it: the email search keywords in Step 2 should be adapted accordingly.

## Known limitations
- This skill doesn't run on its own: in a normal chat it needs to be triggered manually ("sync my job applications"). To run it automatically every N hours, you need **Claude Cowork** with a scheduled task (`/schedule`), pasting these instructions in.
- `notion-query-database-view` and `notion-query-data-sources` require a Notion Business+ plan with Notion AI: if the workspace doesn't support it, matching against existing rows has to be done via `notion-search` or by browsing, with a higher margin of error.
- Generic alert emails (job alerts, newsletters from learning platforms) should never be treated as job applications.
- There's no tool available to permanently delete a Notion page. Duplicate rows (see Step 6.5) are relabeled and moved out of the database rather than deleted outright; the user can permanently delete them from Notion's own trash/sidebar if they want.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [daliaabbruciati](https://github.com/daliaabbruciati)
- **Source:** [daliaabbruciati/job-application-tracker-SKILL](https://github.com/daliaabbruciati/job-application-tracker-SKILL)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-daliaabbruciati-job-application-tracker-skill-job-application-tracker-skill
- Seller: https://agentstack.voostack.com/s/daliaabbruciati
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
