## Summary Restructures the Twenty Partners app into an **Application-driven matching workspace**: leads post briefs (Opportunities), partners browse and **self-apply**, admins review applications and assign a winner. The candidacy funnel lives on `Application.state`; the deal lifecycle lives on the stock Opportunity `stage`. Version **1.0.0** — **breaking**: removes the legacy `matchStatus` field and the auto-match flow. Prod upgrade path is **uninstall → deploy → install** (not an in-place upgrade). ## How "Apply" works — no workflow, no special permission Partners apply by **creating an Application directly** from a listed brief (a normal record write, governed by the Application object permission). The `on-application-created` logic function (shipped in the manifest) then resolves the partner from `createdBy`, sets `state = APPLIED`, stamps `partner`/`partnerUser`/`lastActivityAt`, and dedupes by (opportunity, partner). - **No `WORKFLOWS` permission flag.** An earlier iteration used a manual "Apply" workflow, but running a manual workflow requires the `WORKFLOWS` flag, which **cannot be granted on an app-owned role** (the manifest sync drops `role → permissionFlag` links, and the metadata API rejects out-of-band grants on app roles). Self-apply via record-create sidesteps this entirely and is prod-viable as-is. - `Application.state` **defaults to `APPLIED`** so a partner never sees a misleading "Invited" flicker while the async handler runs. Admin invites set `INVITED` explicitly. ## ⚠️ Manual setup after install (per workspace) 1. **`yarn rls:configure`** — applies the partner row-level predicates and verifies field-locks (predicates can't ship in the manifest). Required for partner scoping. 2. **"Mark as Winner" workflow** — one manual-trigger workflow on **Application** → *Update Record* that sets `Opportunity.partner`, which drives the WON/BACKUP cascade. Admins run it (admins bypass the flag via `canUpdateAllSettings`); equivalent to editing the Opportunity's `partner` field directly. Steps in `src/workflows/README.md` (the **Apply** section there is superseded by self-apply). ## What's included - **Data model:** new `BACKUP` Application state; symmetric cascade owned by `on-opportunity-partner-won` — assign → winner `WON`, other applicants `BACKUP`; unassign → all reopen to `APPLIED`. `Opportunity.partner` is the single source of truth. - **Removed:** `matchStatus` field + `on-opportunity-auto-match` (dead). Deal lifecycle now on the stock `stage`. - **Partner row-level security (B7):** RLS predicates scope partners to their own `Partner`/`Person`/`Company`/`Application` rows; `Opportunity` is `(partnerUser IS me) OR (isListed = true)` so listed briefs are visible to all partners; `Application` is `(partnerUser IS me) OR (lastActivityAt IS EMPTY)` — the IS-EMPTY branch lets a partner's own insert pass (partnerUser is stamped just after insert). Field-locks make Opportunity `stage`/`amount` and most Application fields read-only for partners (pitch stays editable). Applied via `yarn rls:configure`. - *Trade-off:* an unstamped application (lastActivityAt null) is briefly readable by any partner — sub-second window, permanent only if the handler fails to stamp. Acceptable for an internal marketplace; the front-component Apply path (below) would remove it. - **Idempotency:** `on-application-created` dedupes duplicate applications by (opportunity, partner). - **Views & navigation**, reorganized into sections: - **Partner Workspace:** Open Briefs · My Applications · My Profile · My Deals - **Matching Admin:** Briefs to Match · Deals (board) · Applications · Applications by Opportunity · All Opportunities · Follow-up Applications · Follow-up Briefs - **Partners:** per Stage · per Country · Partner Applications · Validated (per-group COUNT) - Opportunity & Partner record **side panels** via FIELDS_WIDGET views (surface relations incl. `applications`, so a brief shows all its applications). ## Known issues / follow-ups - **Pre-existing failing unit test (not introduced here):** `on-partner-application-created › "posts a Discord embed when an APPLICATION-sourced partner is created"` — the handler/test are byte-identical to base; tracked separately. - **Follow-up views** lack the "older than 7 days" staleness filter — no confirmed relative date operand in this Twenty version (TODOs left in the views). - **Apply UX (future):** a front-component "Apply" button on the brief, calling an authenticated `/s/apply-to-brief` logic function (runs as the app), would replace the "create a record" entry point — nicer UX, and it removes the RLS IS-EMPTY trade-off. Not required to ship. ## Testing - Partner self-apply verified end-to-end as a partner (create application from a brief → lands on `APPLIED`). - Unit tests for the WON/BACKUP cascade + application handlers pass (the one failing test above is the pre-existing, unrelated Discord handler). - Lint clean (`oxlint`). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21816?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-light.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
The #1 Open-Source CRM
Website ·
Documentation ·
Roadmap ·
Discord ·
Figma
Why Twenty
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
Learn more about why we built Twenty
Installation
Cloud
The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
Build an app
Scaffold a new app with the Twenty CLI:
npx create-twenty-app my-app
Define objects, fields, and views as code:
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
nameSingular: 'deal',
namePlural: 'deals',
labelSingular: 'Deal',
labelPlural: 'Deals',
fields: [
{ name: 'name', label: 'Name', type: FieldType.TEXT },
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
],
});
Then ship it to your workspace:
npx twenty app:publish --private
See the app development guide for objects, views, agents, and logic functions.
Self-hosting
Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.
Everything you need
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the User Guide for product walkthroughs, or the
Documentation for developer reference.
|
|
|
|
|
|
Stack
TypeScript
Nx
NestJS, with BullMQ,
PostgreSQL,
Redis
React, with Jotai, Linaria and Lingui
Thanks
Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
Join the Community
Star the repo ·
Discord ·
Feature requests ·
Releases ·
X ·
LinkedIn ·
Crowdin ·
Contribute





