From 09694b2f3bfb8cdbf4882a669db86f5e48b609df Mon Sep 17 00:00:00 2001 From: Rashad Karanouh <11599358+rashad@users.noreply.github.com> Date: Mon, 15 Jun 2026 18:16:23 +0400 Subject: [PATCH] feat(partners): add twenty-partner-match skill (#21601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds `twenty-partner-match` — a Claude Code skill that closes the partner pipeline loop: query validated partners from the API, score and explain candidates against a lead's match criteria, pause for human validation, then generate and open all intro emails in Gmail (1 client notification + 2N partner emails for N confirmed partners) - Updates `twenty-partner-design-doc` to distinguish **default zero-inference mode** (strict, 1-page brief) from `--full` inference mode, and adds **Step 8** which always produces `partner-match-criteria.md` alongside the brief - Updates `design-doc-doctrine.md` with the full zero-inference / full-mode doctrine so the Claude Code skill and a future in-product `defineSkill` stay in sync ## Skill chain ``` /twenty-lead-intro-call-summary → /twenty-partner-design-doc → /twenty-partner-match ``` `/twenty-partner-match` chains back into the earlier skills if `partner-match-criteria.md` is missing, and applies critical review if the brief is thin before querying the API. ## Credentials The skill reads `~/.twenty/credentials.env` for API keys (never committed). See `SKILL.md` for setup instructions. Review in cubic --- .../skills/twenty-partner-design-doc/SKILL.md | 28 ++- .../design-doc-doctrine.md | 49 ++++- .../src/skills/twenty-partner-match/SKILL.md | 200 ++++++++++++++++++ 3 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-match/SKILL.md diff --git a/packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-design-doc/SKILL.md b/packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-design-doc/SKILL.md index ea409ff78b..0eaa6815a2 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-design-doc/SKILL.md +++ b/packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-design-doc/SKILL.md @@ -1,6 +1,6 @@ --- name: twenty-partner-design-doc -description: Use when turning a qualified Twenty lead — a call-summary brief plus any client braindump/docs — into an implementation design doc a partner can scope and quote from. Trigger when pointed at a lead folder (e.g. partners-experience//) and asked to "draft a design doc", "translate this into Twenty terms", "scope this for a partner", or "prep the partner handoff" for a discovery-qualified prospect. Chains after twenty-lead-intro-call-summary. +description: Use when turning a qualified Twenty lead — a call-summary brief plus any client braindump/docs — into a partner brief a partner can scope and quote from. Trigger when pointed at a lead folder (e.g. partners-experience//) and asked to "draft a design doc", "create a partner brief", "translate this into Twenty terms", "scope this for a partner", or "prep the partner handoff". Default output is zero-inference (only what the client said; empty sections get a placeholder). Pass --full for inference mode. Chains after twenty-lead-intro-call-summary. trigger: /twenty-partner-design-doc --- @@ -16,6 +16,23 @@ Turn a qualified lead's materials into a **design doc**: a translation of the cu - If there is a raw transcript but no brief, run **twenty-lead-intro-call-summary** first — this skill chains after it. - Read everything. Convert `.docx` with `textutil -convert txt "" -output /tmp/out.txt` (macOS) or an equivalent extractor. +## Default: zero-inference partner brief + +The default output is a **zero-inference partner brief**: only what the client explicitly said, nothing invented. + +- `🔮 inf.` **never appears.** If you feel the urge to use it, that line should not exist. +- Required sections with no grounded content get: `> ⬜ Not discussed on call — needs input before this section can be filled.` +- Data-model table: **no Source column**; only rows for objects the client named; only fields the client named; `—` for empty cells. +- Target length: 1 page. Each section 1–5 lines max. +- Step 2: extract stated facts only. Step 4: verify only sections with grounded content. +- Step 7 self-check: `🔮 inf.` anywhere = failure (remove the whole inference); Source column = failure; doc >2 pages = warning. +- Save as `YYYY-MM-DD--partner-brief.md`. +- See "Default behavior" in `design-doc-doctrine.md` for the full rules. + +## Mode: `--full` (inference-enabled design doc) + +Pass `--full` when discovery is complete and a richer inferred model is useful. Enables `🔮 inf.` tags, adds the Source column to the data model, fills gaps with inferences, and allows longer output. Save as `YYYY-MM-DD--design-doc.md`. See "Full mode" in `design-doc-doctrine.md`. + ## Steps 1. **Gather** — read all source materials in full. Coverage is the point. @@ -24,7 +41,14 @@ Turn a qualified lead's materials into a **design doc**: a translation of the cu 4. **Verify load-bearing claims live** — use **WebFetch** against the Twenty doc map in the doctrine's Verification section before asserting any capability. Build the §11 appendix as you go. 5. **Reconcile discrepancies** — sources that disagree (call vs braindump; a name differing across/within sources) get flagged both ways, never silently resolved. 6. **Resolve ❓ with the operator** — after a full v1 draft, use **AskUserQuestion** to ask the Twenty team member the unknowns a Twenty insider can answer; leave customer-facing unknowns as ❓. **If running autonomously** (no operator — a subagent/batch run), skip the questions and leave every unknown as ❓ in the body and §11. -7. **Self-check, then save** — scan the output for: an em dash; a bare `~`; first-person voice outside customer quotes; local file paths; a header that isn't the four-field table; **any flag that isn't one of the four canonical emoji-and-text pairs** (`🔮 inf.`, `**❓ open**`, `**⚠️ heavy**`, `**🛑 blocker**`) — a stray 🟥, 🚩, 🚨, ✅, or a naked emoji without its text label is wrong; **a Data-model table missing the `Source` column** (`client` / `inf.`); **a section that just says "X was not named" / "no automations named" / a "left out on purpose" list** — cut it, unknowns go in Open questions; **any bare `§N` reference** instead of a functional anchor link `[§N](#n-section-slug)`; **renumbering gaps** (e.g. cut a section but kept the old numbers around it); **a section that is mostly paragraphs where bullets or a table would do** — exception: Open questions stays a numbered list; **build / runtime / SDK mechanics that don't change the quote** (Docker version, OAuth flavour, auto-system relations, env-var names, CI/CD workflow detail); a point repeated across sections instead of a `[§N](...)` cross-reference; a leftover glossary / domain-language section; any capability claim stated as fact without a References source. Fix, then save to the lead folder as `YYYY-MM-DD--design-doc.md`. +7. **Self-check, then save** — scan the output for: an em dash; a bare `~`; first-person voice outside customer quotes; local file paths; a header that isn't the four-field table; **any flag that isn't one of the three canonical emoji-and-text pairs** (`**❓ open**`, `**⚠️ heavy**`, `**🛑 blocker**`) — a stray 🟥, 🚩, 🚨, ✅, 🔮, or a naked emoji without its text label is wrong (in default mode `🔮 inf.` is a failure, not just a format issue; remove the whole inference); **a Data-model table that has a `Source` column** (default mode — remove it; only `--full` mode uses it); **a section that just says "X was not named" / "no automations named" / a "left out on purpose" list** — cut it, unknowns go in Open questions; **any bare `§N` reference** instead of a functional anchor link `[§N](#n-section-slug)`; **renumbering gaps** (e.g. cut a section but kept the old numbers around it); **a section that is mostly paragraphs where bullets or a table would do** — exception: Open questions stays a numbered list; **build / runtime / SDK mechanics that don't change the quote** (Docker version, OAuth flavour, auto-system relations, env-var names, CI/CD workflow detail); a point repeated across sections instead of a `[§N](...)` cross-reference; a leftover glossary / domain-language section; any capability claim stated as fact without a References source. Fix, then save to the lead folder as `YYYY-MM-DD--partner-brief.md` (or `design-doc.md` in `--full` mode). +8. **Write partner-match-criteria.md** — always, as a third artifact alongside the brief. Save as `partner-match-criteria.md` in the same lead folder. Structure: + - **Hard requirements** — table: Criterion | Why. Include: language/region, deployment type, data model complexity signal, migration capability, engagement model (fixed vs retainer). + - **Strong preference** — bullets: domain familiarity, migration experience, willingness to scope for free, ability to work with non-technical end-users. + - **Nice to have** — bullets: industry-specific workflow experience, partner size fit. + - **Red flags** — bullets: anything that would disqualify a partner silently (language, hosting model, engagement type, technical depth). + - **What to send the partner** — numbered steps: which files to share, what to ask the partner to confirm, when to make the intro. + - **Matching notes** — 3–5 bullets for the partnerships team: technical nuances, prospect's sophistication level, migration unknowns, who the decision-makers are. Draw from the Open questions and Implementation complexity sections of the brief. ## Worked example diff --git a/packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-design-doc/design-doc-doctrine.md b/packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-design-doc/design-doc-doctrine.md index c85afc3024..56cac94c5f 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-design-doc/design-doc-doctrine.md +++ b/packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-design-doc/design-doc-doctrine.md @@ -29,19 +29,18 @@ After the table, one **"What this is"** callout framing the doc as a partner-sco **Flag legend (emoji + short text label, used together so they're scannable and unambiguous):** -- `🔮 inf.` modelling inference (yours; the partner should confirm with the client). Used inline, often, unbolded. - **❓ open** open question to resolve before quoting. - **⚠️ heavy** product-constrained or needs special design / has a real cost. - **🛑 blocker** dealbreaker-grade. Doesn't quote without resolution. -Use these four pairings only. Don't invent new flags, swap the emoji (🟥 / 🚩 / 🚨 / ✅), or drop the text label and use the emoji alone. +Use these three pairings only by default. Don't invent new flags, swap the emoji (🟥 / 🚩 / 🚨 / ✅), or drop the text label and use the emoji alone. In `--full` mode a fourth flag is added: `🔮 inf.` — see Full mode section. ### Required sections (always present) Number sequentially in the final doc, with no gaps: - **Context**: the 30-second read: who they are, what they want, deployment requirement, scale, language/region. -- **Data model in Twenty terms**: the core. Present objects as a **table, one row per object**: `Object | Std/Custom | Source | Represents | Key fields | Core relations`. The **Source** column is `client` (object/concept grounded in client speech) or `inf.` (you coined it). Inline, tag inferred field names and relations `🔮 inf.`. Spell out SELECT option sets. **Model the relationships, not just the fields**: who introduced/sourced a record (e.g. an ambassador to Opportunity `sourcedBy` link), parent/child, ownership carry as much scoping signal as the attributes. State product constraints **only when they change the build or quote**, and inline (no formula fields → reporting ratios need a logic-function-maintained stored field; custom objects auto-get attachments/notes/tasks/timeline → relationship tracking is free). Where a customer term collides with a Twenty term (their "partner" = a donor), note the mapping inline as a small "term collisions" bullet list above the table; do **not** add a glossary section for it. +- **Data model in Twenty terms**: the core. Present objects as a **table, one row per object**: `Object | Std/Custom | Represents | Key fields | Core relations`. Include only objects the client explicitly named; include only fields the client explicitly named. If a cell has no grounded content, write `—`. Spell out SELECT option sets. **Model the relationships, not just the fields**: who introduced/sourced a record (e.g. an ambassador to Opportunity `sourcedBy` link), parent/child, ownership carry as much scoping signal as the attributes. State product constraints **only when they change the build or quote**, and inline (no formula fields → reporting ratios need a logic-function-maintained stored field; custom objects auto-get attachments/notes/tasks/timeline → relationship tracking is free). Where a customer term collides with a Twenty term (their "partner" = a donor), note the mapping inline as a small "term collisions" bullet list above the table; do **not** add a glossary section for it. In `--full` mode, add a `Source` column (`client` / `inf.`) and tag inferred field names and relations `🔮 inf.`. - **Roles, permissions & RLS**: map named roles to Twenty's object / field / row-level model; answer "do we need RLS?" against verified, plan-gated capability. - **Hosting & compliance**: cloud vs self-host, data-residency requirement (verify), GDPR. Flag contradictions. - **Suggested phasing**: "(the partner's call, not Twenty's)" layers, labelled a suggestion. @@ -61,12 +60,48 @@ Number whatever you include sequentially. A doc with Context, Data model, Integr Scale each section to its content. **Coverage of surface area matters more than depth per item.** +## Default behavior: zero inference + +The default output is a **zero-inference partner brief**: sharp, small, and strictly grounded in what the client said. + +- **Nothing is inferred.** If the client didn't say it, it doesn't appear in the doc. +- **Required sections with no grounded content** get exactly one placeholder line: `> ⬜ Not discussed on call — needs input before this section can be filled.` +- **Conditional sections** (Automations, Integrations, Reporting, Views) with nothing grounded are **omitted entirely** — same rule as always; the placeholder applies only to required sections. +- **Data-model table cells** with no grounded content get `—`, not an invented value. +- **`🔮 inf.` never appears.** If you find yourself wanting to use it, that line should not exist in the doc. +- **Target length:** 1 page. Each section is 1–5 lines max. +- **Verification** is run only for sections that have grounded content — no need to verify empty sections. +- **Save as** `YYYY-MM-DD--partner-brief.md`. + +### Self-check (zero-inference additions) + +After the standard self-check: +- `🔮 inf.` anywhere is a hard failure — remove the entire inference, not just the tag. +- Source column in the data-model table is a failure — remove it. +- Doc longer than 2 pages is a warning — cut until only grounded content remains. + +## Full mode (`--full`) + +Pass `--full` to produce a **full inference design doc** instead of the default zero-inference brief. Use when: discovery is complete, the lead is well-documented, and inferences are needed to give the partner a richer starting model. + +| Dimension | Default (zero-inference) | `--full` | +|---|---|---| +| Inferences | Never | Allowed; tagged `🔮 inf.` | +| Empty section | Placeholder line | Fill with inferences + `🔮 inf.` tags | +| Data-model Source column | Omitted | `client` / `inf.` | +| Inferred field names / relations | Not included | Tagged `🔮 inf.` | +| Length | 1 page target | As long as content requires | +| Verification | Grounded sections only | All load-bearing claims | +| Filename | `YYYY-MM-DD--partner-brief.md` | `YYYY-MM-DD--design-doc.md` | + +In `--full` mode the fourth canonical flag is active: `🔮 inf.` (modelling inference; yours; the partner should confirm with the client). Used inline, often, unbolded. + ## Rules (and why each matters) - **Be concise: maximum signal per word.** Say a lot in few words. Cut throat-clearing, scene-setting, hedges, and feature-tour prose; prefer a table or a tight clause to a paragraph. Length is not coverage: a short doc that names every requirement beats a long one that pads each. A hesitant buyer reads a focused doc; a bloated one reads as cost. - **Bullets and tables over paragraphs, with one exception.** Default to bullets; reach for a table whenever rows share structure (objects, views, plans, paths). Use prose only for a nuance no bullet or table cell can carry. The Open questions section is the deliberate exception: always a numbered list, so the partner can read item 1, 2, 3 aloud. - **Business decisions over technical mechanics.** Scope is what the partner *builds* and what the client *receives*: data sensitivity, who-sees-what, plan choice, hosting choice, integration surface, cost drivers. Cut SDK / runtime / build-tool internals that don't change the quote (Docker version, OAuth flavour, auto-system relations, env-var names, version-control workflow). The partner reads References for the docs that cover those. -- **Fact vs inference is the primary visibility split.** Plain prose = stated by the client. `🔮 inf.` tag inline = your modelling guess, every time it appears. The Data-model table carries a **Source** column (`client` / `inf.`). A partner skimming the doc must be able to see at a glance which lines they need to confirm with the client. +- **Grounded content only (default).** Every line in the doc must trace back to something the client said. If it wasn't stated, it doesn't appear — use the placeholder or `—` for empty cells. In `--full` mode, inferences are allowed and tagged `🔮 inf.`; the Data-model table gains a `Source` column (`client` / `inf.`) so the partner can see at a glance which lines to confirm with the client. - **Section cross-references are functional anchor links.** Every `§N` reference must be a markdown anchor link: `[§N](#n-section-slug)`. The slug follows GitHub Flavored Markdown auto-anchoring (lowercase; spaces → hyphens; punctuation dropped; `&` removed leaving a double hyphen). A bare `§N` is unreadable to a partner skimming the doc, who just sees numbers with no way to jump. - **No "left out" / "not named" placeholders.** The doc speaks only about content grounded in the source. A bullet that says *"sessions/programmes/schools left out on purpose"* or a section that says *"no reporting was named"* is filler: cut it. If you want to flag the absence, write it as a question in Open questions ("Are sessions/programmes first-class objects?") that gates a specific decision; otherwise, silence. - **Never repeat yourself.** State each fact, constraint, or claim once, in its home section; elsewhere link to it (functional `[§N](...)`) rather than restate. Open questions and References are deliberate roll-ups: there, give the pointer and the decision the item gates, not a re-explanation of the body. Repetition is the main source of bloat, and two copies of a claim drift out of sync. @@ -87,7 +122,7 @@ Scale each section to its content. **Coverage of surface area matters more than - One line per paragraph: **no mid-sentence hard wraps** (they render as broken lines). - **Never use em dashes (the long dash).** Restructure the sentence, or use a colon, comma, parentheses, or a period instead. - **Never a bare `~`** for "approximately": GitHub markdown pairs `~...~` into strikethrough. Write "around" / "about". -- **Flags are the four emoji + text pairs only** (`🔮 inf.`, `**❓ open**`, `**⚠️ heavy**`, `**🛑 blocker**`). Don't swap the emoji or drop the text label. +- **Flags in default mode: three emoji + text pairs** (`**❓ open**`, `**⚠️ heavy**`, `**🛑 blocker**`). In `--full` mode a fourth is added: `🔮 inf.`. Don't swap the emoji or drop the text label. - **Section cross-references are functional anchor links** (`[§N](#n-section-slug)`), never bare `§N`. - Mark unverified capability claims `**❓ open**`, never as fact. @@ -136,7 +171,7 @@ If a `.md` 404s, drop the suffix or re-derive from the docs index: the map can g |---|---| | "Twenty isn't a BI tool" / "can't do row-level" | Stale training. Twenty has Dashboards; row-level is on the Organization plan. **Verify live.** | | Checked only the app-SDK doc for a product capability | Row-level lives in the product/pricing layer. **Verify the right layer.** | -| Added fields not in the source | Scope growth, wrong quote. Ground every field; tag inferences `🔮 inf.`. | +| Added fields not in the source | Scope growth, wrong quote. Only include what the client explicitly named; leave the cell `—` otherwise. | | Made a human actor (e.g. ambassador) its own object by default | A human is a Person + role flag first; an object only if it needs its own pipeline/reporting. | | Flagged an automation as "Workflow" | Prescribes the build, penalizes app-builders. Present both. | | Flagged a limit with no fix | Dead-end flag. Pair every problem with a path. | @@ -151,7 +186,7 @@ If a `.md` 404s, drop the suffix or re-derive from the docs index: the map can g | Used an emoji other than the four canonical (🟥 / 🚩 / 🚨 / ✅), or used the emoji without the text label | Stick to `🔮 inf.`, `**❓ open**`, `**⚠️ heavy**`, `**🛑 blocker**`. The text label disambiguates. | | Section is mostly paragraphs | Default to bullets and tables; prose only when a nuance can't fit a list. Exception: Open questions stays a numbered list. | | Included build / runtime / SDK mechanics that don't move the quote | Wrong altitude. Business decisions, scope consequences, and References only; mechanics belong in the later technical phase. | -| Data-model table missing a Source column | Partner can't see at a glance which rows the client confirmed vs which are your inferences. Add `client` / `inf.`. | +| Data-model table has a Source column (default mode) | In the default zero-inference mode, the Source column is unnecessary noise — every row is `client`. Omit it; only add it in `--full` mode. | | Cross-references are bare `§N` instead of functional links | The partner sees disconnected numbers and can't navigate. Use `[§N](#n-section-slug)` everywhere. | | Included a section that just says "X was not named" / "no automations named" / a "left out on purpose" list | Cut the whole section / bullet. Unknowns go in Open questions; the doc speaks only about grounded content. | | Renumbered with gaps (e.g. cut Reporting but kept §5-§11 numbering as §5, §6, §8) | Renumber sequentially, no gaps. A partner doesn't know which sections were omitted. | diff --git a/packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-match/SKILL.md b/packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-match/SKILL.md new file mode 100644 index 0000000000..89beecdaf0 --- /dev/null +++ b/packages/twenty-apps/internal/twenty-partners/src/skills/twenty-partner-match/SKILL.md @@ -0,0 +1,200 @@ +--- +name: twenty-partner-match +description: Match a qualified lead to Twenty partners and generate all intro emails in one run. Use when a lead folder exists and you need to find the right implementation partner, either after running twenty-partner-design-doc or standalone when partner-match-criteria.md is already present. Chains back into twenty-lead-intro-call-summary and twenty-partner-design-doc if the criteria file is missing. +trigger: /twenty-partner-match +--- + +# twenty-partner-match + +Match a qualified lead to Twenty partners and generate all intro emails in one run. + +Chains after `twenty-lead-intro-call-summary` → `twenty-partner-design-doc`. Can be run standalone if `partner-match-criteria.md` already exists in the lead folder. + +--- + +## Credentials + +Before running, `~/.twenty/credentials.env` must exist with: + +```env +TWENTY_PARTNERS_API_URL=https://partners.twenty.com +TWENTY_PARTNERS_API_KEY= + +# Only needed if the input is a Fireflies link/ID +FIREFLIES_API_KEY= +``` + +The partners API key lives in `packages/twenty-apps/internal/twenty-partners/.env.prod` (gitignored) — copy it to `~/.twenty/credentials.env` on first setup. The skill reads `credentials.env` at startup and stops cleanly if a required key is missing. + +--- + +## Phase 0 — Prerequisites + +### 0a. Credentials check + +Read `~/.twenty/credentials.env`. Verify `TWENTY_PARTNERS_API_URL` and `TWENTY_PARTNERS_API_KEY` are present. If not, stop and tell the user exactly which key is missing and where to add it. + +### 0b. Brief check + +Look for `partner-match-criteria.md` in the lead folder. + +**If missing:** Ask the user what is available — a text transcript, a Fireflies meeting ID/URL, or an existing call summary or brief. Then chain: +- Fireflies input → fetch transcript via Fireflies API (requires `FIREFLIES_API_KEY`) → `/twenty-lead-intro-call-summary` → `/twenty-partner-design-doc` +- Text transcript → `/twenty-lead-intro-call-summary` → `/twenty-partner-design-doc` +- Existing summary → `/twenty-partner-design-doc` + +`partner-match-criteria.md` is produced by `/twenty-partner-design-doc` as Step 8. + +**If present — be critical before proceeding.** Read the file and assess quality: + +- Are there at least 2 hard requirements with justification? +- Is the language requirement explicit? +- Are the required skills specific enough to differentiate partners (e.g. "self-hosted Docker" beats "technical")? +- Are there red flags listed? + +If the brief is thin on any of these axes, say so and ask targeted follow-up questions before querying the API. A match built on a vague brief is noise. Only proceed once the criteria are solid enough to produce a meaningful ranking. + +--- + +## Phase 1 — Matching + +### 1a. Fetch candidates + +Query the partners API: + +```graphql +query ListPartners($after: String) { + partners( + filter: { + validationStage: { eq: VALIDATED } + availability: { eq: AVAILABLE } + } + after: $after + ) { + pageInfo { + hasNextPage + endCursor + } + edges { + node { + id name slug languagesSpoken skills deploymentExpertise + partnerScope partnerTier country region city introduction + persons { edges { node { name { firstName lastName } emails { primaryEmail } } } } + company { id name } + } + } + } +} +``` + +Paginate until `pageInfo.hasNextPage` is false, passing `pageInfo.endCursor` as `$after` each iteration. Endpoint: `$TWENTY_PARTNERS_API_URL/graphql`. + +### 1b. Evaluate and rank + +Evaluate every candidate against the criteria in `partner-match-criteria.md`. Apply hard requirements as eliminators first (a partner missing a hard requirement does not appear in results, even with a note). Then rank remaining candidates by fit across: + +- **Language** (primary) — exact match on `languagesSpoken` +- **Skills and expertise** (primary) — semantic match: "ERP experience" is relevant to complex billing, "self-hosted Docker" maps to `SELF_HOST` deploymentExpertise, etc. +- **Country / region** (secondary) — proximity helps but is not a blocker unless the criteria say otherwise + +### 1c. Present results + +Show **at minimum 2 candidates**, more if others score well. For each: + +``` +## [Partner name] — [fort / moyen / faible] + +**Pourquoi ça matche** +- point 1 +- point 2 +- point 3 + +**Ce qui manque ou est risqué** +- point + +**Contacts** : [name, email if available] +``` + +Be honest. If only one candidate is a strong match, say so and explain why the others are weaker — don't artificially inflate scores to fill a quota. + +### 1d. Pause for validation + +Ask the user which partners to introduce. They can pick one, several, or none (and ask to search differently). Wait for explicit confirmation before generating emails. + +--- + +## Phase 2 — Emails + +For each confirmed partner, generate three email types. For N confirmed partners, the total is `1 + 2N` emails. + +### Email rules (always applied) + +- No em dashes (`—`) — use `:` or `,` instead +- Tutoiement by default +- Sign: `Cheers,\nRashad\nPartnerships @twenty` +- Subject prefix: `[Twenty]` +- No self-introduction for partner emails (they know who Rashad is) +- Open in Gmail via `https://mail.google.com/mail/?view=cm&fs=1&to=...&su=...&body=...` using `open -a "Google Chrome"` + +### Email 1 — Client notification (one per run) + +- **To**: client email +- **Subject**: `[Twenty] Ton projet CRM — intro(s) à venir` +- **Content**: who Rashad is + role at Twenty, brief project recap (2 sentences), announce incoming introductions by agency name(s) + +### Email 2 per partner — Solo partner outreach + +- **To**: partner contact email (from `persons` → `emails.primaryEmail`; leave blank if none found) +- **Subject**: `[Twenty] Opportunité partenaire : [Client], [one-line project description]` +- **Content**: why this opportunity matches their profile, project complexity (custom objects, deployment, migration, seats), invite them to respond or get on a call, brief to attach + +### Email 3 per partner — Three-way intro + +- **To**: partner contact email +- **CC**: client email +- **Subject**: `[Twenty] [Client] x [Partner] : Projet CRM` +- **Content**: introduce client to partner (one sentence each), project in two sentences, brief to attach, hand off + +### Opening in Gmail + +Open all emails sequentially with a 1.5s delay between each. Use Python: + +```python +import subprocess, urllib.parse, time, sys +params = {"view": "cm", "fs": "1", "to": to, "su": subject, "body": body} +if cc: params["cc"] = cc +url = "https://mail.google.com/mail/?" + urllib.parse.urlencode(params) +if sys.platform == "darwin": + subprocess.run(["open", "-a", "Google Chrome", url]) +else: + # Linux / Windows: fall back to the system default browser + import webbrowser + webbrowser.open(url) +time.sleep(1.5) +``` + +Remind the user to attach `YYYY-MM-DD--partner-brief.md` to all partner emails before sending. + +--- + +## Output files + +Save email content as text files in the lead folder for reference: + +- `email-1-client.txt` +- `email-2--solo.txt` +- `email-3--intro.txt` + +Use the partner's `slug` field for filenames. + +--- + +## Chain context + +This skill is the third step in the Twenty partner pipeline: + +``` +/twenty-lead-intro-call-summary → /twenty-partner-design-doc → /twenty-partner-match +``` + +The `partner-match-criteria.md` produced by `/twenty-partner-design-doc` (Step 8) is this skill's primary input. Keep the criteria file updated as you learn more about the lead — it feeds both this skill and the eventual in-product matching logic.