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.
## what
- converge stuck call recordings after the meeting starts, not based on
scheduled end
- replace the pending-transcript cron with Recall transcript artifact
reconciliation
- list existing Recall transcripts before creating one, so we avoid
duplicate requests
- keep a local pending marker as an idempotency latch for
stale/redelivered events
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21788?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
- Add post-call Recall recording ingestion for transcripts, audio, and
video
- Request/retrieve async transcripts and reconcile stale pending
transcript markers
- Complete call recordings atomically once all artifacts and billable
timestamps are available
- Charge `CALL_RECORDING` usage once per completed recording based on
recording duration
- Add Recall recording/media API helpers, transcript marker utilities,
and audio/video field identifiers
- Update generated metadata/SDK files and billing usage operation
support
- Add unit coverage for ingestion, completion, charging, Recall API
behavior, and reconciliation flows
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21758?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 PR adds a scheduled reconciliation pass for the Twenty Meeting Bot
app so call recording state does not depend only on event-driven updates
from calendar changes and Recall webhooks.
Why we need this --
- A call recording row can be created, but the process can fail before
the Recall bot id is written back.
- Recall webhooks can be missed or delivered late, leaving Twenty stuck
in an older local state.
- A bot can disappear from Recall, leaving Twenty with a stale
externalBotId.
- A cancellation can fail locally, leaving an app-managed Recall bot
that would still join the meeting.
What this adds --
- A cron logic function that heals botless scheduled call recordings.
- A convergence pass that pulls Recall bot state for stale local rows,
including SCHEDULED rows.
- Orphaned bot cleanup for app-managed Recall bots that are no longer
claimed by an open call recording.
- Guards so destructive bot cleanup does not affect bots claimed by
another app registration.
- Tests for the new stale-state, missed-webhook, and orphan cleanup
behavior.
Not included --
- Media ingestion.
- Transcript pipeline.
- Billing.
- Marking call recordings as COMPLETED.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21720?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 (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. -->
Adds a `recall-webhook` logic function (`POST /webhook/recall`,
unauthenticated) to the `twenty-meeting-bot` app. It verifies the
Recall/Svix `whsec_` signature over the raw body, parses bot lifecycle
events, matches the corresponding `CallRecording` (by
`twentyCallRecordingId` metadata, falling back to `externalBotId`), and
updates lifecycle fields — `status`, `externalBotId`,
`externalRecordingId`, and `startedAt`/`endedAt` (only when unset) —
guarded against stale out-of-order events that would move the status
backwards. Adds the required `RECALL_WEBHOOK_SECRET` server variable.
This opens the real provider test path: install the app → schedule a bot
through the existing calendar-event flow → point a Recall webhook
endpoint at Twenty → bot lifecycle events update the matching
`CallRecording`.
Unit tests cover signature verification, status mapping, the downgrade
guard, metadata/bot-id matching, and timestamp fill.
Deferred to later PRs:
- transcript/media ingestion, file uploads, and the completion charge
(so `COMPLETED` is never set here)
- repair/reconcile cron jobs
Also flips `DEFAULT_RECALL_REGION` to `eu-central-1` (separate commit)
to match the Recall account region.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21659?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. -->
Adds two single-record enrichment logic functions to the People Data
Labs app — `enrich-company` and `enrich-person` — that call PDL's
single-record Enrichment endpoints (`/company/enrich`,
`/person/enrich`). Each function declares both a workflow-action trigger
and an AI-tool trigger, so the same function is usable as a workflow
step and as an AI tool. They take a single `{ recordId,
overrideExistingValues? }` and return a single `EnrichResult`.
The new functions replace the previous `enrich-company-tool` /
`enrich-person-tool` AI-tool functions (which delegated to the bulk
endpoints), avoiding duplicate near-identical tools for the LLM. The
bulk `enrich-companies` / `enrich-people` workflow actions are
unchanged.
Implementation reuses the existing enrichment machinery: the
single-record adapters spread the existing company/person adapters and
only override `enrichBatch`, so identifier extraction, TTL guard, field
mapping (fill-only-if-empty), billing, and error backoff all carry over.
A new `post-pdl-single-enrich` util posts params directly and classifies
the response via the existing `parsePdlItem`.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21650?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 PR adds the backend scheduling slice for call recording. It wires
the `twenty-meeting-bot` internal app to reconcile calendar events,
calendar-channel associations, and workspace member auto-record
preference changes, then schedule, cancel, or reschedule Recall bots
based on the resulting policy.
It also adds the needed calendar-channel owner lookup support, generated
metadata updates, app config/default role updates, unit tests, and CI
for the internal app.
Coming next:
- Recall webhook handling and signature validation
- Stale-state convergence for failed Recall cleanup/recreate cases
- Media, transcript, audio, and video ingestion
- Billing charge flow
- Frontend/settings UI for recording controls
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21629?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. -->
## Problem
Partner applications **502** for any applicant whose company is already
in the CRM.
The `submit-partner-application` logic function dedupes applicants
**only by person email**. When no person matches that email, it takes
the create path and calls `createCompany` unconditionally. But
`Company.domainName` has a **UNIQUE index**, so whenever a company with
the applicant's domain already exists — which is common, since the **TFT
import seeds companies** — the mutation throws `"duplicate entry"`. The
handler's `catch` returns `{ ok: false }`, and the website
`/api/partner-application` route surfaces it as a **502**. The applicant
can never be submitted.
Real case that surfaced this: an applicant whose company (`BKG
Integration UG`, domain `bkg-integration.de`) was already present from
the TFT import with no Partner/Person attached.
## Fix
Extract `findOrCreateCompanyId`:
- Look the company up by **exact domain** (`domainName.primaryLinkUrl
eq`) and **reuse** it when found.
- Only `createCompany` when no domain matches.
- The matched company is **never renamed** — the existing CRM name wins
over the applicant's free-text `companyName`.
Person-email dedup is unchanged (already handled upstream in the
handler).
### Known limitation
Matches **active** rows only. A *soft-deleted* company still holds the
unique index and would re-collide; clear those with `yarn purge:prod`.
Noted inline.
## Tests
Adds an integration test: pre-seed a company by domain → submit an
application with the same domain → assert the partner reuses the same
company id and the company name is untouched.
## Version
`twenty-partners` 0.5.1 → **0.5.2** (patch: bug fix, no schema change).
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21615?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
- 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. -->
## Summary
- add 2.13 upgrade commands for call recording request status and
dropping CalendarEvent recordingPreference
- remove the recording preference from the core CalendarEvent standard
object
- add a scaffold-generated twenty-meeting-bot app with logo and the
CalendarEvent meetingBotPreference field
## Tests
- yarn install
- yarn lint
- yarn twenty dev:typecheck
- git diff --check
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21584?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
Bumps **all 14 `twenty-apps`** (internal, examples, community) to the
freshly published SDK **2.13.0**, and upgrades `vitest` 3 → 4 so `vite`
resolves to 8 (rolldown).
- `twenty-sdk` / `twenty-client-sdk`: `2.10.1` → `2.13.0` (each app's
original spec format preserved — plain, `^`, and `npm:…@`).
- `vitest`: `^3.x` → `^4.0.0` in the 13 apps that use it
(`call-recording` has no vitest).
## Why
Each app's `yarn.lock` had open Dependabot esbuild alerts — high
`GHSA-gv7w-rqvm-qjhr` and low `GHSA-g7r4-m6w7-qqqr`, both fixed in
esbuild `0.28.1`.
The SDK bump alone does **not** clear them: the advisories fire on *any*
esbuild `< 0.28.1`, and each app pulled a vulnerable `esbuild@0.27.7`
transitively via **vite** (through the `vitest` devDependency),
independent of the SDK. Bumping `vitest` to 4 resolves `vite@8`
(rolldown), which drops the esbuild dependency entirely.
`twenty-partners` additionally needed a recursive esbuild re-resolution
(its `tsx` dep had `esbuild@~0.28.0` pinned at the still-vulnerable
`0.28.0`).
After this change, **all 14 lockfiles resolve esbuild `0.28.1` only** —
zero copies `< 0.28.1`.
## Test
- All 14 lockfiles verified free of esbuild `< 0.28.1`.
- vitest 4 + vite 8 confirmed working: `people-data-labs` runs **334
tests across 83 files, all passing**.
- `twenty-for-twenty`'s suite fails only because its global setup
requires a live Twenty server (`/healthz`) — environmental, would fail
identically under vitest 3.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21553?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. -->
Adds per-enrichment billing to the People Data Labs app. Each
**matched** record charges the workspace in Twenty credits via
`chargeCredits` (`twenty-sdk/billing`), following the same pattern as
the exa app.
- Person match: **336,000 micro-credits** ($0.336 — PDL list price $0.28
+ 20% margin)
- Company match: **120,000 micro-credits** ($0.12 — PDL list price $0.10
+ 20% margin)
> **Note:** the 20% margin is a first draft, not final — it's a single
constant (`src/constants/billing-margin-multiplier.ts`) and easy to
adjust once we settle on pricing.
PDL only consumes a credit on a successful match, so `not_found`,
errors, and skipped records are free. The charge is emitted once per PDL
batch call (≤100 records) with `operationType: CODE_EXECUTION`,
`quantity` = match count, and `resourceContext` `pdl/person` /
`pdl/company`, at the moment PDL responds — a match whose record write
later fails is still billed since the PDL cost was already incurred.
Billing failures are non-fatal and never break an enrichment.
Prices and margin live as constants in `src/constants/` for easy
retuning. No SDK bump needed (`twenty-sdk@2.10.1` already ships
`./billing`).
Removes the People Data Labs app navigation menu — the "People Data
Labs" folder and its two view entries (Enriched People, Enriched
Companies) — from the sidebar, along with the now-unused navigation menu
item identifiers.
The "Enriched (PDL)" view definitions are kept and remain available on
the Person and Company objects; they just no longer appear as a folder
in the navigation menu.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21478?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
Tightens the **Partner** self-service role's field-level permissions so
a partner can edit its own profile but not admin/ops-controlled or
ownership fields. All locks are `canUpdateFieldValue: false` on the
Partner object.
**Admin-managed scalar fields (7):** `slug`, `validationStage`,
`reviewed`, `ranking`, `partnerTier`, `applicationNotes`, `lastMatchAt`
**Ownership relation FKs (2):** `partnerUser`, `company`
## Why
- The 7 scalar fields are admin/ops-controlled (validation, ranking,
tiering, internal notes) — a partner must not be able to self-promote or
alter ops data.
- `partnerUser` is the **RLS pivot**: the row-level predicate scopes a
partner to records where `partnerUser IS <their workspace member>`. If a
partner could clear or repoint it, they'd drop their own record out of
scope (an orphan only admins can see). It is already locked on
Opportunity; this brings Partner in line.
- `company` is read-only at the object level for partners, so its FK
link must not be repointable from the Partner side either.
The remaining Partner relations (`opportunities`, `persons`,
`partnerContents`) need no lock — they are already protected by
inverse-side field locks or object-level read-only / no-access rules.
## Scope
- One source file: `src/roles/partner.role.ts` (9 field-permission
entries).
- No schema changes — additive permission tightening; upgrades cleanly
via `deploy` + `install`.
- Version: patch bump `0.5.0 → 0.5.1`.
## Summary
Adds an external **Partner** self-service role that sees and edits only
its own
records via row-level security (RLS), so a validated partner can sign in
and manage
just the deals they're matched on.
## What's included
- **`partnerUser` relation** on Partner, Person, Company, Opportunity (+
inverse
relations on Workspace Member) — the login member a record belongs to.
- **RLS predicates** scoping each of those objects to "partnerUser IS
the current
workspace member", plus a self-scope on Workspace Member so member-typed
relations
resolve without exposing the internal team roster. Applied out-of-band
via
`yarn rls:configure` (the app manifest cannot ship RLS predicates).
- **Assign / unassign cascade** (`on-opportunity-partner-assigned` logic
function):
assigning a Partner to an Opportunity stamps `partnerUser` onto the
Opportunity +
its Company + People; removing the Partner clears it (and cascades to
the
Company/People when no other deal of that member still uses them).
- **Partner role permissions**
- Partner profile: full read/update.
- Opportunity: read all; **update `stage` and `amount` only** (every
other
user-facing field locked).
- Company / Person: read-only.
- Workspace Member: read-only, RLS-scoped to self.
- **`partnerUser` column** added to the Validated Partners view so the
login member
can be assigned inline.
## Install / upgrade note
After install or reinstall, run `yarn rls:configure` (`:prod` variant
for prod) to
(re)apply the RLS predicates and verify the field-permission locks.
Manifest sync
handles object/field permissions; predicates are applied by this script.
## Platform gaps found (for the eng team)
1. **Manifest sync doesn't invalidate the roles-permissions Redis
cache.** Permission
changes deployed via `yarn twenty dev --once` persist to the DB but
aren't reflected
in the cached snapshot used for enforcement until
`engine:workspace:metadata:permissions:roles-permissions:<workspaceId>:{data,hash}`
is flushed. Relevant on any real workspace when permissions change.
2. **Locking a server-injected field silently breaks all updates.** The
`*.updateOne`
pre-query hook writes `updatedBy` into every update, so
`canUpdateFieldValue:false`
on `updatedBy` makes the permission check reject *every* record update
with
`PERMISSION_DENIED`. Field-permission lock lists must exclude
server-managed/injected
fields (`updatedBy`; and `position`, co-written with `stage` on kanban
drag).
## Version
Minor bump → `0.5.0` (new role, new fields, new behaviour;
backwards-compatible).
## Testing
- Verified locally as a partner user: edits own profile; edits
Opportunity stage +
amount; Company/Person read-only; sees only matched deals; unassigning a
partner
removes the deal (and its company/people) from the partner's view.
- `yarn rls:configure` passes (5 predicates upserted; 24 Opportunity
fields locked,
stage + amount editable).
- Lint clean.
## Add People Data Labs enrichment logic functions
Implements the **enrichment logic functions** that call the PDL API and
map responses onto standard + `pdl*` fields.
- **Logic functions** — shared core powers bulk workflow actions
(`enrich-person` / `enrich-company`) and single-record AI tools. Each
guards a TTL, matches via identifier, fills standard fields only when
empty, always writes `pdl*` fields, and normalizes SELECT values against
field options.
- **Data model** — PDL employer now links via the **standard `company`
relation** (find-or-create, fill-only-if-empty); removed the dedicated
`pdlCurrentCompany` relation, six `pdlJobCompany*` scalars, and the
`pdl*` indexes.
**Not functional yet:** the post-install workflow seeding
(`seed-enrichment-workflow.ts`) is left in place but blocked — it needs
workflow-builder mutations that the CRUD-only app SDK schema doesn't
expose.
**Deferred:** metering/billing and auto-enrichment triggers.
https://github.com/user-attachments/assets/a570a484-ae79-4c57-a274-e068fdab78e4https://github.com/user-attachments/assets/6edfb5e1-cada-4892-87e0-b485eb359692
Clears 8 Dependabot alerts via in-range transitive/parent bumps and one
dead-dependency removal. **No `resolutions` overrides** were used —
every fix is a real version bump within existing semver ranges or a
parent upgrade.
### Root `yarn.lock`
- **react-router** 6.30.3 → 6.30.4 (open redirect via protocol-relative
URL) — pulled through react-router-dom, ranges unchanged — alert #1382
- **yaml** 2.8.1 → 2.9.0 (stack overflow on deeply nested collections) —
alert #734
- **uuid** `^13.0.0` → 13.0.2 in twenty-sdk + create-twenty-app (buffer
bounds check) — alert #1164
- **ip-address** `^9.0.5` dropped by bumping **socks** 2.8.3 → 2.8.9
(now depends on `ip-address ^10.1.1`, which is unaffected) — alert #1171
### `seed-dependencies` lockfile
- **uuid** `^10.0.0` → `^11.1.1` (direct dep; removed now-redundant
`@types/uuid` since uuid v11 ships its own types) — alert #1287
- **ip-address** `^9.0.5` dropped via the same socks bump — alert #1170
### `twenty-for-twenty` lockfile
- **resend** bumped to 6.12.4 (`^6.12.0` range kept), which drops its
`svix@1.90.0 → uuid@^10` transitive chain — alert #1278
### `twenty-companion`
- Removed the unused **simplemde** dependency. The note editor loads
SimpleMDE from a CDN `<script>` tag and never imports the npm package;
`easymde` (its maintained fork) is already a dependency — alert #690
### Not addressed here
The remaining alerts can't be closed without `resolutions` overrides
(deliberately avoided in this PR) or a larger migration:
- **qs** (#1305, #1304), **lodash** (#824 high / #823 / #385), **ws**
(#1238), **postcss** (#1061) — vulnerable copies are pinned exact /
bundled by parents (express, body-parser, @nestjs/*, next,
styled-components, zapier) with no in-range patch.
- **webpack-dev-server** (#1237/#692/#691) — pinned by
`@electron-forge/plugin-webpack` (still on v4); dev-tooling only.
- **uuid <11.1.1** (#1289) — spread across `^3`/`^8`/`^9` transitive
ranges; reaching v11 is a breaking jump.
- **apollo-server-core** (#735/#736) — requires an Apollo Server 3 → 4
migration.
The match-telemetry-event-with-people logic function triggers on
selfHostingUser.* (both created and updated) and unconditionally wrote
personId back to the record on every run. Since the handler's own write
produces an updated event — and the telemetry webhook also updates
unrelated fields (name, serverUrl, etc.) on returning signups — the
matching logic re-ran unnecessarily, querying people and writing on each
pass.
This adds an early-return guard so the handler only does work when
there's actually something to match:
Skip when the selfHostingUser is already linked (personId set) and the
primary email hasn't changed.
Still (re)match on first link and on genuine email changes — the
legitimate reasons for listening on updated.
The guard reads before.email.primaryEmail via an 'before' in properties
narrowing so it stays type-safe across the create/update event union.
## What
Twenty Partners app — partner enrichment + a simpler, partner-centric
view structure.
**Partner**
- New `website` (LINKS) field.
- Icons for the opportunity `partner` relation and `matchStatus` fields
(were the default "123").
**Partner views** (Partners folder → Applications / Validated / All)
- **Applications**: grouped by validation stage, showing Application +
Potential; columns Categories / Skills / Type of Team / Languages /
Country / LinkedIn.
- **Validated**: grouped by availability; columns harmonized with
Applications, plus Partner Tier.
- Wider Name / Categories columns.
**Opportunity pipeline** (Pipeline folder, simplified by partner
presence)
- `OPP without partner` / `OPP with partner` tables (filtered on partner
*is empty* / *is not empty*), `OPP all`, and a kanban **board** grouped
by match status.
- Sort by match status (uses the option position, i.e. pipeline order).
- Removes the previous matching views (waiting-for-match,
matches-overview, partner-deals).
## Version
Patch bump `0.4.2 → 0.4.3` (relative to main).
## What
Clears the 4 remaining High alerts in the standalone `twenty-apps`
lockfiles (hello-world, call-recording). Both are transitive and already
in-range, so a plain lockfile refresh picks up the patched releases — no
resolutions.
| Package | From → To | Requested by | Advisory |
|---|---|---|---|
| lodash | 4.17.x → 4.18.1 | `@genql/runtime` (`^4.17.20`),
`twenty-client-sdk` (`^4.17.21`) | GHSA-r5fr-rjxr-66jc |
| picomatch | 4.0.x → 4.0.4 | `tinyglobby` (`^4.0.3`) |
GHSA-c2c7-rcm5-vvqj |
Only the two app `yarn.lock` files change. These are isolated
example/internal apps (not in the root workspace), in the same family as
the already-merged #21371 / #21374.
## What
The standalone apps under `packages/twenty-apps/*` each ship **their own
`yarn.lock`** (they're not part of the root workspace). Three of them
still pinned the vulnerable transitive `vite@7.3.1`:
- `examples/hello-world`
- `examples/postcard`
- `internal/call-recording`
`vite <= 7.3.1` is affected by three advisories, all first patched in
**7.3.2**:
| Advisory | Summary | Open Dependabot alerts |
|----------|---------|------------------------|
|
[GHSA-v2wj-q39q-566r](https://github.com/advisories/GHSA-v2wj-q39q-566r)
(CVE-2026-39364) | `server.fs.deny` bypassed with queries | #894, #892,
#891 |
|
[GHSA-4w7w-66w2-5vf9](https://github.com/advisories/GHSA-4w7w-66w2-5vf9)
| Path traversal in optimized-deps `.map` handling | #901, #899, #898 |
|
[GHSA-p9ff-h696-f583](https://github.com/advisories/GHSA-p9ff-h696-f583)
| Arbitrary file read via dev-server WebSocket | #908, #906, #905 |
The root `yarn.lock` was already remediated separately (vite 7.3.2 /
8.0.16); these three sub-package lockfiles were the only ones still
flagged open.
## How
Ran `yarn up -R vite` per app to re-resolve vite within the existing
range; it lands on **7.3.5**.
## Scope
- **Lockfile-only**, 3 apps. No `package.json` changes.
- Each lockfile diff is 3 lines (version / resolution / checksum).
- Verified no vite resolution below the patched thresholds remains
anywhere in the repo.
## Summary
Propagates the just-published **`twenty-sdk@2.10.1`** security patch
into the `twenty-apps/*` mini-apps, clearing the bulk of the
nested-lockfile Dependabot alerts (the `tmp` + `undici` clusters).
Each app carries its own `yarn.lock`, so the fix only reaches them once
they bump the SDK. `2.10.1` drops the two vulnerable transitive deps
every app inherited:
| Vuln dep | Source | Fixed by |
|---|---|---|
| `tmp@0.0.33` (GHSA-ph9p / GHSA-52f5) | `inquirer ^10 →
external-editor` | `inquirer ^14` → `@inquirer/editor@5` (no
external-editor) |
| `undici@<6.24` (5 GHSAs) | `@genql/cli` | vendored genql codegen
(`@genql/cli` removed) |
## Changes
Bumps `twenty-sdk` **and** `twenty-client-sdk` (whichever each app pins
— several pin both) to `2.10.1` and regenerates each lockfile.
**10 apps updated** (all on the v2 line — minor bump, low risk):
`twenty-slack`, `twenty-discord`, `twenty-linear`, `twenty-partners`,
`twenty-fireflies`, `people-data-labs`, `twenty-for-twenty`, `exa`,
`github-connector`, `postcard`.
Verified per-app after regen: **`tmp@0.0.33` = 0** and **`undici@5` =
0** in every updated lockfile.
## Deliberately excluded
Three apps pin a **pre-2.0** SDK, where `→ 2.10.1` is a major jump that
risks breaking the app and needs per-app validation:
- `examples/hello-world` (`0.9.0`)
- `internal/call-recording` (`0.6.3-alpha`)
- `internal/self-hosting` (`1.22.0-canary.6`)
These still carry one `tmp`/`undici` alert each and should be handled in
a follow-up.
## Related
- `twenty-sdk@2.10.1` release (tag `sdk/v2.10.1`) — backport of #21339
(undici) + #21340 (tmp) from `main`.
Adds an `on-partner-application-created` logic function triggered on the
`partner.created` database event. When the website application form
creates a new Partner, it posts a rich embed to a Discord channel
(applicant, company, country, languages, partner scope, skills) with a
deep link to the record.
## How it works
- Fires only on genuine form submissions — discriminates via
`createdBy.source === 'APPLICATION'`, which excludes seed/import (`API`)
and manual UI (`MANUAL`) creation.
- Runs out-of-band on the worker (database event trigger), so it adds
**no latency** to the applicant's submission, and the linked Person
already exists by the time it runs.
- Best-effort: a Discord failure never fails the trigger (wrapped in
`try/catch`, 8s timeout).
## Configuration (per workspace — Settings → Apps → Twenty Partners →
Variables)
- `DISCORD_WEBHOOK_URL` (secret) — the incoming webhook URL. **The
feature is a no-op when unset.**
- `PARTNER_APP_FRONTEND_URL` — workspace front-end base URL for the
record deep link (e.g. `https://partners.twenty.com`).
## Notes
- New logic function + two application variables; version bumped to
**0.4.0** (minor).
- Unit tests cover the source-guard branches, the on/off switch, the
embed contents/ordering, and best-effort failure handling.
- The website and the existing `submit-partner-application` handler are
untouched.
Resolves the **vitest Critical** Dependabot alerts
(`GHSA-5xrq-8626-4rwp`, vitest `< 3.2.6`) — #1422–#1433.
Each `packages/twenty-apps/*` project is an **independent yarn project**
with its own `package.json` + `yarn.lock` (not part of the root
workspace). 12 of them declared `vitest: ^3.1.1` and locked an older
3.2.x. This bumps the range to `^3.2.6` and refreshes each lockfile to
**3.2.6** (latest 3.x, published 2026-06-01).
Projects updated: `community/github-connector`,
`examples/{hello-world,postcard}`,
`internal/{exa,people-data-labs,self-hosting,twenty-discord,twenty-fireflies,twenty-for-twenty,twenty-linear,twenty-partners,twenty-slack}`.
- Dev-scope only (test runner); no runtime impact.
- The **root workspace already uses vitest 4.x** (≥ the fix) and is
intentionally untouched.
- Verified: no `vitest < 3.2.6` remains in any `twenty-apps` lockfile.
## What
The standalone apps under `packages/twenty-apps/*` each ship **their own
`yarn.lock`** (they're not part of the root workspace), and those
lockfiles still pulled vulnerable transitive versions of `axios`,
`undici`, `tmp`, `qs`, `ws`, `brace-expansion`, `uuid` (via `twenty-sdk`
/ `twenty-client-sdk`). This was ~130 of the open Dependabot alerts —
none of them reachable from the root-lockfile PRs.
Ran `yarn up -R` per app to re-resolve the vulnerable transitives within
their existing ranges, across all 13 flagged apps:
- **`axios` → 1.17.0** — clears the entire proxy-auth-leak / ReDoS /
config-merge MITM advisory set (the 56 axios alerts)
- **`qs`, `brace-expansion`, `uuid`** → patched
- **`undici`, `ws`** → patched on the in-range majors (older majors that
parents pin exactly remain, same situation as the root lockfile)
## Scope
- **Lockfile-only**, 13 apps. No `package.json` changes.
- Test **fixtures** (`packages/twenty-apps/fixtures/*`) intentionally
left untouched — Dependabot didn't flag them and they back snapshot
tests.
# Scaffold People Data Labs enrichment app
Defines the data model for enriching **Person** and **Company** with
People Data
Labs data. **Scaffold only** — the enrichment logic (the "mapper")
follows
separately; see the package README.
## Included
- **Fields** on Person & Company (PDL base data set).
- **Enums as SELECT / MULTI_SELECT** validated against PDL canonical
files (v34.1).
- **Standard-field mapping**: no `pdl*` shadow where a standard field
exists.
- **Location → ADDRESS**; **relation** `pdlCurrentCompany` ↔
`pdlCurrentEmployees`.
- **Metadata**: `pdlId`, `pdlLikelihood`, `pdlEnrichmentStatus`,
`pdlLastEnrichedAt`, `pdlRawPayload`.
- Shared option constants + helper, indexes, and a view per object.
- **Add delete many**, `delete_many_{object}` added alongside the
existing `delete_one_{object}`.
- **Uniformize naming**, crud module, type names, and MCP helper
constants renamed for consistency.
- **Optimize tool schema (learn phase)**
- `find_many(_companies)`: **7 158 → 2 700 tokens**
- `find_one(_company)`: **280 → 126 tokens**
- ....
- Main mechanism: `reused: 'ref'` (line 7 of
`to-tool-json-schema.util.ts`). Zod walks the schema tree, tracks which
Zod schema instances appear more than once, and emits each reused
instance exactly once in `$defs`, replacing all subsequent occurrences
with a `$ref`. Works because filter and value schemas are now extracted
as shared objects.
- **Optimize system prompt (tool catalog)**, DATABASE_CRUD section
restructured to list operation patterns (`find_many_{object}`, …) once +
objects once, instead of the full N×M cross-product of tool names.
- **Optimize execute_tool**, shared record-properties schema (same
`$defs` deduplication applies at call time); introduced `upsert_many`;
added `selectedFields` to `find_*` so the agent only fetches the fields
it needs.
## Summary
Partners-app changes spanning the Partner object, its data scripts,
table views, and sidebar navigation.
### Remove the "Project Budget Typical" field
Dropped the `projectBudgetTypical` currency field from the Partner
object and every reference to it:
- `get-partner-by-slug` and `list-available-partners` logic-function
selections
- the seed script (type, write mapping, and per-partner data)
- the `import-from-tft` mapping (also dropping the now-unused
`partnerBudgetAverage` TFT source selection)
`projectBudgetMin` is intentionally kept.
### Rework partner views
- **Partners** (all-partners) view: replaced the **Deployment
Expertise** column with **Categories** (the `partnerScope` field).
- **Validated partners** view: added a **Languages Spoken** column.
- Set view `position`s so the in-object view switcher orders **Validated
→ Applications → Partners**.
### Navigation order
Reordered the "Partners" folder navigation items so the sidebar reads
**Validated partners → Partner applications → Partners** (Partner
content stays last).
### Also included
The previously-pushed fix that excludes partners with an empty slug from
the available-partners list.
## Notes
- No deploy/sync performed. The view-column and navigation-ordering
changes take effect once the app manifest is synced (`yarn twenty dev
--once` locally).
- The `deploymentExpertise` field itself is unchanged — only its column
was removed from the all-partners view.