# /gather-recent-context - Sync Job-Email Context into Twenty CRM Notes You are scanning the user's Fastmail inbox for conversations about **active job loops** (interview invites, assessments, scheduling, offers, recruiter follow-ups) and syncing each conversation's context onto its matching Opportunity in Twenty CRM as a **Note**. The mailbox is read-only to this command; Twenty receives classified summaries, never raw email bodies or attachments. Two halves, cleanly split: - **You** (this command) own the judgment: finding relevant threads via the **Fastmail MCP** tools and classifying each one into a structured signal. - **`tools/gather_recent_context.py`** owns everything deterministic: matching threads to active opportunities, building the note markdown, deduplicating by thread id (`job_scraper/context_sync.json`), and applying via Twenty REST (`TWENTY_API_KEY`) - or emitting a plan you apply through the Twenty MCP session. Follow these steps **in order**. --- ## Step 0: Parse Input `$ARGUMENTS` may contain: - Nothing → default lookback: 14 days - `since ` → override the lookback start - A company name → scope the scan to that one company's threads --- ## Step 1: Preflight (both MCP servers) Silently optional, same bar as `/sync-twenty`: a missing prerequisite ends in one clear message and a clean exit. 1. **Fastmail MCP** must be available (tools like `search_email`, `read_thread`). If not, say so and stop. 2. **Twenty** must be reachable one way or the other: `TWENTY_API_KEY` in the environment (script applies via REST) **or** the Twenty MCP session (you apply the plan). Neither → one line and stop. 3. Never initiate an OAuth flow from this command. --- ## Step 2: Find Candidate Threads (Fastmail MCP) 1. Build the search set from the CRM's **active loops**: fetch opportunities with stage in `RANKED / DRAFTED / APPLIED / INTERVIEW / OFFER` (via `execute_tool` → `find_many_opportunities`, selecting `id`, `name`, `stage`, `companyId`) and the company names via `find_many_companies` (`id`, `name`). Opportunity names are role-only; the employer comes from the `companyId` relation. 2. `search_email` with the lookback from Step 0 - query for recruiting/job signals (subject or sender mentioning the tracked companies, plus generic signals like "interview", "assessment", "application", "offer", "recruiter"). Prefer several narrow searches over one broad one; skip newsletters and job-board digests (LinkedIn/Indeed notification blasts are noise, not conversations). 3. `read_thread` on each candidate to see the whole conversation before classifying. --- ## Step 3: Classify into the Contract For each relevant thread, produce one entry - this is the exact input contract of the script: ```json {"threads": [{ "thread_id": "", "company": "", "role": "", "date": "YYYY-MM-DD", "from": "", "subject": "", "summary": "<1-3 sentences: what happened and what's next>", "signal": "interview | assessment | offer | rejection | scheduling | other" }]} ``` Rules: the summary is your words, not a quote; no email bodies, links, or attachments are copied; a thread you cannot confidently tie to a job conversation is dropped, not guessed. Write the JSON to a temp file (e.g. `/tmp/gather-emails.json`). --- ## Step 4: Run the Script ```bash python3 tools/gather_recent_context.py --emails /tmp/gather-emails.json # plan only python3 tools/gather_recent_context.py --emails /tmp/gather-emails.json --apply # with TWENTY_API_KEY ``` The script matches threads to active opportunities (normalized company name, role-token tiebreak), groups them into one note per opportunity titled `Email Context — (YYYY-MM-DD)`, skips every thread id already in `job_scraper/context_sync.json`, and writes `job_scraper/context_plan.json`. - **With `--apply`** (REST key available): notes and note-targets are created and the dedup state is committed. Done - go to Step 6. - **Without a key:** the plan file is your work order - go to Step 5. --- ## Step 5: Apply the Plan via Twenty MCP (keyless path) For each entry in the plan's `notes` array: 1. `create_many_notes` with `{position: "last", title, bodyV2: {markdown}}` (batches of ≤ 20). 2. `create_many_note_targets` linking each returned note id to its `opportunity_id` via `targetOpportunityId`. 3. After **all** notes land, commit the dedup state: ```bash python3 tools/gather_recent_context.py --commit-plan job_scraper/context_plan.json ``` Never commit state for notes that failed to create - the uncommitted thread ids simply reappear next run. --- ## Step 6: Report ``` ## Recent Context Sync - YYYY-MM-DD Scanned threads (lookback: days) · classified as job conversations Notes created: (one per opportunity) · Unmatched: · Already processed:

| Opportunity | Signal(s) | Threads | |-------------|-----------|---------| | Backend Engineer — Acme | interview, scheduling | 2 | ``` List unmatched threads (company + subject) so the user can add the opportunity or correct the company name; they are not state-committed and will re-surface next run. --- ## Important Rules 1. **Mailbox read-only.** Never label, archive, delete, or send from this command. 2. **Summaries only.** Raw email bodies, quoted text, links, and attachments never reach Twenty. 3. **Active loops only.** Threads about companies with no active-stage opportunity go to `unmatched`, never to a closed loop's record. 4. **Idempotent by thread id.** `job_scraper/context_sync.json` (personal state, gitignored) guarantees a re-run never duplicates a note. 5. **Notes are append-only context.** This command creates new notes; it never edits or deletes existing ones - including the `Fit Analysis` notes owned by `/sync-twenty`. 6. **Never guess a match.** An ambiguous thread is reported, not synced.