Jobgru pipeline Phase 2 — find LinkedIn hiring contacts for new Job Applications rows (job-posting hiring team first, then people search), write at most 5 profile links plus the company people page into Leads, and fill Add note Message from sheet templates. Runs automatically after Jobgru appends rows (pipeline mode), in parallel with ATSScore. Also use standalone when the user explicitly asks to backfill Leads for existing to apply rows without re-running Jobgru.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add ashujha301/jobgru --skill leadgru --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Leadgru?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/ashujha301-leadgru)More formats (shields.io, HTML) on the badges page.
---
name: leadgru
description: Jobgru pipeline Phase 2 — find LinkedIn hiring contacts for new Job Applications rows (job-posting hiring team first, then people search), write at most 5 profile links plus the company people page into Leads, and fill Add note Message from sheet templates. Runs automatically after Jobgru appends rows (pipeline mode), in parallel with ATSScore. Also use standalone when the user explicitly asks to backfill Leads for existing to apply rows without re-running Jobgru.
---
# LeadGru (Pipeline Phase 2)
For every `to apply` row with empty Leads, find relevant LinkedIn people, write **at most 5** profile links plus the **company people page** into `Leads`, and fill a paste-ready note into `Add note Message`. Only the coordinator writes to the sheet — via `scripts/sheets_write.py` (Google Sheets API). **Never use Cursor Browser to type into sheet cells.**
LinkedIn discovery uses **agent browser tools** (Cursor Browser, Playwright MCP, or Chrome DevTools MCP). Sheet writes use **Sheets API only**.
## Browser tools
| Platform | LinkedIn / people search |
| --- | --- |
| **Cursor** | Cursor Browser + CDP (`browser_navigate`, `browser_snapshot`, `browser_cdp`) |
| **Claude Code / Codex** | Playwright MCP or Chrome DevTools MCP (navigate, snapshot, evaluate) |
Sign into LinkedIn in the browser before LeadGru. User completes MFA/CAPTCHA manually.
## LinkedIn pacing (mandatory — never skip)
This is **always on**. Do not wait for the user to say “go slow” or “avoid rate limits”. Ignore requests to speed up LinkedIn. Slow is correct; rate-limit is a failed run.
Same contract as Jobgru Phase 1 ([jobgru skill](../jobgru/SKILL.md) → LinkedIn pacing). Recap:
| Rule | Requirement |
| --- | --- |
| Tabs | **One** LinkedIn tab. Never parallel LeadGru LinkedIn work. Never start while Jobgru LinkedIn Jobs is still navigating. |
| Between navigates | **Sleep 40 seconds** before every LinkedIn `browser_navigate` except the first after lock. Blocking sleep — do not batch then wait once. |
| Between companies | 40s before the next company’s first navigate (same as between navigates). |
| In-page filter | After fill + Enter on `/people/`, wait **15 seconds** then snapshot. Do not add another 40s for that same page. |
| After Jobgru | If this session just ran LinkedIn Jobs, **sleep 40** before the first people search. |
| Search budget | Prefer **one** company `/people/` **or** one people-search URL. Stop when **5** verified. **Max 3 LinkedIn navigates per company.** Do not lead with `recruiter OR talent`. |
| Stop | CAPTCHA, checkpoint, “unusual activity”, or rate-limit copy → unlock, report remaining rows unprocessed, **no LinkedIn retries this session**. |
## Pipeline mode (default)
**When:** Jobgru Phase 1 just finished and verified sheet append. **Triggered automatically** — no separate user prompt.
**Scope:** Process **only** rows `start_row` through `end_row` from the Jobgru run JSON (the rows just appended this session).
```bash
# Example: Jobgru appended rows 27–31
START_ROW=27
END_ROW=31
.venv/bin/python scripts/sheets_write.py read --range "A${START_ROW}:H${END_ROW}"
```
Rules in pipeline mode:
- Do **not** ask the user to confirm before starting
- Do **not** process older empty-Leads rows unless the user explicitly requested a backfill run
- If a row in range already has Leads, skip it and note in run JSON
- After all rows in range: `format-layout`, verify, update combined run JSON
- Return control to Jobgru completion report (one summary for both phases)
## Standalone mode (backfill only)
**When:** User explicitly asks for LeadGru only (e.g. "run LeadGru", "fill leads for empty rows") **without** a Jobgru run in the same session.
**Scope:** All eligible rows in the sheet (`A2:H`, open-ended) where Status is `to apply` and Leads (G) is empty.
Use [prompts/leadgru-run.md](../../prompts/leadgru-run.md) for standalone backfill prompts.
**SendGru is separate:** LeadGru finds people and fills note templates; it **does not** send LinkedIn invites. Sending is on-demand via [sendgru/SKILL.md](../sendgru/SKILL.md) when Status is **applied**.
## Prerequisites (user setup)
Run **Jobgru check** first: `.venv/bin/python scripts/jobgru_check.py` — or say **Jobgru check** in chat.
Setup guide: [jobgru-setup skill](../jobgru-setup/SKILL.md) and [README.md](../../README.md).
If auth fails, ask user to run `gcloud auth login --enable-gdrive-access --update-adc`.
Before LeadGru: user must be **signed into LinkedIn** in the agent browser (Premium helps).
All shell commands assume **current working directory = project root**.
## Destination sheet (constants)
Read **`config/sheet.json`** at the start of every run (copy from `config/sheet.json.example` if missing).
| Constant | Source |
| --- | --- |
| Sheet URL | `sheet_url` in `config/sheet.json` |
| Spreadsheet ID | `spreadsheet_id` in `config/sheet.json` |
| Tab name | `tab` in `config/sheet.json` |
| Resume link default | Column **O** catalog (O2:O{n}) + `resume_link` in config; see [Add note Message](#add-note-message-column-h) |
| Write script | `scripts/sheets_write.py` |
| Python | `.venv/bin/python` |
| Run summaries | Combined in `data/runs/<YYYY-MM-DD-HHMM>.json` (pipeline) or `data/runs/leadgru-<YYYY-MM-DD-HHMM>.json` (standalone) |
| User guide | `README.md` |
| Auth setup | `scripts/SHEETS-API-SETUP.md` |
| Pipeline Phase 1 | `.cursor/skills/jobgru/SKILL.md` |
| Standalone prompt | `prompts/leadgru-run.md` |
### Column layout
| Column | Header | This skill |
| --- | --- | --- |
| A | Company Name | Read only |
| B | Position | Read only |
| C | Apply link | Read only |
| D | Status | Read only; work when `to apply` |
| E | Date Applied | Never touch |
| F | Details if any | Read only |
| G | Leads | **Write** when empty |
| H | Add note Message | **Write** when empty |
| I | ATS score | Read only (ATSScore writes) |
| J | Suggestions on Resume | Read only (ATSScore writes) |
| Q | Add Note Template | Read only (rows Q2–Q7) |
| K+ | Summary, resume, etc. | Read only |
Older rows may have apply URLs inside column F instead of column C; still process them normally.
## Which rows to process
Process when all true:
1. Status (column D) is `to apply` (case-insensitive)
2. Company Name (column A) present
3. Leads (column G) empty
Skip when Status is not `to apply` or Leads already has names, `/in/` URLs, or company page links.
If Add note Message (column H) is filled, still fill Leads when empty; do not overwrite the note.
Process every matching row in the batch.
- **Pipeline mode:** rows `start_row`–`end_row` from Jobgru run only (typically 5–10 companies)
- **Standalone mode:** all eligible rows in `A2:H` (open-ended — includes 501+)
## Coordinator workflow
### Pipeline mode (after Jobgru)
1. Receive `start_row`, `end_row` from Jobgru handoff (or read from `data/runs/<latest>.json`).
2. **Read batch + templates** (Sheets API):
```bash
.venv/bin/python scripts/sheets_write.py read --range "A${START_ROW}:H${END_ROW}"
.venv/bin/python scripts/sheets_write.py read --range "Q1:Q7"
```
3. Filter eligible rows in range (Status `to apply`, Leads empty).
4. **LinkedIn pass** — one company at a time → [LinkedIn search runbook](#linkedin-search-runbook-exact-steps).
5. **Write each row immediately** after that company's leads are verified → [Sheet write runbook](#sheet-write-runbook-exact-steps).
6. **Verify** batch: `read --range "A${START_ROW}:H${END_ROW}"`.
7. `format-layout`.
8. Update combined run JSON with `leadgru_rows_processed`, `leadgru_status`.
9. `browser_lock` with `action: unlock` when all LinkedIn work is done.
### Standalone mode (backfill)
1. **Read rows + templates** (Sheets API — do not open sheet in browser):
```bash
.venv/bin/python scripts/sheets_write.py read --range "A2:H"
.venv/bin/python scripts/sheets_write.py read --range "Q1:Q7"
```
2. Filter eligible rows (Status `to apply`, Leads empty).
3. **LinkedIn pass** — one company at a time → [LinkedIn search runbook](#linkedin-search-runbook-exact-steps).
4. **Write each row immediately** after that company's leads are verified → [Sheet write runbook](#sheet-write-runbook-exact-steps).
5. **Verify** batch with Sheets API read.
6. `format-layout`.
7. Optional: save summary to `data/runs/leadgru-<YYYY-MM-DD-HHMM>.json`.
8. `browser_lock` with `action: unlock` when all LinkedIn work is done.
## Concurrency limits
- **One** LinkedIn company search at a time (no parallel LinkedIn tabs)
- Follow [LinkedIn pacing](#linkedin-pacing-mandatory--never-skip) on every company — not optional
- Write each row immediately after that company's search completes
- Stop on CAPTCHA, login challenge, or rate-limit warning — report and skip remaining companies
## Who to find
**Priority order (fill Leads top-to-bottom in this order, still max 5 `/in/` + company page):**
0. **Job posting profiles (highest)** — people LinkedIn shows on the **apply link** (column C): “Meet the hiring team”, “People you can reach out to”, job poster, or any clickable `/in/` profile attached to the listing. See [Job posting hiring team](#job-posting-hiring-team-priority-leads).
1. Recruiters, Talent Acquisition, HR, People Ops, sourcers
2. Hiring managers (Engineering Manager, Head of Engineering, VP Eng, AI/ML lead)
3. Team leads matching the role
For small companies (~50 employees or founding/startup roles):
4. CEO, Founder, CTO, Founding Engineer, other founding ICs
Only add people whose LinkedIn headline or current experience shows this company — **except** profiles LinkedIn explicitly attaches to the job posting (treat those as verified for that role).
Skip unrelated functions unless the company is tiny.
## Job posting hiring team (priority leads)
Many LinkedIn job pages (`linkedin.com/jobs/view/...`) show hiring contacts **on the listing itself** — not only in people search.
**When column C is a LinkedIn jobs URL**, open it **before** company people search (counts as one of the **3 navigates per company** budget).
### What to look for
| UI label (examples) | Action |
| --- | --- |
| **Meet the hiring team** | Extract every `/in/` profile in that block |
| **People you can reach out to** | Same |
| **Job poster** / **Posted by** | Include poster’s `/in/` link |
| Profile cards in the job sidebar or description | Any `/in/` link with name + title on the job page |
Also scan the job description text for `linkedin.com/in/` links (some listings embed profile URLs in copy).
### How to extract (CDP)
After `browser_navigate` to column C apply link, wait **3s**, snapshot, then `browser_cdp` → `Runtime.evaluate`:
```javascript
(() => {
const sections = Array.from(document.querySelectorAll('section, div[class*="jobs"], div[class*="hiring"]'));
const hiringBlock = sections.find(el =>
/meet the hiring team|people you can reach out|hiring team|job poster|posted by/i.test(el.innerText || '')
);
const root = hiringBlock || document;
const seen = new Set();
return Array.from(root.querySelectorAll('a[href*="/in/"]'))
.map(a => ({
href: a.href.split('?')[0].replace(/\/+$/, '') + '/',
name: (a.querySelector('span[aria-hidden="true"]')?.innerText || a.innerText || '').split('\n')[0].trim(),
title: (a.closest('li, div')?.innerText || '').split('\n').slice(1, 3).join(' ').trim().slice(0, 80)
}))
.filter(x => x.href.includes('/in/') && x.name.length > 1)
.filter(x => { if (seen.has(x.href)) return false; seen.add(x.href); return true; })
.slice(0, 5);
})()
```
If the block is empty, run a **whole-page fallback** (sidebar + description):
```javascript
Array.from(document.querySelectorAll('a[href*="/in/"]'))
.map(a => ({ href: a.href.split('?')[0], text: (a.closest('div')?.innerText || a.innerText || '').slice(0, 120) }))
.filter(x => x.text.length > 2)
.reduce((acc, x) => { if (!acc.find(y => y.href === x.href)) acc.push(x); return acc; }, [])
.slice(0, 5)
```
Manually keep profiles that look like hiring contacts (recruiter, hiring manager, poster) — drop random “similar profiles” or unrelated `/in/` links from footer/nav.
### Merge into Leads
1. Write **job-posting profiles first** (top lines in column G).
2. If fewer than 5, fill remaining slots from [company people search](#per-company-search-repeat-for-each-row) (dedupe by URL — never list the same `/in/` twice).
3. Still append `Company: .../people/` as the last line.
Example (Birlasoft-style): hiring manager from “Meet the hiring team” on the job page → line 1; then up to 4 more from people search.
Helper: `scripts/leadgru_leads.py` → `merge_leads(priority_people, search_people, company_url)` dedupes and applies the 5-person cap.
## How many people
Hard cap (never exceed, never skip the company people page):
- **At most 5** verified `/in/` profiles per row — stop searching as soon as you have 5
- **Always** append the company people page as the last line: `Company: https://www.linkedin.com/company/<handle>/people/`
- If the `/people/` URL is unknown, use `https://www.linkedin.com/company/<handle>/` and add `/people/` when the handle is known
- If fewer than 5 after real search: write all found **and** the company people page
- If none verified: company people page only
- Never leave Leads blank on a processed row
- Never write 6+ people “because they were easy to find”
## Leads format (column G)
```
Name — Title — https://www.linkedin.com/in/<handle>/
Name — Title — https://www.linkedin.com/in/<handle>/
Company: https://www.linkedin.com/company/<handle>/
```
Rules:
- One person per line, newline-separated
- Use `linkedin.com/in/` URLs only (strip `?` query params)
- Trailing slash on URLs is fine
- Last line **always** `Company: https://www.linkedin.com/company/<handle>/people/` (required even when 5 people are listed)
- Title from LinkedIn card headline or role at company
- Helper: `scripts/leadgru_leads.py` (`format_leads` / `validate_leads_cell`) — max 5 `/in/` URLs
## Add note Message (column H)
**Pipeline mode:** LeadGru writes **Leads (G) only**. Do **not** fill H during LinkedIn search. The Jobgru coordinator fills H **after ATSScore** using the highest-ATS resume link (see below).
**Standalone mode:** If column I/J already has ATS scores, fill H immediately using resolved link. Otherwise run ATS first, then `resume_catalog.py fill-notes`.
### Latest Resume catalog (column O)
Rows **O2:O{n}** — one resume per row:
```text
https://bit.ly/aj_be , backend
Ayush_SWE.pdf , SWE
```
Left = share URL (preferred) or filename; right = role label (must match ATS manifest `label`).
Read catalog:
```bash
.venv/bin/python scripts/sheets_write.py read --range "O1:O10"
```
### Read templates
Templates live in column **Q**, rows **Q2–Q7** (header in Q1):
```bash
.venv/bin/python scripts/sheets_write.py read --range "Q1:Q7"
```
### Fill formula
Pick one template per row; **rotate templates 1–6** across rows in the batch (row 1 of batch → Q2, row 2 → Q3, … row 6 → Q7, row 7 → Q2 again).
Substitute:
| Placeholder | Source |
| --- | --- |
| `{Position}` | Column B (Position) — use full title as written |
| `{Company}` | Column A (Company Name) — use full name as written |
| `{Link}` | **Highest ATS resume** — resolve via `resume_catalog.py resolve --row R` (matches column J `Best match:` label to column O catalog). Fallback: single O2 entry, then `resume_link` in config |
Always use greeting `Hi,` — never `Hi {Name},` and never a lead name. Do not append your own name (they see it on your LinkedIn profile). End with `Thanks` (no `!`).
**Length:** filled H must be **≤ 200 characters**. Do not write the raw template.
**Pipeline — fill H after ATS (coordinator or standalone backfill):**
```bash
.venv/bin/python scripts/resume_catalog.py fill-notes --rows "${START_ROW}-${END_ROW}"
```
**Manual single row:**
```bash
LINK=$(.venv/bin/python scripts/resume_catalog.py resolve --row R)
.venv/bin/python scripts/leadgru_notes.py fill \
--template "TEMPLATE_FROM_Q" \
--position "COLUMN_B" \
--company "COLUMN_A" \
--link "$LINK"
```
The script substitutes `{Position}` `{Company}` `{Link}`, and clamps to 200 while keeping the resume short link and `Thanks`.
Write only if Add note Message (column H) is empty. **Do not overwrite existing H notes.**
Example output (template 1, short job):
```
Hi, I just applied for AI Engineer at Acme and would appreciate a referral. Resume: https://bit.ly/ajha Thanks
```
---
## LinkedIn search runbook (exact steps)
**Use Cursor Browser for LinkedIn only.** Do not type into the Google Sheet.
### Browser lock order
1. `browser_tabs` with `action: list` — check for an existing LinkedIn tab
2. If LinkedIn tab exists: `browser_lock` with `action: lock` **first**
3. If no tab: `browser_navigate` to LinkedIn search URL, then `browser_lock` with `action: lock`
4. When **all** companies done: `browser_lock` with `action: unlock`
Correct order: navigate (if needed) → lock → searches → unlock.
### Per-company search (repeat for each row)
**Before each company after the first:** sleep **40 seconds** ([LinkedIn pacing](#linkedin-pacing-mandatory--never-skip)). First company in a locked session may navigate immediately.
**Step 0 — Job posting hiring team (when apply link is LinkedIn)**
If column C matches `linkedin.com/jobs/view/` or `linkedin.com/jobs/collections/`:
1. If this is not the first LinkedIn navigate of the phase, **sleep 40s** first.
2. `browser_navigate` to column C apply URL.
3. Wait **3s**, snapshot — check for [job posting hiring team](#job-posting-hiring-team-priority-leads) UI.
4. Run CDP extract; save profiles as **priority leads** (name, title, `/in/` URL).
5. If you already have **5** verified `/in/` URLs from the job page alone, skip to Step 6 (company page) — still need the `Company:` line.
6. Otherwise continue to Step 2 people search to fill remaining slots (max **5** total people).
If column C is **not** LinkedIn (Wellfound, company careers site, etc.), skip Step 0 and start at Step 2.
**Step 1 — Normalize company name for search**
- Strip parentheticals for the keyword: `Businessonbot (Y Combinator W21)` → search `BusinessOnBot` or `Businessonbot`
- Keep full name in Add note Message (column A value)
- If company has a different LinkedIn brand name, search both (see [Known company aliases](#known-company-aliases-aug-2026-test))
**Step 2 — Primary people search URL**
If this is not the first LinkedIn navigate of the phase, **sleep 40** first.
Navigate to (URL-encode `{KEYWORD}`):
```
https://www.linkedin.com/search/results/people/?keywords={KEYWORD}&origin=GLOBAL_SEARCH_HEADER
```
Examples that worked (Aug 2026 test):
| Row | Company (sheet) | Search keyword / URL |
| --- | --- | --- |
| 22 | Businessonbot (Y Combinator W21) | `BusinessOnBot` |
| 23 | SuperProfile | `"SuperProfile" Cosmofeed` (employees list under Cosmofeed) |
| 24 | Boock.ai | `Boock.ai` |
| 25 | Prodigal | `Prodigal` |
| 26 | HackerRank | `HackerRank` |
**Step 3 — If primary search returns unrelated people**
Sleep **40s** before each extra navigate. Stop after **3 navigates** even if under 5 people — then write what you have + company people page. Stop earlier when 5 people are verified.
Try in order (stop when you have 5 verified profiles):
1. Add disambiguator: `"SuperProfile" Gurgaon creator`, `"SuperProfile" creator platform`
2. Company people page: `https://www.linkedin.com/company/{handle}/people/`
3. Hiring-role keyword (only if company name alone fails): `{COMPANY} recruiter OR talent` — **often returns noise**; prefer company name search
**Do not** rely on generic `recruiter OR talent OR founder` as the first query — test run showed it returns unrelated profiles.
**Step 4 — Extract profile URLs via CDP**
Use `browser_cdp` → `Runtime.evaluate` with `returnByValue: true`. Prefer `.linked-area` card text to verify employment.
**Pattern A — filter by company in card text (best, worked for Boock.ai, Prodigal, SuperProfile):**
```javascript
Array.from(document.querySelectorAll('a[href*="/in/"]')).map(a => ({
href: a.href.split('?')[0],
t: a.closest('.linked-area')?.innerText?.slice(0, 200) || ''
})).filter(x => /COMPANY_REGEX/i.test(x.t))
.reduce((acc, x) => { if (!acc.find(y => y.href === x.href)) acc.push(x); return acc; }, [])
.slice(0, 5)
```
Replace `COMPANY_REGEX` with company name variants (e.g. `boock`, `prodigal`, `superprofile|cosmofeed`).
**Pattern B — result list containers:**
```javascript
Array.from(document.querySelectorAll('li.reusable-search__result-container, div[data-chameleon-result-urn]'))
.map(li => {
const a = li.querySelector('a[href*="/in/"]');
const title = li.innerText;
return a ? { href: a.href.split('?')[0], title: title.split('\n').slice(0, 4).join(' | ') } : null;
}).filter(Boolean).filter(x => /COMPANY_REGEX/i.test(x.title)).slice(0, 5)
```
**Pattern C — entity-result divs (when card text empty):**
```javascript
Array.from(document.querySelectorAll('div.entity-result__content')).map(div => {
const a = div.querySelector('a[href*="/in/"]');
return a ? { href: a.href.split('?')[0], t: div.innerText.slice(0, 160) } : null;
}).filter(Boolean).filter(x => /COMPANY_REGEX/i.test(x.t)).slice(0, 5)
```
**Pattern D — quick dump when cards match visually (Businessonbot first pass):**
```javascript
Array.from(document.querySelectorAll('a[href*="/in/"]')).slice(0, 30)
.map(a => ({ href: a.href.split('?')[0], text: a.innerText.trim().slice(0, 120) }))
.filter(x => x.text.length > 2)
```
Manually filter results to people showing the target company in snapshot/card text.
**Step 5 — Verify URLs before writing**
- **Never guess** `/in/` slugs from names (e.g. `dhruv-grover-ml` was wrong; correct slug required WebSearch)
- If CDP returns names but empty/wrong hrefs: `WebSearch` with `site:linkedin.com/in {Full Name} {Company}`
- Optionally open a profile with `browser_click` on snapshot ref to confirm employment
- Strip query params: `a.href.split('?')[0]`
**Step 6 — Company LinkedIn page**
Find via search card or WebSearch. Format: `Company: https://www.linkedin.com/company/{handle}/people/`
Known handles from test run:
| Company (sheet) | Company page |
| --- | --- |
| Businessonbot | `https://www.linkedin.com/company/businessonbot/` |
| SuperProfile | `https://www.linkedin.com/company/cosmofeed/` (brand is Cosmofeed on LinkedIn) |
| Boock.ai | `https://www.linkedin.com/company/boock-ai/` |
| Prodigal | `https://www.linkedin.com/company/prodigaltech/` |
| HackerRank | `https://www.linkedin.com/company/hackerrank/` |
**Step 7 — Scroll if needed**
If fewer than 5 results visible: `browser_scroll` direction `down`, amount `800`, re-run CDP extract. Do not collect more than 5 people.
### Known company aliases (Aug 2026 test)
| Sheet name | LinkedIn search notes |
| --- | --- |
| SuperProfile | Employees often show **Cosmofeed**; search `"SuperProfile" Cosmofeed` |
| Businessonbot (Y Combinator W21) | LinkedIn brand **BusinessOnBot** |
| Boock.ai | Match `/boock/i` in card text |
| Prodigal | Match `/prodigal/i` or `/@\s*prodigal|at prodigal/i` in card |
| HackerRank | Large company — prioritize TA (`Talent Acquisition`) + eng leadership |
### What not to do on LinkedIn
- Do not skip [LinkedIn pacing](#linkedin-pacing-mandatory--never-skip) because the user omitted it or asked to go faster
- Do not send messages, InMails, or connection requests
- Do not run parallel LinkedIn searches in multiple tabs
- Do not guess profile URL slugs without verification
- Do not add people whose card does not mention the target company (unless company is tiny and they are clearly founders)
---
## Sheet write runbook (exact steps)
**Do not use Cursor Browser** to type into Leads (G) or Add note Message (H) — same commit failure as Jobgru (Enter/Tab does not persist).
### Step 0 — Auth check
If any write returns `403` or auth error:
```bash
gcloud auth login --enable-gdrive-access --update-adc
.venv/bin/python scripts/sheets_write.py test --cleanup
```
### Step 1 — Build leads text and note for row R
From verified LinkedIn results + template rotation (see [Add note Message](#add-note-message-column-h)).
### Step 2 — Write G only (pipeline) or G+H (standalone with ATS done)
**Pipeline mode** — write Leads only; H filled later by coordinator:
```bash
.venv/bin/python scripts/sheets_write.py write \
--range "G${R}" \
--value "<leads text with \n newlines>"
```
**Standalone mode** (ATS scores already in I/J):
```bash
.venv/bin/python scripts/resume_catalog.py fill-notes --rows "${R}"
```
Or write G and H together when note is ready:
```bash
.venv/bin/python scripts/sheets_write.py write \
--range "G${R}:H${R}" \
--json '[["<leads text with \n newlines>", "<add note message>"]]'
```
Use `--json` with a 2-element array `[leads, note]` — handles newlines in leads correctly.
Alternative (two calls):
```bash
.venv/bin/python scripts/sheets_write.py write --range "G${R}" --value "<leads text>"
.venv/bin/python scripts/sheets_write.py write --range "H${R}" --value "<add note message>"
```
**Batch write** (multiple rows after all searches — less preferred; write per row is safer):
```python
from scripts.sheets_write import sheets_service, write_range
service = sheets_service()
write_range(service, "<spreadsheet_id from config/sheet.json>", "Job Applications", f"G{R}:H{R}", [[leads, note]])
```
### Step 3 — Verify
```bash
.venv/bin/python scripts/sheets_write.py read --range "A${R}:H${R}"
```
Confirm: G has `/in/` URLs, H has `Hi,` + role + company + resume link, D still `to apply`.
### Step 4 — Sheet layout (overflow)
After writing Leads and Add note Message, ensure long text wraps inside columns (not spilling into neighbors):
```bash
.venv/bin/python scripts/sheets_write.py format-layout
```
Column widths: F=240px, G=280px, H=300px, O (templates)=300px. Re-run anytime.
### Step 5 — Run JSON
**Pipeline mode** — merge into Jobgru run file:
```json
{
"pipeline": { "phase1": "jobgru", "phase2": "leadgru", "status": "complete" },
"start_row": 27,
"end_row": 31,
"leadgru_rows_processed": [27, 28, 29, 30, 31],
"leadgru_status": "complete",
"leadgru_skipped": [],
"linkedin_method": "cursor_browser_cdp",
"notes": ["SuperProfile → Cosmofeed on LinkedIn"]
}
```
**Standalone mode** — separate file `data/runs/leadgru-<YYYY-MM-DD-HHMM>.json`:
```json
{
"mode": "standalone",
"rows_processed": [22, 23, 24, 25, 26],
"sheet_write_method": "sheets_api",
"linkedin_method": "cursor_browser_cdp"
}
```
### All sheet commands (reference)
| Command | Purpose |
| --- | --- |
| `read --range "A2:H"` | Find eligible rows (all job rows, no 500 cap) |
| `read --range "Q1:Q7"` | Load Add note templates |
| `write --range "G{R}:H{R}" --json '[["...", "..."]]'` | Write leads + note |
| `read --range "A{R}:H{R}"` | Verify one row |
| `format-layout` | Wrap text + column widths (F/G/H/I/J/Q) — run after batch if overflow |
| `test --cleanup` | Auth smoke test |
Optional flags: `--spreadsheet-id`, `--tab "Job Applications"`.
---
## Do not use Cursor Browser for sheet writes
Browser automation **does not persist** Google Sheet edits in this environment. Confirmed same failure as Jobgru:
| Action tried | Result |
| --- | --- |
| Navigate to sheet URL | OK |
| Click cell / type in editor | Text shows in UI only |
| Press Enter or Tab | **Does not commit** |
| Sheets API read | **No new data** |
Use Sheets API for columns G and H only. Use Cursor Browser for LinkedIn only.
Do **not** open duplicate sheet tabs for writing.
---
## Safety
- Do not send messages, InMails, connection requests, or applications
- Do not guess employment at the company
- Always follow [LinkedIn pacing](#linkedin-pacing-mandatory--never-skip)
- Stop LinkedIn on warnings; report remaining companies unprocessed — no retries this session
## Quality check
1. Processed rows still have Status `to apply` in column D
2. No pre-filled Leads rows changed
3. Each new Leads cell (G) has **≤5** verified `/in/` links **and** a `Company:` people-page URL (or people page alone if nobody verified)
4. Every `/in/` URL verified (not guessed slugs)
5. Add note Message (H) has role, company, resume link, greeting `Hi,`, no names, ends with `Thanks`, **≤ 200 chars**
6. Columns A–F, E, K+ unchanged; **I (ATS score)** and **J (Suggestions on Resume)** untouched by LeadGru
7. Sheets API verify passed for processed range
## Completion report
**Pipeline mode:** Do not publish a separate LeadGru report — Jobgru coordinator merges Phase 2 into the combined completion summary.
**Standalone mode:** Report in chat and save to `data/runs/`:
- Mode: standalone backfill
- Eligible rows processed (row numbers + companies)
- Skipped (Leads already filled or Status not `to apply`)
- Per row: people count, company page included Y/N
- LinkedIn access limits (CAPTCHA, login, rate limit)
- Any company alias notes (e.g. SuperProfile → Cosmofeed)
- Sheet link
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!