## Summary
- Bumps `twenty-sdk` / `twenty-client-sdk` to the exact `2.19.0-alpha.1`
prerelease for the apps under `packages/twenty-apps` that actually
target a mutated standard identifier, and refreshes their lockfiles.
- Declares `"engines": { "twenty": ">=2.19.0" }` in those apps so
pre-2.19 servers refuse to install or upgrade to the rebuilt packages.
Only apps that reference a standard object's **system-field** universal
identifier, define a **relation into** a standard object, or call the
field-UID derivation helper need 2.19 (the identifiers those touch
changed from hardcoded UUIDs to deterministic hashes). Apps that only
define their own custom objects, or add plain scalar fields to a
standard object via its stable object-level id, were left on their prior
SDK pins. Currently bumped: `postcard`, `self-hosting`,
`twenty-partners`, `call-recorder`, `people-data-labs`,
`twenty-last-contact`.
## Context
Follow-up to #22565 (deterministic system field universal identifiers)
and #22599 (SDK prerelease bump).
Packages built with SDK ≤ 2.18 carry legacy system field identifiers and
are rejected by servers running `main`. Rebuilding with the 2.19 SDK
fixes that — but a rebuilt package must not be *upgraded into* by a 2.18
server, since 2.18 has no deterministic-identifier validation and would
diff the changed system field identifiers as a destructive delete +
create (the 2.19 backfill has not run there yet).
The `engines.twenty` constraint closes that gap: `doInstallApplication`
validates it via `validateServerCompatibility` before any mutation, and
this check has shipped since ~2.10, so every 2.18 server enforces it.
Resulting matrix:
- 2.18 fresh install of a rebuilt app: works, converges as a no-op once
the 2.19 backfill runs
- 2.18 upgrade of an existing install: rejected with
`SERVER_VERSION_INCOMPATIBLE` before any mutation
- 2.19 (post-backfill) install/upgrade: syncs cleanly
## Expected CI failures
**The `CI Twenty Apps` integration-test jobs are expected to fail on
this PR** (e.g. `people-data-labs`, `twenty-partners`). This is a
server-version mismatch, not an app bug — lint, typecheck and unit tests
all pass:
- The integration step spawns a real Twenty server from Docker Hub
`twentycrm/twenty-app-dev:latest` and runs `twenty dev` to sync each
app's metadata into it.
- `latest` currently resolves to **v2.18.5** — no `2.19` image is
published to Docker Hub yet.
- These apps now reference 2.19's **deterministic system-field universal
identifiers** (e.g. `company.createdBy`, `opportunity.createdAt`). A
2.18 server still carries the legacy identifiers, so the sync rejects
every 2.19-derived reference with `INVALID_VIEW_DATA` /
`FIELD_METADATA_NOT_FOUND` ("Field metadata not found").
- The failure surfaces as low-level field errors rather than a clean
`SERVER_VERSION_INCOMPATIBLE` because the `engines.twenty` gate
(`validateServerCompatibility`) only runs on the `app:install` / publish
paths — **not** on the `twenty dev` dev-sync path the integration tests
use.
These jobs will go green automatically once `twenty-app-dev:2.19` is
published to Docker Hub (or once CI pins the spawn action's
`twenty-version` to a 2.19 tag).
## Intentionally not included
- App `version` fields are untouched; they'll be bumped at publish time.
## Test plan
- [x] Refresh each bumped app's `yarn.lock` (`2.19.0-alpha.1` is now on
npm)
- [ ] Rebuild one app manifest and verify default field identifiers
match `getFieldUniversalIdentifier`
- [ ] Verify a 2.18 server rejects an upgrade to a rebuilt package with
`SERVER_VERSION_INCOMPATIBLE`
- [ ] Re-run `CI Twenty Apps` integration jobs once a
`twenty-app-dev:2.19` image is available
Made with [Cursor](https://cursor.com)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22601?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. -->
# Introduction
Closetwentyhq/core-team-issues#2641
Auto-provisioned field metadata used to get its `universalIdentifier`
from three unrelated sources: random `v4()` on the server when creating
custom objects, hardcoded values in `STANDARD_OBJECTS`, and an ad-hoc
`v5` derivation in the SDK manifest build. This PR unifies all of them
behind the shared `getFieldUniversalIdentifier` derivation:
```
universalIdentifier = f(applicationUniversalIdentifier, objectUniversalIdentifier, fieldName)
```
## Ownership model
The rollout is built on an explicit split of who owns a field's
universal identifier:
- **The 8 system fields** (`id`, `createdAt`, `updatedAt`, `deletedAt`,
`createdBy`, `updatedBy`, `position`, `searchVector`) are
**server-owned**. Their universal identifiers are always the
deterministic derivation, on **every** application (standard,
workspace-custom, installed). Clients cannot provide custom values: a
temporary check in `validateObjectMetadataSystemFieldsIntegrity` rejects
any non-derived system field identifier at migration build time. This
check stands in until system fields are generated exclusively server
side by the metadata side-effect engine and stripped from client inputs
— at which point it becomes structurally impossible to send one.
- **`name` is a default field, not a system field**: it is
auto-provisioned when absent (server side for custom objects, SDK side
for application objects) but authors can define their own. It is only
derived where it is guaranteed to be auto-provisioned. In particular,
standard objects keep their **historical hardcoded** `name` identifiers:
the standard app authors its `name` fields like any installed app would,
and moving those identifiers would break every installed application
referencing them (e.g. views on `opportunity.name`).
- **User-created and author-provided fields** keep random / explicit
identifiers, untouched.
## Server
- `validateObjectMetadataSystemFieldsIntegrity` now validates, on top of
the existing type/`isSystem` checks, that each system field's
`universalIdentifier` equals the deterministic derivation. Runs for
every object creation going through the migration orchestrator: app
sync, custom object creation, standard provisioning
- `build-default-flat-field-metadatas-for-custom-object.util.ts` derives
the system field identifiers (and the auto-provisioned `name`) with
`getFieldUniversalIdentifier` instead of `v4()`
-
`build-default-relation-flat-field-metadatas-for-custom-object.util.ts`
derives both the forward and the reverse default relation field
identifiers deterministically
- `generateMorphOrRelationFlatFieldMetadataPair` accepts optional
`sourceFieldUniversalIdentifier` / `targetFieldUniversalIdentifier` so
callers can inject deterministic values; user-created relations still
default to `v4()`
## twenty-shared
- `STANDARD_OBJECTS` system field identifiers (the 8) are now computed
at module load via `buildStandardObjectSystemFields`; `name` and every
other identifier keep their hardcoded values
- New snapshot test pinning **every** universal identifier of
`STANDARD_OBJECTS`: any identifier change now requires an explicit
snapshot update and should ship with a coordinated backfill
## SDK (breaking, pre-GA)
- `generateDefaultFieldUniversalIdentifier` delegates to
`getFieldUniversalIdentifier` and now requires
`applicationUniversalIdentifier`
- Reverse default relation field identifiers are derived from the
field's real coordinates (standard object UID + actual field name, e.g.
`targetRocket` on `attachment`) instead of the legacy custom-object UID
+ synthetic `${fieldName}Inverse` hash input. Field *names* are
unchanged
- The manifest build threads the application universal identifier
through default field injection (two-pass over object configs)
- `twenty dev:add` now resolves the application universal identifier
upfront and refuses to scaffold anything until `defineApplication`
declares one — no more `fill-later` placeholder for the app UID in
generated files
## Upgrade
A 2.19 **workspace command** backfills existing
`fieldMetadata.universalIdentifier` rows to the deterministic
derivation. Coverage follows the ownership model:
- **The 8 system fields**: taken over for **every application**,
whatever value they currently hold. This is both safe and required now
that sync rejects non-derived values — leaving a row unconverged would
make its application unsyncable
- **`name`**: workspace-custom app → always taken over
(server-generated, no author to clobber); installed applications → only
rows still carrying the legacy SDK derivation are recomputed,
author-provided identifiers are never touched; standard app → never
touched (hardcoded in `STANDARD_OBJECTS`)
- **Default relation fields**: workspace-custom app → forward fields on
custom objects and reverse fields on the standard relation objects;
installed applications → legacy-derivation probe only
All identifiers of a workspace are updated inside a single transaction,
then the command flushes the field-metadata-related workspace caches and
bumps the metadata version.
Stored `applicationRegistration.manifest` snapshots are intentionally
**not** rewritten: installs and upgrades always sync from the
`manifest.json` inside the resolved package (npm/tarball), the stored
column is only used for display/marketplace purposes.
## Breaking behavior for old packages (fail closed)
Packages built with an older SDK carry legacy system field identifiers
in their tarball `manifest.json`. Installing or upgrading such a package
now fails with an explicit `INVALID_SYSTEM_FIELD` validation error
("universal identifier is not deterministic") instead of silently
mismatching against the backfilled rows and triggering a destructive
delete+create. The remediation is to rebuild the package with the new
SDK; the backfill has already converged the installed rows, so the
rebuilt manifest syncs cleanly.
## Test plan
- [x] `twenty-sdk` unit tests (526 tests) and typecheck
- [x] `twenty-shared` unit tests (1635 tests) including the
`STANDARD_OBJECTS` snapshot; `name` identifiers verified byte-for-byte
identical to `main`
- [x] Lint and typecheck clean on all touched packages
- [x] Integration: create a custom object and verify system + default
relation field identifiers match the deterministic derivation
(`create-one-object-metadata-deterministic-field-universal-identifiers`,
13 assertions passing)
- [x] Integration: `failing-sync-application-object-system-fields`
extended with a non-derived system field identifier case; all
identifiers in the spec pinned deterministically so snapshots embedding
expected/actual values are stable across runs (verified with a double
run)
- [x] Integration: all application sync suites pass with the derived
system field identifiers now required by the
`buildDefaultObjectManifest` test helper (9 suites, 20 tests)
- [x] Full test-database reset: standard app provisioning and seeded
workspaces pass the new validation
- [x] SDK manifest build verified on the postcard example app: all
auto-generated default field identifiers match the derivation
- [ ] Run
`upgrade:2-19:backfill-deterministic-field-universal-identifiers`
(dry-run then real) on a seeded workspace and verify identifier
convergence with a rebuilt app manifest
Removes the internal `twenty-for-twenty` app from this public repo. It
has been consolidated into the private `twenty-eng` monorepo, which now
also owns the Resend module (moved there in a companion PR).
- Removes `packages/twenty-apps/internal/twenty-for-twenty/**` (177
files).
- No build wiring referenced it (no nx project, not in `nx.json`/root
workspaces); the only mention elsewhere is a naming-convention comment
in `twenty-linear`.
## ⚠️ Sequencing
- This removes **source only** — it does **not** uninstall the app
currently deployed on the workspace. Merge only **after** the twenty-eng
app has taken over the Resend objects there, so the live integration
isn't left orphaned.
- Supersedes the migration-plan doc PR (#22546), which added a doc into
this now-removed directory; that doc now lives in the twenty-eng app.
#22546 can be closed.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22549?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. -->
## What & why
Bumps the **Twenty for Twenty** internal app to the current SDK line and
serves it on the matching `twenty-app-dev` image.
- `twenty-sdk` / `twenty-client-sdk`: `^2.14.0` → **`^2.18.0`** (npm
latest)
- Runs against `twentycrm/twenty-app-dev:v2.18.5` (same 2.18 line)
- App version `0.1.0` → **`0.2.0`**
### The `twenty-sdk/ui` break
twenty-sdk 2.18 **removed the `twenty-sdk/ui` subpath** (#22326, "Remove
twenty-ui reexport from the SDK"). The intended replacement —
`twenty-ui@1.0.0-alpha.1` subpaths — requires **React 19 + a
monaco-editor peer** that this React-18 app can't adopt, so the app no
longer builds against 2.18 as-is.
Instead of migrating to twenty-ui, this replaces the four
`twenty-sdk/ui` consumers with **self-contained local components** under
`src/ui/`, imported via a new `@ui` alias:
- `Callout`, `H2Title`, `Status`
- Tabler-style inline-SVG icons (`IconAlertCircle`, `IconInfoCircle`,
`IconMail`, `IconRefresh`, `IconHelp`)
- a `ThemeColor` type
They mirror the twenty-ui components 1:1 using the `--t-*` theme CSS
variables the front-component host injects (the same inline-style
pattern the app already used for theme tokens) — no new runtime deps, no
React 19 requirement.
## Test plan
- `twenty dev:build` ✓, `yarn typecheck` ✓, `yarn lint` ✓
- Synced into a local `twenty-app-dev:v2.18.5` container (`twenty dev
--once`) — objects/fields/views/app install applied cleanly
- Verified rendering live: Sync Status page (`H2Title` headings +
`Status` "Not synced" pills) and all three `Callout` variants
(error/info/neutral) with correct colors + icons
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22542?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. -->
## Summary
**Version:** 1.2.0
(`packages/twenty-apps/internal/twenty-partners/package.json`)
Adds marketplace B2 client brief intake to the partners app:
- `POST /s/client-briefs` logic function — validates payload, creates an
unlisted Opportunity (`isListed = false`) with `— client brief` name
suffix
- **Client briefs** ops view for review before listing
- Unit + integration tests
Merge this **before** the website PR (`rk-client-brief-web`).
## Test plan
- [ ] `yarn twenty dev --once` syncs schema
- [ ] `yarn test` passes in `twenty-partners`
- [ ] Smoke: `POST /s/client-briefs` with `x-application-secret` creates
Opportunity with `isListed = false`
- [ ] Client briefs view visible in nav
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22290?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. -->
## What & why
Removes the `twenty-sdk/ui` reexport. Apps now use Twenty UI by
installing
[`twenty-ui@1.0.0-alpha.1`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1)
from npm and importing its subpaths directly. The reexport re-exported
types that didn't resolve, forcing typecheck workarounds.
## Changes
- **twenty-sdk**: delete `src/ui/index.ts`, drop the `./ui` export,
remove it from the browser vite build, and rewire the CLI manifest-mock
to `twenty-ui` (`.css` falls through to the empty-CSS loader).
`twenty-ui` stays a devDependency for the CLI fixture tests.
- **Renderer + create-twenty-app template**: import from `twenty-ui`
subpaths; the template pins `twenty-ui@1.0.0-alpha.1`.
- **Docs**: new "Using Twenty UI components" section (install + subpath
imports + `useTheme()` for theme tokens), codex references, and the
cross-doc-contract validator.
The `twenty-for-twenty` / `twenty-slack` example apps are intentionally
left on `twenty-sdk/ui`: they consume the published SDK (which still
ships `./ui`), and `twenty-ui@1.0.0-alpha.1` requires react 19 + a
`monaco-editor` peer the react-18 apps can't satisfy. They migrate once
the SDK is republished.
## Summary
**Package version:** `1.1.16`
- Adds **slug** to the Partner record-page `FIELDS_WIDGET` view so it
appears in the side panel for admins and partners (partners remain
update-locked on slug via `partner.role.ts`).
- Restores the **Notes** tab on the custom Partner `RECORD_PAGE` layout
— the marketplace v2 layout replaced the platform default but only
included Home + Timeline.
## Test plan
- [ ] `yarn lint` in `packages/twenty-apps/internal/twenty-partners` — 0
errors
- [ ] `yarn twenty dev --once` on a local partners workspace — sync
succeeds
- [ ] Admin: open a Partner record full page → slug visible under Name
in side panel; **Notes** tab present and can create a linked note
- [ ] Partner role (My Profile): slug visible, not editable; Notes tab
works
- [ ] After merge: `deploy` + `install` on prod partners workspace
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22165?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. -->
## What
Introduces a `packages/twenty-apps/public/` folder and moves the
publicly publishable apps into it, then generalizes the apps CI workflow
to cover both folders.
### Moves
The following apps were moved from `packages/twenty-apps/internal/` to
`packages/twenty-apps/public/` (via `git mv`, history preserved):
- `people-data-labs`
- `twenty-discord`
- `twenty-exa`
- `twenty-fireflies`
- `twenty-last-contact`
- `twenty-linear`
- `twenty-meeting-bot`
- `twenty-slack`
These remain in `internal/`: `self-hosting`, `twenty-for-twenty`,
`twenty-partners`.
### Workflow
- Renamed `.github/workflows/ci-internal-apps.yaml` →
`.github/workflows/ci-twenty-apps.yaml`.
- The discover job now scans **both** `packages/twenty-apps/internal`
and `packages/twenty-apps/public`:
- the "no nested `.github`" guard checks both folders,
- `changed-files` watches both globs,
- the matrix builder iterates over both roots (guarded with `existsSync`
so a missing folder is a no-op).
- Each matrix entry still carries its own `path`, so the `ci` job works
unchanged regardless of which folder an app lives in.
## Notes
- The apps are standalone packages (own `yarn.lock`, not part of the
root Nx workspaces), so no root `package.json` / `nx.json` / `tsconfig`
changes were needed.
- The companion publish workflow lives in `twentyhq/twenty-infra`
(`publish-internal-apps.yaml` → `publish-public-apps.yaml`) and is
updated in a paired PR.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Fmu3DWf1yTTkVW49eSkXwh)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22096?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. -->
## Summary
Two related cleanup items for the partners workspace:
### 1. Opportunity `stage` field — shared constant + reference catalog
- Adds `src/constants/opportunity-stage-options.ts` exporting
`OPPORTUNITY_STAGE_FIELD_UNIVERSAL_IDENTIFIER` and an
`OPPORTUNITY_STAGE_OPTIONS` catalog (stock stages + **Done** /
**Dead**).
- **`deals-board.view.ts`** imports the shared field id instead of
inlining `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`.
- The catalog is **not** synced by the app manifest — it documents
planned option ids for scripts/reference only.
**Done / Dead on prod:** added manually in Settings → Objects →
Opportunity → Stage (not via app sync). `defineField` extension of the
standard `stage` field was attempted and rejected
(`FIELD_ALREADY_EXISTS`). Deals kanban groups in the manifest stay
NEW–CUSTOMER only; Done/Dead columns appear when the workspace has those
options.
**Prod data cleanup (outside this PR):** all opportunities moved to
**Done** except `ed95573c-cbe6-4dcc-a459-5369a7449636` (Aranya CRM
Migration, kept in **New**).
### 2. Partners per Stage view — TABLE with stage grouping
Replaces the old KANBAN board with a **grouped TABLE** (same pattern as
Partners per Country / Applications):
- **Type:** `TABLE` (was `KANBAN`)
- **Group by:** `validationStage` — Application, Potential, Validated,
Former, Rejected
- **Columns:** Name, Country, Categories (`partnerScope`), Tier
(`partnerTier`)
- **Nav icon:** `IconTable`
View / view-field / group universal ids were aligned to prod after
KANBAN→TABLE install recreated the view (Twenty cannot change view type
in place).
## Version
**`1.1.9`** in this branch. Pre-merge deploys to `partner-twenty-com`
went through **1.1.5 → 1.1.11** while iterating on the view; prod may be
ahead of this branch’s pinned view id — one more id-alignment install
after merge may be needed if nav doesn’t land on the grouped table.
## Files touched (twenty-partners only)
| File | Change |
|---|---|
| `src/constants/opportunity-stage-options.ts` | New — stage field id +
option catalog |
| `src/views/deals-board.view.ts` | Import shared stage field constant |
| `src/views/partners-per-stage.view.ts` | KANBAN → grouped TABLE +
columns |
| `src/navigation-menu-items/partners-per-stage.navigation-menu-item.ts`
| Icon → `IconTable` |
| `package.json` | **1.1.9** |
## Test plan
- [x] `yarn lint` in `packages/twenty-apps/internal/twenty-partners` —
0/0
- [x] Prod: Done/Dead stage options present; Deals kanban shows 7
columns, no duplicates
- [x] Prod: Partners per Stage — TABLE grouped by validation stage with
Name / Country / Categories / Tier
- [x] Prod: Opportunity cleanup — 20 → Done, 1 stays New (Aranya CRM
Migration)
- [ ] After merge + install on fresh workspace: Partners per Stage
grouping renders without manual view fixes
## Summary
- Accept optional `useCase` in the TFT → partners
`import-opportunity-from-tft` webhook payload
- Map it to `Opportunity.need` on create (TFT Use Case → partners Needs)
- Add unit tests for happy path and null `useCase` handling
- Bump twenty-partners to 1.1.4 (patch)
## Test plan
- [x] `yarn test:unit` (43 tests passing)
- [x] `yarn lint` (0 errors)
- [ ] Deploy to local/partners workspace with `yarn twenty dev --once`
- [ ] POST smoke test with `useCase` in body; confirm **Need** field
populated
- [ ] TFT workflow: add `"useCase": "{{record.useCase}}"` to HTTP body
(manual)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22054?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. -->
## Summary
- Restores 17 partner profile fields in the `FIELDS_WIDGET` view backing
the Partner record page side panel (My Profile, admin partner views).
- Read-locks `validationStage` and `partnerTier` for the Partner role so
admins still see them on the record page but partners do not on My
Profile.
- Renames legacy `profilePicture` label to "Profile Picture (legacy)" to
distinguish from the new file field.
- Bumps `twenty-partners` to **1.1.3** (already deployed to prod).
## Test plan
- [ ] `yarn lint` in `packages/twenty-apps/internal/twenty-partners` — 0
errors
- [ ] `yarn twenty dev --once` on a local workspace — sync succeeds
- [ ] As admin: open a Partner record → side panel shows all profile
fields including validationStage and partnerTier
- [ ] As Partner role (My Profile): side panel shows profile fields but
**not** validationStage or partnerTier
- [ ] Upgrade path: install v1.1.3 on an existing workspace — view
fields update in place
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22024?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. -->
## Summary
Bumps **vitest** `^3.1.1 → ^4.1.9` in `twenty-meeting-bot`, which lets
**vite** resolve to **8.0.16** — and vite 8 dropped esbuild entirely
(moved to rolldown). That **removes the vulnerable transitive
`esbuild@0.27.7` outright**, resolving [Dependabot alert
#1470](https://github.com/twentyhq/twenty/security/dependabot/1470) —
GHSA-g7r4-m6w7-qqqr (esbuild dev-server arbitrary file read on Windows,
`>=0.27.3 <0.28.1`).
## Why a parent-bump, not a resolution
- The vulnerable esbuild came from `vite@7.3.5` (`esbuild ^0.27.0`, a
`0.x` caret capped at `<0.28` — so `yarn up` couldn't reach the fix).
- vite is gated by vitest's vite range: vitest **3.x** allows only
`^5||^6||^7` (caps vite at 7 → esbuild 0.27); vitest **4.x** allows
`^8`, and **vite 8 has no esbuild dependency at all**.
- So bumping vitest lets vite resolve to 8, which **eliminates the
vulnerable dependency entirely** — no `resolutions` entry to force or
maintain. (Matches the repo's stated preference: fix by upgrading the
parent, not by resolution.)
## Verification
- `yarn install` — vite resolves to `8.0.16`; all `@esbuild/*@0.27.7`
platform packages pruned; the only esbuild left is `0.28.1`
(already-fixed, from another consumer).
- `yarn typecheck` — passes.
- `yarn test:unit` — **202 tests / 30 files pass** under vitest 4.1.9,
no peer warnings; `vite-tsconfig-paths` still compatible with vite 8.
- `yarn install --immutable` — passes.
- (Integration `yarn test` is gated on a live Twenty server, so not run
here — that requirement is independent of this bump.)
- Separate yarn project — changes are confined to
`twenty-meeting-bot/{package.json,yarn.lock}`; no root impact.
## Note
vite 8 supports tsconfig-paths resolution natively
(`resolve.tsconfigPaths: true`), so `vite-tsconfig-paths` could be
dropped in a follow-up — left as-is to keep this change minimal.
## What
The TFT `HTTP Request` action POSTs `null` for empty fields (e.g.
`amountMicros:null`, `closeDate:null`). The import schema typed those as
`z.number()/z.string().optional()`, which reject `null` (it is not
`undefined`), so the endpoint returned `ok:false / invalid_input` before
any API call.
## Fix
A `dropNulls` preprocessor on the request schema converts `null`
(top-level or nested) to "field absent" before validation. Null optional
fields are simply omitted from the created opportunity; required `name`
still fails correctly if null. No schema-shape or behaviour-contract
change.
## Tests
Added a case feeding the failing payload shape (`amountMicros:null`,
`closeDate:null`) → `created:true` with `amount`/`closeDate` omitted.
42/42 unit pass, lint clean.
Patch bump `1.1.1 → 1.1.2`.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22017?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. -->
## What
- The SDK manifest build now sources an app's `aboutDescription` (the
long-form "About" tab content) from its `README.md`. An explicit
`aboutDescription` in the config still wins, matching the existing
marketplace CDN fallback.
- Removed the now-duplicated `aboutDescription` from internal app
configs and deleted the standalone `ABOUT_DESCRIPTION` constant files.
- Rewrote internal app READMEs to read as user-facing About content:
stripped developer/build/source-path noise, and expanded the thin ones.
`call-recording` and `self-hosting` (one-liners over substantial apps)
and `people-data-labs` were rewritten from a close reading of the code;
`twenty-exa` was verified for accuracy.
- Added a unit test (and a fixture README) covering README →
`aboutDescription` in the build.
## Why
The README and the About description were maintained separately and
drifted. Making the README the single source keeps the About tab
accurate and removes duplicated copy.
## Notes for reviewers
- Internal apps depend on the published `twenty-sdk`, so the build
change takes effect for them after an SDK release + dependency bump.
Until then, published apps still get README → `aboutDescription` via the
marketplace CDN sync.
- Standard/Custom app descriptions are unchanged (they are resolved in
the frontend, not via the manifest).
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22012?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. -->
## Summary
- Sends an explicit Recall.ai recording retention policy when creating
or rescheduling meeting bots.
- Uses the optional server variable
`MEETING_BOT_RECORDING_RETENTION_HOURS` instead of a workspace/app
variable.
- Defaults to `166` hours (6 days and 22 hours), keeping Twenty-hosted
deployments below Recall.ai's 7-day free storage window while still
allowing self-hosters to configure a longer retention period.
## Why
Recall.ai accounts created after June 12, 2025 retain recording media
forever unless retention is configured. Twenty ingests the meeting
artifacts into its own storage, so Recall.ai media retention should be
bounded by default to avoid unnecessary third-party storage cost.
## Changes
- Replaces the days-based app variable with the server variable
`MEETING_BOT_RECORDING_RETENTION_HOURS`.
- Adds a default retention constant of `166` hours.
- Builds `recording_config.retention = { type: 'timed', hours }`
centrally through `getRecallBotRecordingConfig()`.
- Applies the same recording config to both bot creation and bot
rescheduling.
- Documents the server variable and warns that values above `168` hours
may incur Recall.ai storage charges.
- Updates Recall API tests to assert retention is sent and invalid
values fall back to the safe default.
## QA
- [x] `yarn test:unit`
- [x] `yarn lint`
- [x] `yarn exec tsc --noEmit -p tsconfig.spec.json`
- [x] `git diff --check`
- [x] Live Recall.ai bot payload includes `recording_config.retention =
{ type: 'timed', hours: 166 }`
---------
Co-authored-by: Emmanuel Hernandez <emmanuel.hernandez@clickbalance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: ehconitin <nitinkoche03@gmail.com>
## Why
Upgrading the already-installed `twenty-partners` app in place (0.5.x →
1.x) via `yarn twenty app:install` aborts during the sync reconcile:
```
view: INVALID_VIEW_DATA: Cannot delete the only view for this object (379b11d5-…)
viewField: INVALID_VIEW_DATA: Label identifier view field cannot be deleted (21afcc69-…)
```
The marketplace-v2 change deleted `all-partners.view.ts`. On an
installed workspace that view is the Partner object's primary view and
holds the **label-identifier** viewField (the `name` column). Twenty's
manifest sync refuses to delete an object's *only* view or a
label-identifier viewField, so the in-place upgrade fails. (Fresh
installs are unaffected; only upgrades from a version that had
`all-partners` hit this.)
## What
Repurpose the retired `all-partners` identity for `partners-validated`
so the sync performs an **update in place** instead of a delete:
- `partners-validated.view.ts` now uses the old view id `379b11d5-…`,
and its `name` column reuses the old label viewField id `21afcc69-…`.
- Remove the now-dangling `ALL_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER`
constant (its file was already gone).
- Patch bump `1.1.0` → `1.1.1`.
The resulting view is the intended "Partners Validated"; the other
retired Partner view (`validated-partners`) deletes cleanly because the
object keeps other views.
## Revision
**Patch** — migration bugfix, no new behaviour.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21995?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. -->
## What
Adds a one-way, manual copy of a single Opportunity from the
**twentyfortwenty (TFT)** workspace into **partners**. No automatic/echo
sync — one record per button press.
A TFT-side **manual Workflow** (a "Run workflow" button on the
Opportunity record) → **HTTP Request** action → `POST /s/opportunities`
on the partners app. The new `import-opportunity-from-tft` logic
function:
- **Shared-secret guard** on the `x-application-secret` header vs the
existing `PARTNER_APPLICATION_SECRET` app variable (the SDK's
`isAuthRequired` only accepts user JWTs, not API keys — same pattern as
`submit-partner-application`).
- **Idempotent on `tftOpportunityId`** (a field the partners Opportunity
object already has); falls back to `name` for manual calls. Re-press →
no duplicate.
- **Find-or-create** the Company (by name) and the point-of-contact
Person (by primary email), then `createOpportunity` with `name / amount
/ closeDate / stage / companyId / pointOfContactId`.
## Out of scope
- The **TFT-side workflow is a UI step** (built once in the TFT
workspace) — it can't live in this repo. The exact HTTP action config +
body mapping lives in the workflow itself.
- Owner/workspace-member copy and stage-enum remapping are intentionally
skipped (YAGNI).
## Files
- `src/logic-functions/import-opportunity-from-tft.logic-function.ts` —
the handler + manifest.
- `src/logic-functions/__tests__/import-opportunity-from-tft.test.ts` —
unit tests (auth reject / idempotent / mapped create).
- `package.json` — version bump **0.5.5 → 0.6.0** (minor; new feature).
## Verification (local bundle)
- `yarn test:unit` 3/3 · `yarn lint` 0/0 · `yarn twenty dev --once` →
`created logicFunction import-opportunity-from-tft`, no manifest
warnings.
- Live `POST /s/opportunities` with the secret → `201
{ok:true,created:true,id}` (confirms the app role can create Opportunity
+ Company + Person). Re-POST same `tftOpportunityId` → `created:false`.
Wrong secret → `unauthorized`.
## Deploy note
Additive (new logic function + HTTP route) — upgrades cleanly with
`deploy` + `install`. `PARTNER_APPLICATION_SECRET` is already set on
prod, so no new application variable to configure.
## 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. -->
Adds a read-only "Transcript" tab to the CalendarEvent record page,
contributed by the Twenty Meeting Bot app. Renders the diarized
transcript stored on `CallRecording.transcript`, with placeholder states
for pending and failed transcription.
Front-end only — the transcription pipeline (request/download/reconcile
+ PENDING/FAILED markers) already landed on main.
Deferred: live-mount verification via `yarn twenty dev`.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21792?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. -->
## What
The People Data Labs enrichment logic functions (`enrich-person`,
`enrich-company`, `enrich-people`, `enrich-companies`) now expose an
**`Update fields`** select instead of the `overrideExistingValues`
boolean, and always return the enriched data in their output.
`Update fields` options:
- **Yes and overwrite**: persist, overwriting existing standard fields
- **Yes and don't overwrite** (default): persist, filling standard
fields only when empty
- **No**: write nothing to the record (no CRM fields, no PDL metadata,
no company creation)
## Why
The functions previously only persisted data. With `No`, they can now
fetch from PDL and return the result without modifying the record, so
downstream workflow steps can consume it. Every matched result now
carries a `data` object with the mapped record fields (standard + `pdl*`
values), and the bulk functions also declare their `results[]` array in
the output schema.
Billing is unchanged: a successful PDL match is still charged in all
modes, since the API cost is incurred regardless of persistence.
## Notes
- Default behavior is preserved (unset input means fill-empty +
persist).
- Typecheck, lint, and the full unit suite (368 tests) pass.
hardcoded activate_after -- 0 on everyone_left_timeout was getting
rejected by recall with a 400 (activate_after can't be 0). pulled it
into a named const, set to 1.
its weird -- recall doc says default is 0 -- but its erroring out for us
if we send zero. looks like the 0 default only applies when you leave
the field out, not when you pass it explicitly(did not verify). keeping
it as lowest possible value (1) for now.
what the property does -- after the meet starts, how long before the bot
starts watching the "everyone left" timeout. 1s is basically immediate,
which is what we wanted with 0 anyway.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21820?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. -->
## What
Adds two Twenty partner-pipeline skills to
`twenty-partners/src/skills/`, plus a patch version bump.
### `twenty-partner-application-triage`
Ranks the partner-application backlog by net-new value and surfaces a
short chase-list of high-value applicants who haven't booked a call.
Read-only against the live partners workspace. Ships `rank.py` as its
scoring helper.
### `twenty-partner-meeting-recap`
After partner calls, pulls Fireflies meetings, matches each to an
existing Partner by attendee email/domain, writes a recap
(transcript-first, Fireflies summary as fallback), and injects it as a
Note linked to the partner via `NoteTarget`. Skips leads/discovery calls
(no Partner match) and meetings whose content isn't ready yet. Optional
`--prune` deletes the Fireflies recording once its recap is safely in
the CRM (confirmed first).
## Version
`twenty-partners` 0.5.4 → **0.5.5** (patch: additive skill docs, no app
behaviour change).
## Notes
- Both skills read credentials from `~/.twenty/credentials.env`; no
secrets committed.
- All GraphQL queries/mutations are the proven ones used against the
live workspace.
## fix(security): bump tar to 7.5.16 across lockfiles (PAX file
smuggling)
Resolves [#1472 1474 1476 1479 1481 1483 1485 1487 1489 1491 1493 1496
1498 1505](https://github.com/twentyhq/twenty/security/dependabot/1472
1474 1476 1479 1481 1483 1485 1487 1489 1491 1493 1496 1498 1505).
### What
`tar` (`node-tar`) `<= 7.5.15` applies a PAX size override to
intermediary GNU long-name/long-link headers, causing a tar-parser
interpretation differential (file smuggling). Patched in `7.5.16`.
### Why these alerts
The advisory is scanned across many independent Yarn projects, so it
surfaced as one alert per lockfile: the root `yarn.lock` plus 13
`packages/twenty-apps/**` lockfiles (each pulls `tar` transitively).
### How
- Refreshed `tar` to `7.5.16` in the root and all 13 app lockfiles —
they hold `tar` via `^7.5.x` ranges that already permit it, so this is
an in-range lockfile refresh (no override) via `yarn up -R tar`.
- The root additionally had `tar@7.5.15` exact-pinned by
`@mintlify/previewing`, which has **no upstream fix** (latest `4.0.1163`
still pins `7.5.15`). Added a scoped resolution
`@mintlify/previewing/tar -> ^7.5.16`, **extending the existing scoped
tar resolutions** already used for `@electron/rebuild` and
`@electron/node-gyp`.
### Not included
`seed-dependencies/yarn.lock` (alert #1500) is intentionally excluded:
that lockfile and its checksum constants are already modified by the
open form-data PR, so its `tar` bump will follow separately to avoid a
conflict.
### Verification
- No `tar <= 7.5.15` remains across the root or any app lockfile.
- `yarn install --immutable` passes.