## Summary Two related threads for the internal `twenty-partners` app: 1. **Redesign `partnerQuote` → `partnerContent`.** The object was mis-modeled as a sales/pre-invoice doc (`amount`, opportunity link). In TFT it's actually a marketing-content catalog — customer quotes, case studies, partner quotes, logos — moving through a production lifecycle. This renames it in place and reshapes it to mirror TFT's `CustomerContent`. 2. **Import tooling improvements** to the TFT importer + multi-env workflow. ## Changes **Schema (`partnerContent`)** - Rename `partnerQuote` → `partnerContent` (object, view, nav, relation fields, identifiers). - Add `contentType` MULTI_SELECT `[CUSTOMER_QUOTE, CASE_STUDY, PARTNER_QUOTE, LOGO]` and `interview` LINKS. - Add `customerCompany` / `customerPerson` relations; keep `partner`; drop the `opportunity` link (TFT has none). - Drop `amount`; rename the FILES field `quoteFile` → `documents` (`attachments` is a reserved morph-relation name). **Importer (`import-from-tft.ts`)** - Import the full content catalog (all types), not just `PARTNER_QUOTE`. - Map TFT `partnerTimezone` → `region`, default `languagesSpoken=[ENGLISH]`, and set `deploymentExpertise=[SELF_HOST]` when scope includes `HOSTING_ENVIRONMENT`. - Filter to partner-relevant records only: opportunities linked to a partner (20 of 164), content linked to a partner (10 of 22). Drops general sales-pipeline / customer-only noise. - Dedupe companies by **normalized domain** (Twenty's unique key), not just name — fixes duplicate-entry crashes when the same company arrives under different names. - Progress logging throughout. **Tooling** - `purge-soft-deleted` script (soft-deleted rows block re-imports via unique constraints). - Multi-env script variants (`*:prod`) selected via `ENV_FILE`. ## Testing Verified on a local Twenty instance and on `partner.twenty.com`: - 122 partners, 20 partner-linked opportunities, 10 partner-linked content (all types), 229 domain-deduped companies. - Schema confirmed via metadata introspection; `yarn twenty typecheck` clean. ## Notes - Renaming an installed object isn't a pure in-place migration on a server that already had `partnerQuote` — the working path is `uninstall → deploy → install` (safe here: prod had no data). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
twenty-partners
A Twenty app that turns the CRM into the operating system for the Twenty partner program: intake partner-eligible deals, match them to vetted marketplace partners, and track the matching pipeline end-to-end.
Built on Twenty with twenty-sdk v2.5.
What's inside
- Custom object:
Partner— slug, status, availability, served geos, languages spoken, deployment expertise, Calendly link, last-match timestamp. Seesrc/objects/partner.object.ts. - Opportunity extensions —
matchStatus,designDocStatus,introSentAt,lastRelanceSentAt,tftId, plus apartnerrelation. - Logic functions
on-opportunity-auto-match— fires whenmatchStatusis set toAUTO_MATCH. Assigns the longest-idle available partner and flips status toMATCHED. If no partner is available, hands off toMANUAL_MATCHwith an audit Note explaining why.list-available-partners— surfaces matchable partners for a given opportunity.post-install— first-run setup.
- Roles (
src/roles/)- Twenty Partner Ops — internal team role, full CRUD on Partner/Company/Person/Opportunity.
- Partner — placeholder external-partner role. Do not assign until Twenty ships row-level permissions — it currently grants access to every record.
- Views (
src/views/)Waiting for match— opportunities awaiting human action (matchStatusisTO_BE_MATCHEDorMANUAL_MATCH).Matches overview— full matching funnel grouped bymatchStatus(configure Kanban grouping manually in the UI).Opportunities— replacement of the native opportunities view with the partner columns.PartnersandAll matched deals— partner-side index and deal log.
- Sidebar nav — surfaced in workflow order:
Waiting for match,All partner deals,Matches overview,Partners,Opportunities. - Seed scripts (
src/scripts/) — populate a fresh workspace with realistic demo data.
Match status pipeline
matchStatus is a non-nullable SELECT field with a default of TO_BE_MATCHED. The 10 states follow the deal lifecycle:
| Status | Meaning |
|---|---|
TO_BE_MATCHED |
Default — deal entered, awaiting assignment |
MANUAL_MATCH |
Needs a human to pick a partner |
AUTO_MATCH |
Triggers automatic partner assignment |
MATCHED |
Partner assigned |
INTRODUCED_TO_A_PARTNER |
Customer intro sent |
WORKING_WITH_A_PARTNER |
Engagement underway |
IMPLEMENTING |
Active implementation |
WON |
Deal closed won |
RECONNECT_LATER |
Paused — reconnect in future |
LOST |
Deal closed lost |
Getting started
Requires a local Twenty server at http://localhost:2020 and Node ^24.5.
yarn install
yarn twenty dev
Default dev credentials: tim@apple.dev / tim@apple.dev.
Run yarn twenty help for the full CLI reference.
Common commands
| Command | What it does |
|---|---|
yarn twenty dev |
Start the dev server and sync the app on file changes |
yarn twenty server status |
Check the local Twenty server |
yarn lint / yarn lint:fix |
Run oxlint |
yarn test |
Run integration tests (vitest.config.ts) |
Seeding demo data
Two idempotent seed scripts. Both run via the vitest.seed.config.ts config that skips
the global app uninstall/reinstall.
# 1. Marketplace partners (run first — pipeline seed wires opportunities to these by slug)
yarn vitest run --config vitest.seed.config.ts src/scripts/seed-marketplace-partners.ts
# 2. Pipeline demo: 3 companies, 3 people, 15 opportunities spread across matchStatus values
yarn vitest run --config vitest.seed.config.ts src/scripts/seed-pipeline-demo.ts
Both scripts skip records that already exist (by slug, name, or firstName+lastName),
so they are safe to re-run.
Known limitations
Current SDK gaps blocking further polish:
- Custom Partner record page layout (RECORD_TABLE has no relation scoping).
- Native Opportunities view column-order override.
- Kanban view configuration from app code (
ViewType.KANBANis currently ignored). - App and field descriptions.