feat(partners): add twenty-partner-match skill (#21601)
## 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. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21601?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+26
-2
@@ -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/<LEAD>/) 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/<LEAD>/) 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 "<file>" -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-<lead>-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-<lead>-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-<lead>-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-<lead>-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
|
||||
|
||||
|
||||
+42
-7
@@ -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-<lead>-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-<lead>-partner-brief.md` | `YYYY-MM-DD-<lead>-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. |
|
||||
|
||||
+200
@@ -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=<your key>
|
||||
|
||||
# Only needed if the input is a Fireflies link/ID
|
||||
FIREFLIES_API_KEY=<your 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-<lead>-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-<partner-slug>-solo.txt`
|
||||
- `email-3-<partner-slug>-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.
|
||||
Reference in New Issue
Block a user