## Summary (twenty-partners app, v0.5.4)
- **Profile picture upload (additive)**: `profilePicture` stays a URL
(LINKS) — existing partners keep their picture — and a new
`profilePictureFile` (FILES) field is added for uploads. The read logic
functions (`list-available-partners`, `get-partner-by-slug`) select both
and **prefer the uploaded file, falling back to the legacy URL**,
returning the existing `{ primaryLinkUrl }` shape so the public
directory and the website are unchanged.
- **Region** auto-derived from the partner's country on application
creation (static lookup).
- **Deployment expertise** derived: defaults to `CLOUD`, adds
`SELF_HOST` when the partner covers Hosting & Infrastructure.
- **Partner.website** now set from the submitted domain.
- Removed 5 unused `partnerScope` categories (0 production usage); seed
remapped.
- Removed one-off data scripts (`import-from-tft`,
`migrate-partner-scope`, `partner-scope-map`).
Rebased on `main` (includes #21615 company-reuse).
## Why additive, not a field-type change
Twenty treats a field's `type` as **immutable**: an app upgrade silently
ignores a LINKS→FILES change (`fieldMetadata.type` is `toCompare: false`
in the server's flat-entity config). An in-place flip would leave the
column LINKS on prod while the display queries asked for a FILES `url`,
**breaking the partner directory**. The additive `profilePictureFile`
upgrades cleanly with no data loss; existing URLs keep working via the
legacy field + fallback. Removing the 5 unused enum options is also a
clean upgrade (0 records use them).
## Deploy notes
- Version `0.5.4`. Fully additive schema change → installs in place, no
data migration required.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21709?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. -->
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.