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.
---
.../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.