agentleFS
askoulwassimaskoulwassim/perfect-placement/skills/connect/SKILL.md

connect

Skill3 starsChanged 33 days agoMITView on GitHub

Find people who can open doors for a picked role, classify each one (hiring manager, hiring team, recruiter, peer, unrelated), and write outreach drafts (Hook / Proof / Ask + sign-off, in the user's voice) that land as Gmail drafts. Use after a role is picked (on-deck or later). Never sends, never connects or messages on LinkedIn.

64 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use connect-apps, skill-share and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • ArogyaReddySkill

    connect-apps

    Connect Claude to external apps like Gmail, Slack, GitHub. Use this skill when the user wants to send emails, create issues, post messages, or take actions in external services.

    Also in 19 projects that use this

  • ArogyaReddySkill

    skill-share

    A skill that creates new Claude skills and automatically shares them on Slack using Rube for seamless team collaboration and skill discovery.

    Also in 17 projects that use this

  • ComposioHQSkill

    agent-deep-links

    Build, validate, and troubleshoot deep links for Codex, Cursor, VS Code, Visual Studio, and similar tools. Use when users ask for clickable links (especially in Slack) that open threads, files, folders, or app settings.

    Also in 3 projects that use this

  • ComposioHQSkill

    helium-mcp

    Search real-time news with bias scoring, get live stock/ETF/crypto data with AI analysis, ML options pricing, balanced news synthesis, and meme search via the Helium MCP server.

    Also in 3 projects that use this

  • trendinuxSkill

    twitter-algorithm-optimizer

    Analyze and optimize tweets for maximum reach using Twitter's open-source algorithm insights. Rewrite and edit user tweets to improve engagement and visibility based on how the recommendation system ranks content.

    Also in 12 projects that use this

  • LangChainSkill

    langsmith-fetch

    Fetches LangSmith traces for debugging agent behavior. Use when troubleshooting agent issues, reviewing conversation history, or investigating tool calls.

    Also in 18 projects that use this

  • +1 more from ComposioHQ
  • +5 more from ArogyaReddy

The file

Show its rules and text

We split this skill into the rules it gives an agent

16 rules. 0 are common, 9 are distinctive, and 7 are very specific to this project.

  • 0

    Common

    Most of the 56 edited versions we read keep these.

  • 9

    Distinctive

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Collect hints (judgment): Read the JD (roles/<role_id>.md) and, if useful, the company team page.

    line 15
  2. Very specific:

    Hiring team: if the posting is on LinkedIn and lists "Meet the hiring team", note those names (hiring_team: true).

    line 20
  3. Distinctive:

    Never contact mail servers or verification sites: no SMTP, no mailbox probe, no test email, no email-checker or verification website or …

    line 42
  4. Distinctive:

    No em dashes, no en dashes.

    line 54
  5. Very specific:

    When people.find returns nobody usable, Connect falls back automatically (#78).

    line 65
  6. Very specific:

    Hiring team or recruiting address: Search the JD, the careers page, the dossier (companies/<slug>/dossier.md) and the company site for a …

    line 67
  7. Very specific:

    Call people.fallback { role_id, team_inbox: [{ email, name?, source: "jd" | "careers" | "dossier" | "company_site", url?

    line 69
  8. Distinctive:

    Read the Gmail draft and keep its subject and body exactly as they are (the user may have edited them in Gmail).

    line 90
  9. Very specific:

    Update that same draft with To = to.email.

    line 91
  10. Very specific:

    Never send an email. outreach.send is unimplemented on purpose.

    line 116
  11. Distinctive:

    Never create a Gmail draft without To for a recipient with no address at all (#132): it stays a vault draft.

    line 117
  12. Distinctive:

    Never connect, message, follow, like or InMail on LinkedIn, and never click a LinkedIn button.

    line 118
  13. Distinctive:

    Never store more than name, headline, URL, relationship and email (with its source and confidence).

    line 119
  14. Distinctive:

    Never contact a mail server or a verification site (SMTP, mailbox probes, test emails, email checkers).

    line 121
  15. Distinctive:

    Never run people.find on a role the user has not picked (an inbox or archived role).

    line 122
  16. Distinctive:

    Never commit anything from the vault.

    line 123

“Common” is measured: kept by at least 6 in 10 of the 56 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

connect

Stage 06 of the flow: runs on pick, outputs contacts + Gmail drafts.

You supply judgment (who is in the JD, who you read on LinkedIn, the draft text). Code does mechanics (URL allowlist, caps, budget, dedupe, tiers, files). Every write is an op.

Steps

  1. Check the role is picked. roles.get { role_id }. Starting Connect (runs.start or people.find) on an active or held role picks it (-> on-deck, role_picked, #105); inbox and archived roles are refused.
  2. Collect hints (judgment). Read the JD (roles/<role_id>.md) and, if useful, the company team page. List people named there: name, headline, URL or email if shown. Do not search for personal emails (the email step below looks for work addresses only).
  3. Read LinkedIn (judgment, read-only, #82). Only when the prompt's LinkedIn line says it is on (config.linkedin.mode: "live") and only for this picked role. Use {{browser.name}}'s read and navigate tools (listed under Tools); nothing that clicks, types or runs script is granted. Start with {{browser.tabs}} and reuse the tab this Chrome session created ({{browser.navigate}} it); create one with {{browser.new_tab}} only if none is usable, so no empty extra tab is left. Then, waiting the stated delay (min_delay_s to 2x) before every page, read in this order and stop at the page cap (max_pages_per_run) or the profile budget the prompt states:
    • Your 1st-degree connections at the company: https://www.linkedin.com/search/results/people/?keywords=<Company>&network=["F"].
    • The company People page, filtered by the role title's keywords: https://www.linkedin.com/company/<linkedin-slug>/people/?keywords=<title keywords>.
    • Mutuals for the top 2nd-degree candidates (at most max_mutual_expansions): the "N mutual connections" link on their card (a /search/results/people/ URL).
    • Hiring team: if the posting is on LinkedIn and lists "Meet the hiring team", note those names (hiring_team: true). Read only names, headlines, profile URLs (https://www.linkedin.com/in/<slug>), the degree badge (1st, 2nd, 3rd+) and the mutual. Never open a profile, messaging, invitation, follow or feed page; never press Connect, Message, Follow, Like or any button. A sign-in wall, checkpoint, captcha or "unusual activity" page: stop at once, read nothing more, and report it (skipped: "checkpoint" or "signed_out"). {{browser.none}}: skipped: "no_browser".
  4. Find. people.find { role_id, hints, linkedin: { pages_opened, skipped? } }. Hints:
    • From the JD and team page: { name, headline?, linkedin_url?, email?, source: "jd" | "company_site" }.
    • From your LinkedIn read: { name, headline, linkedin_url, source: "linkedin", degree: "1" | "2" | "cold", via?: { name, headline, linkedin_url, tie? } (2nd degree, the mutual), hiring_team?: true }.
    • team_side and brand (judgment, #132): set team_side: true for a person who is not on the hiring team but is on the team side: they work at the company or one of its product brands (a product sold under a different name: check the dossier's aliases / brands, companies/<slug>/dossier.md), in the role's org or an adjacent function on that product (product, design, engineering leadership). Example: the Head of Design at the company's product brand for a product role. Add brand: "<Brand>" when they work under a brand name that differs from the company name. Leave both out for anyone else (a peer in an unrelated org, someone at another company). Team side only ever adds, like hiring_team.
    • via.tie (judgment, #98): { same_company, same_industry, history }, how close the mutual is to this person: same_company when the mutual's headline puts them at the target's company, same_industry when it is the same industry, history when you saw they worked or talked together. Omit what you did not see (default false). Always pass linkedin.pages_opened (0 if you opened none) and, if you read nothing, skipped (no_browser, signed_out, checkpoint, capped, not_found, error). The engine keeps only /in/<slug> profile URLs, counts every person and mutual against the daily budget, drops hints beyond it (capped: true: stop for today, do not work around it) and records linkedin_mode: "agent" only when you really read LinkedIn. Add dry_run: true first if the user wants a preview.
  5. Classify. For each found contact: people.classify { contact_id, role_id }. The resolver answers; low confidence lands in Needs you with the pick shown. Tier is mechanics:

    Hiring team (hiring manager, hiring team, recruiter)Not hiring team
    1st degreewarm_hiring: ask for a conversation about the rolewarm_other: coffee chat, or intro to the hiring manager
    2nd degreebridge: note to the mutual + forwardable blurb (or a direct note, see below)same
    Coldcold_hiring: their problem, one proof, one askcold_other: skip unless the user asks
  6. Pick the best path. A 2nd-degree hiring-team or team-side (#132) contact with a mutual has two paths (#98), ranked by code in roles.get (contacts[].paths): direct (a note to the target, tier cold_hiring) or bridge (an intro ask to the mutual, tier bridge). Direct wins when the target's email is confirmed or likely and the mutual is a weak tie; bridge only for a strong mutual with an email; a bridge to a mutual with no email is blocked while a direct path exists. Draft the chosen path (the user may have switched it with people.set_path); the other open path may be drafted too when the user asks (both are kept). outreach.draft refuses a blocked bridge. Run the email step for the target first: their address decides whether direct is reachable. Then draft for the top contact per tier, in order warm_hiring, warm_other, bridge, cold_hiring. Usually one to four drafts per role. Skip cold_other unless asked. No usable contact (none found, or every one is cold_other): follow "No usable contact" below; the role must never stall in Connect.
  7. Email (judgment: what you saw; code: the guess and the check). For each contact you will draft to (bridge: the mutual; a 2nd-degree hiring target: the target and the mutual, so the paths rank on real addresses), find addresses, read-only:
    • Gmail (read-only here: {{gmail.search}}, {{gmail.thread}}, {{gmail.message}}): threads with the person (from: / to: their name) and signatures.
    • Inbox peers (#99): search the user's own mail for @<domain> and note real addresses at the company domain in the from / to / cc headers, each with the display name when the header shows one. Keep only the address (and that name, to infer the format); never copy a subject, body or anything else.
    • Public pages: the JD, the company team page, talks, GitHub, a personal site. Then call people.find_email { contact_id, role_id, hints: [{ email, name?, source: "gmail" | "linkedin" | "public" }], evidence: [{ kind: "inbox_peer" | "public_mention", address, source, url?, name? }], domain? }. hints = addresses that are this person's (or that you saw with whose they are); evidence = inbox peers (source: "gmail") and public mentions (source: the page, url). Pass domain when the company has none on file (dossier or website). The op keeps a known address, else learns the format (first.last, flast, first, firstl, f.last, last), ranks candidates and checks each one passively: syntax, the domain's MX (or A) record, disposable / role / free lists, and the evidence (peers in the candidate's format, an exact sighting). An exact match, or 2+ peers agreeing with none conflicting, is confirmed without asking; a domain with no mail records drops the candidate; anything else is stored as a guess and goes to Needs you, where the user checks it by hand. If the result is not confirmed, look for the exact candidate addresses (candidates[].email) in the user's mail ("<address>") and on public pages you can read, and call people.find_email again with what you found as evidence (it replaces the pending pick). Never guess the address yourself. Never contact mail servers or verification sites: no SMTP, no mailbox probe, no test email, no email-checker or verification website or API, no paid finder. The user does the manual check in Needs you.
  8. Write the draft (judgment). The draft must read like a real email from the user, in their voice. Gather the writer inputs, keeping the two kinds of facts apart:

    • CONTEXT (facts about them, usable for the hook): the contact's headline, the role's JD (roles/<role_id>.md, the JD section), the company dossier (companies/<slug>/dossier.md: snapshot, why, hooks, news). Never present CONTEXT as a fact about the user.
    • STORY (profile/story.md plus the Story component data): the only source of facts about the user.
    • VOICE (profile/voice.md plus the Voice component: style, rules, signoff, samples): tone only. Treat every writing sample as a style exemplar: match its tone, rhythm and phrasing; never reuse its facts, names or numbers. Use all of them, even when voice.md has text.

    The engine's own writer prompt (buildWriterPrompt in packages/engine/people/templates.ts) uses exactly these inputs and rules. Write:

    • Hook: opens with a greeting ("Hi Maya,"), then one or two lines about their world from CONTEXT (reframe what they care about). Max 320 chars. A number here must appear in STORY or CONTEXT.
    • Proof: two or three sentences, first person, from STORY only, tied to what the role needs. At most 3 sentences, max 560 chars. Never a number that is not in STORY.
    • Ask: one small, specific ask (the close). Max 240 chars; bridge max 560 (includes the forwardable blurb).
    • Sign-off: in the user's voice, max 80 chars. Default: Voice signoff, else "Thanks," plus the first name from Story (omit signoff to get the default).
    • Subject: short, plain. Max 80 chars.
    • No em dashes, no en dashes. No flattery ("huge fan", "I admire", "amazing team" are refused). No facts beyond STORY. Bridge drafts are addressed to the mutual, not the target. A direct note to a 2nd-degree target (hiring team or team side) is a cold_hiring note to them (no mention of the mutual needed).
  9. Save. outreach.draft { contact_id, role_id, tier, subject, hook, proof, ask, signoff? }. contact_id is the target; the engine files the draft under the person it is addressed to (a bridge intro under the mutual, with about_contact_id = the target), one draft per recipient. Agent-written drafts pass the same checkDraft as engine-written ones: it refuses rule breaks with the exact issue (em_dash, too_long, too_many_sentences, ungrounded_number, flattery, empty); fix and call again.
  10. To Gmail. For each saved draft, follow "Gmail draft step" below in this same run ({{gmail.create_draft}} is granted to every connect run). It creates a Gmail draft and stops. The user opens Gmail and sends. A draft whose recipient's address is a guess waiting in Needs you (outreach.get's to.decision_id is set) gets no Gmail draft now (#99): leave it saved in the vault; once the address is confirmed the engine queues the Gmail draft step for it with To filled. Never create a Gmail draft without To (#132): a recipient with no address at all (to.email and to.guess both null) gets no Gmail draft; the draft stays in the vault and the Outreach screen offers Copy note and Open profile for LinkedIn, where the user writes to them by hand (pp never messages on LinkedIn). outreach.to_gmail and runs.start refuse it; once an address is found and confirmed (people.find_email), the engine queues the Gmail step as usual.

Chrome tabs

At the end of a run that used Chrome (done or failed, #107): close every tab you opened or used with {{browser.close_tab}}, never a tab the user already had open, and report them on the finish: runs.finish { run_id, status, tabs: { opened: [<every tab id you used>], closed: [<ids you closed>], left_open: [] } }. The run log lists them. A connect run leaves no tab open.

No usable contact

When people.find returns nobody usable, Connect falls back automatically (#78). You find (judgment); people.fallback picks (code), in this order:

  1. Hiring team or recruiting address. Search the JD, the careers page, the dossier (companies/<slug>/dossier.md) and the company site for a team mailbox (recruiting@, talent@, jobs@, careers@, hiring@) or a named recruiter with a work address. Public pages only; never a personal address.
  2. The likely hiring leader. From the dossier: who this role most likely reports to (e.g. the CPO or VP Product), with headline and LinkedIn URL if the dossier has it.
  3. Call people.fallback { role_id, team_inbox: [{ email, name?, source: "jd" | "careers" | "dossier" | "company_site", url? }], leader?: { name, headline?, linkedin_url? }, domain?, email_hints?, reason? } with everything you found (or nothing). The engine also scans the JD and dossier for team mailboxes, keeps only addresses at the company domain, and then:
    • team_inbox: a contact "Hiring team" (or the named recruiter), email_source: public, linked cold_hiring. Draft to it.
    • cold_leader: the leader as a cold_hiring contact; people.find_email guesses the address (flagged as a guess; an unsure one waits in Needs you, so the Gmail draft has no To until confirmed). Draft to them.
    • skipped: nothing usable; the application moves from Connect to Fill. Stop: no draft. (apply.skip_connect { role_id, reason } does the same on its own; the UI's Skip calls it.)
  4. Fallback draft (judgment). The draft is a shortened version of the tailored cover letter (applications/<role_id>/cover_letter.md), in the user's voice: Hook = greeting ("Hi <Company> team," for a team inbox, else the first name) plus why this role; Proof = two or three first-person sentences from the cover letter; Ask = team inbox: pass it to the hiring manager; leader: a short call. The cover letter counts as a fact source next to STORY for these drafts only (it passed the tailor fact check); everything else in step 8 applies, and outreach.draft runs the same checkDraft. Then the Gmail draft step as usual.

Gmail draft step

The engine has no Gmail client. You create the draft with your own Gmail draft tool, then record it. This works from any MCP client that has a Gmail draft tool next to the pp server: {{gmail.source}} A full connect run does this step itself for each draft it saved. The web UI's Create Gmail draft starts a headless connect run for one outreach (runs.start { skill: "connect", outreach_id }) that does only this step (for a draft saved earlier, or one the user edited).

  1. Read the draft. outreach.get { outreach_id }: subject, hook, proof, ask, signoff and body. No subject: the role title plus " at " and the company (roles.get).
  2. Recipient. outreach.get returns to: the contact, or the mutual for bridge drafts (never the target). To = to.email. When outreach.gmail_draft_id is set (the draft is already in Gmail), create nothing (runs.start refuses the step too). When to.decision_id is set (a guessed address waiting in Needs you), create nothing: the engine queues this step once the address is confirmed (outreach.to_gmail and runs.start refuse it meanwhile). When to.email and to.guess are both null (no address at all), create nothing: finish ok with summary: { outreach_id, skipped: "no email" }; never a Gmail draft without To (#132: outreach.to_gmail and runs.start refuse it). When to.email is null otherwise (an unconfirmed guess not in Needs you), leave To empty: to.reason says why. Never use to.guess.
  3. Create a Gmail DRAFT. Body = the body field exactly as returned: greeting line, blank line, Hook, blank line, Proof, blank line, Ask, blank line, sign-off (plain text, built from the stored blocks, which the user may have edited). Create it once. Never send, reply or forward. No Gmail draft tool in this session: create nothing and finish with runs.finish { run_id, status: "error", {{missing.error}}{{gmail.create_draft}}" }{{connect.draft_retry}}
  4. Record it. outreach.to_gmail { outreach_id, gmail_draft_id, confidence }. Medium impact: with confidence the policy grid decides; without it, or unsure, it waits in Needs you. It re-checks the writing rules, stores the id, sets the outreach in_gmail and moves the application to Fill.
  5. Finish (headless run): runs.finish { run_id, status: "ok", summary: { outreach_id, gmail_draft_id, to, to_reason } } (to null and to_reason set when the draft has no To).

Gmail To step

A Gmail draft created with no To (before #99, or while its address was still a guess) is fixed once the address is confirmed: the engine queues runs.start { skill: "connect", outreach_id } for it and that run is this step only (summary_json.step: "gmail_to"). It is the only run granted the Gmail connector's get draft and update draft tools (not part of the full connect allowlist below); create, send, reply, forward, delete, trash and labels stay denied.

  1. outreach.get { outreach_id }: the draft id is outreach.gmail_draft_id, To = to.email.
  2. Read the Gmail draft and keep its subject and body exactly as they are (the user may have edited them in Gmail).
  3. Update that same draft with To = to.email. Never send.
  4. outreach.to_gmail { outreach_id, gmail_draft_id, confidence } with the same id: the engine stores the To.
  5. runs.finish { run_id, status: "ok", summary: { outreach_id, gmail_draft_id, to } }.

Tools

A headless run pre-approves exactly these (SKILL_GRANTS in packages/engine/runners/grants.ts; a test keeps this list and that one equal) and denies everything else. If one you need is denied or missing, do not work around it: runs.finish { run_id, status: "error", summary: { error: "tool denied: <tool>", denied_tools: [...] } }. {{connector.tools}}

  • {{tool:pp}}: the pp MCP tools (every Perfect Placement read and write is an op).
  • {{tool:gmail.search}}: search Gmail for threads with a contact or the company domain.
  • {{tool:gmail.thread}}: read a thread (addresses, signatures).
  • {{tool:gmail.message}}: read one message.
  • {{tool:gmail.create_draft}}: create the Gmail draft (Gmail draft step). Never send.
  • {{tool:browser.tabs}}: see the open Chrome tabs and reuse the session's tab (LinkedIn read, step 3).
  • {{tool:browser.new_tab}}: open a tab for the LinkedIn read, only if the session has no usable tab.
  • {{tool:browser.close_tab}}: close the tabs this run opened or used, at the end (never the user's tabs).
  • {{tool:browser.navigate}}: open an allowlisted LinkedIn people page.
  • {{tool:browser.text}}: read the people cards as text.
  • {{tool:browser.tree}}: read the page's structure when the text is not enough.
  • {{tool:browser.find}}: locate the people list or a "mutual connections" link (read only, never click).

{{connect.tools_note}}

Never

  • Never send an email. outreach.send is unimplemented on purpose.
  • Never create a Gmail draft without To for a recipient with no address at all (#132): it stays a vault draft.
  • Never connect, message, follow, like or InMail on LinkedIn, and never click a LinkedIn button. Read people listings only, within the caps; do not try another way.
  • Never store more than name, headline, URL, relationship and email (with its source and confidence).
  • Gmail is search, read and draft only. Never send, reply, forward, delete, trash or label.
  • Never contact a mail server or a verification site (SMTP, mailbox probes, test emails, email checkers). Evidence is passive: the user's own mail (addresses only) and public pages.
  • Never run people.find on a role the user has not picked (an inbox or archived role); starting Connect on an active or held role is the pick.
  • Never commit anything from the vault. Placeholders only in examples.

First live LinkedIn run (owner present): see RUNBOOK.md.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.