Commit Graph

276 Commits

Author SHA1 Message Date
nitin cdb2590355 Migrate call-recorder tests off own-code vi.mock (#22902)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22902?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. -->
2026-07-15 16:56:26 +02:00
nitin 2201917f33 Harden call recorder Recall API boundary (#22832)
Part 2/5 of splitting #22739. Stacked on #22831.

- `RecallBotSnapshot`: Recall bot payloads are parsed once at the API
boundary (`parseRecallBotSnapshot`);
`getRecallBot`/`listScheduledRecallBots` return typed snapshots, flows
never touch raw provider records
- Retry policy: honors `Retry-After` (seconds or HTTP-date, capped at
60s), treats 409 and 507 (ad-hoc pool exhausted) as retryable with
tailored delays, adds equal jitter to the linear backoff, and returns
instead of sleeping past 10s in-process so invocations never sleep into
their timeout
- `listScheduledRecallBots` accepts a server-side `metadata__` filter
and reports `truncated` instead of failing beyond 10 pages
- Extracts `cancelOrEjectRecallBot` into the recall-api layer
- Replaces the `CALL_RECORDER_MAX_MEDIA_FILE_SIZE_MB` server variable
with a fixed 500 MB constant: uploads stream since #22652, so the cap no
longer guards function memory and does not need to be operator-tunable

Next: billing charge verification, divergence-scoped sync crons, webhook
artifact continuation.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22832?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. -->
2026-07-15 15:20:18 +05:30
nitin f13bde1e03 Align call recorder vocabulary with core dialect (#22831)
Part 1/5 of splitting #22739 into a reviewable stack.

Mechanical renames only, no behavior change:
- `ingestion` -> `import` across data/domain/flows
(`completeCallRecordingIngestion` -> `completeCallRecordingImport`,
`ingestCallRecordingMedia` -> `importCallRecordingMedia`,
`reconcileCallRecordingTranscriptArtifact` ->
`importCallRecordingTranscript`, ...)
- `reapOrphanedCallRecorders` -> `cleanupOrphanedRecallBots`
- `ensureCallRecorder` -> `scheduleRecallBotForCallRecording`,
`healCallRecordingsMissingBot` ->
`scheduleRecallBotsForPendingCallRecordings`
- `extractRecallBotConvergence` -> `extractRecallBotSyncState`
- formatting drift in touched files

Next in the stack: Recall API hardening, billing charge verification,
divergence-scoped sync crons, webhook artifact continuation.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22831?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. -->
2026-07-14 01:51:39 +05:30
Paul Rastoin bc1ccf526f chore(twenty-partners): upgrade to twenty-sdk 2.21 (#22869)
## What

Upgrades the `twenty-partners` internal app to consume twenty-sdk 2.21.

- Bump `twenty-sdk` and `twenty-client-sdk` from `2.19.0-alpha.1` to
`2.21.0` (`package.json` + `yarn.lock`).
- Replace the removed `generateDefaultFieldUniversalIdentifier` helper
with `getFieldUniversalIdentifier` in `partner-applications.view.ts`,
renaming the `fieldName` argument to `name`.

## Why

`generateDefaultFieldUniversalIdentifier` was removed from the SDK and
replaced by `getFieldUniversalIdentifier`. Both compute the same
deterministic uuid-v5 (`fieldMetadata:objectUID:name` under the app
universal identifier), so the resolved `createdAt` field identifier is
unchanged; this is the only code change required to build against 2.21.

## Verified

- Install resolves to 2.21.0; runtime check confirms
`getFieldUniversalIdentifier` is exported and
`generateDefaultFieldUniversalIdentifier` is gone.
- Type check: no real errors.
- `oxlint`: 0 warnings, 0 errors.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01RUQ2yyfZcyKWqG57HbUYxx)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22869?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. -->
2026-07-13 18:12:14 +02:00
Paul Rastoin a6af730353 chore: upgrade call-recorder, last-contact, people-data-labs to twenty-sdk 2.20 (#22852)
## What

Upgrades three public apps to `twenty-sdk` 2.20.

For each app, bumped `twenty-sdk` and `twenty-client-sdk` to `2.20.0`,
raised the `engines.twenty` floor to `>=2.20.0`, and regenerated
`yarn.lock`:

- **call-recorder**: `2.19.0` -> `2.20.0`
- **last-contact** (`@twentyhq/last-contact`): `2.19.0-alpha.1` ->
`2.20.0`
- **people-data-labs**: `2.19.0-alpha.1` -> `2.20.0`

## Verification

- Lockfile diffs are version/checksum-only; the SDK's transitive
dependency set is unchanged between 2.19 and 2.20, so no new packages
were introduced.
- `yarn typecheck` passes cleanly for all three apps against 2.20,
confirming no breaking API changes to adapt to.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01YWiC3qAbBcE1kvBMWvaxba)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22852?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. -->
2026-07-13 12:06:16 +02:00
martmull 7f8a1da27a Fix Cloudflare rate limiting during last-contact backfill (#22811)
## Context

Upgrading `twenty-last-contact` on a production workspace failed with a
Cloudflare error 1015 ("You are being rate limited"). The
`backfill-last-contact` post-install function runs on every version
upgrade and fired 20 concurrent update mutations per batch with no pause
between batches, on top of paginated full-collection reads. On a
workspace with real email/calendar history that burst trips Cloudflare's
rate limit, and since the client SDK throws on any non-2xx response, a
single 429 killed the whole install/upgrade hook mid-backfill.

## Changes

- New `executeWithRetry` util: retries rate-limit (429 / Cloudflare
1015) and transient gateway/network errors (502/503/504, timeouts,
connection resets) with exponential backoff and jitter, capped at 5
attempts. Honors a `retry_after` hint when present in the response body.
Non-retryable errors still throw immediately.
- All backfill queries and mutations are wrapped with it.
- Update batch concurrency reduced from 20 to 10 to keep bursts under
the rate limit in the first place.
- Bumped app version to 1.1.1 with a changelog entry.

## Test

- Added unit tests for `executeWithRetry` (success passthrough,
retry-then-succeed, non-retryable passthrough, retry exhaustion,
`retry_after` handling).
- `yarn test:unit` (28 passed), `yarn typecheck`, `yarn lint` all green
in the app package.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01AtnkEfbpFhLp5qCJbSmZpE)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22811?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. -->
2026-07-10 15:26:07 +02:00
Paul Rastoin ace16add6c fix(twenty-partners): align createdAt view field with 2.19 deterministic ids and bump to 1.2.10 (#22782)
## What

- Bump `twenty-partners` from `1.2.0` to `1.2.10`.
- Fix the red integration tests by pointing the "Partner Applications"
view `createdAt` column at the real field metadata id.

## Why the integration tests were red

The `twenty-partners` CI job spins up `twentycrm/twenty-app-dev:latest`
and runs the integration suite. The suite's global setup does a dev sync
of the app, which failed:

```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
(universalIdentifier: 835c9a7e-72ec-46c5-8d90-39a02998f561)
```

The `partner-applications.view.ts` `createdAt` column referenced
`PARTNER_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER = 421cbcea-...`, an
invented id. `createdAt` is a reserved system field auto-created on the
custom `partner` object, and since 2.19 its universal identifier is
derived deterministically by the server from the application id, the
object id and the field name. The invented id matched nothing, so the
sync rejected the dangling view field and the app never registered,
failing every integration test.

## Fix

Set `PARTNER_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER` to the
deterministically derived value `746e2944-28d0-545e-9832-a46516e1d9a0`
(application id `e662fc1f-...` + partner object id `39101b39-...` +
field name `createdAt`). This matches the same derivation the server
uses for standard object system fields, verified against the
`twenty-shared` opportunity `createdAt` snapshot.
2026-07-10 13:27:21 +02:00
Paul Rastoin 9e20e2222a Fix front-component serving on Safari, kill stale presigned caching, and cache built bundles client-side (#22672)
## Context

Built front-component bundles are served via `GET
/rest/front-components/:id/:cacheKey`. On S3-backed storage (Twenty
Cloud) the endpoint used to 302-redirect the worker's authenticated
fetch to a presigned S3 URL. That redirect caused two bugs, and fixing
it removed the caching the redirect was accidentally providing — so this
PR also adds a proper client-side cache.

Closes twentyhq/core-team-issues#2653.

### Bug 1 — Safari 403 (Authorization header forwarded across redirect)

The renderer worker fetches the bundle with `Authorization: Bearer`. The
controller answered with a 302 to a presigned S3 URL. Per the Fetch
spec, browsers must strip `Authorization` on a cross-origin redirect.
Chrome/Firefox do, but Safari/WebKit forwards it, so S3 receives both a
query-string signature and an `Authorization` header and rejects with
`InvalidArgument: Only one auth mechanism allowed`. Result: front
components never load in Safari on S3-backed storage.

### Bug 2 — 302 cached publicly (browser-independent)

The redirect branch set no `Cache-Control`, so a CDN could cache it far
beyond the presigned URL's TTL (`STORAGE_S3_PRESIGNED_URL_EXPIRES_IN`,
900s). Consequences: any client re-served the cached 302 after 15 min
hits an expired signature (403, also affects Chrome), and the cached
redirect containing a live presigned URL is served to unauthenticated
requests (short-lived auth bypass).

### Regression this introduces — warm-load caching lost

Marking the handoff `no-store` (Bug 2 fix) is correct, but it means the
built bundle is no longer cached anywhere on the S3 path. The browser
HTTP cache cannot compensate: the presigned URL that actually returns
the bytes carries a fresh `X-Amz-Date`/`X-Amz-Signature` on every
request, so each download is a brand-new cache key and never hits. Net
effect without mitigation: every worker mount re-downloads the full
bundle.

## What changed

- **Front components return a 200 JSON body instead of a 302.** The
controller now responds `200 { url }` with `Cache-Control: private,
no-store`. The worker parses the JSON and issues a separate header-less
`fetch(url)` to S3. No redirect means the `Authorization` header is
never forwarded, making it browser-independent, and the handoff carrying
the presigned URL is never cached. The stream path (local storage) is
unchanged.
- **Client-side bundle cache in the renderer (restores warm loads).**
`fetchComponentSource` wraps the fetch chain in a `CacheStorage` layer
keyed by the **content-addressed** `/front-components/:id/:checksum.js`
URL. A hit returns the stored bundle and skips **both** the `no-store`
handoff to Twenty and the S3 download — restoring cross-session warm
loads without ever persisting a presigned credential. Because
`CacheStorage` is writable by any same-origin code (including the
untrusted component code this cache feeds), cached content is verified
against the sha-256 checksum embedded in the URL on every read, and
evicted on mismatch. Caching degrades to a plain fetch where
`CacheStorage` or WebCrypto is unavailable.
- **sha-256 checksums for built front components.** The SDK build and
workspace prefill now fingerprint built front-component bundles with
sha-256 (WebCrypto has no md5), enabling the integrity check above.
Other file folders keep md5. Legacy md5-fingerprinted URLs (32-hex)
simply bypass the cache — already-synced components keep working and
start benefiting from caching on their next build/sync.
- **WebKit e2e coverage.** Added a `webkit` project to the postcard
example's Playwright config mirroring `chrome` (shared setup +
storageState), plus iframe/worker diagnostics logging so front-component
failures surface in the test log. `TZ` is pinned to `Europe/Paris`
because WebKit on Linux ignores Playwright's `timezoneId` emulation and
rejects the runner's legacy `CET` alias, which crashed the record page
before the component could render.

### Why we hand off to S3 instead of streaming through Twenty

On S3-backed storage we deliberately **do not** proxy/stream the bundle
bytes through the API. The controller returns the presigned URL and the
worker fetches the content directly from S3, for two reasons:

- **Server CPU/bandwidth.** Streaming every bundle on every cold load
would put the API server on the hot path for all front-component
content. Handing off to S3 keeps that load off the server.
- **Domain isolation.** Front-component content is fetched from the
object-storage domain (e.g. `s3.domain.com`), a different origin than
the API and the front app. Serving untrusted/app-authored bundle content
from a separate domain than `twenty.com` keeps it off the app's origin.

The stream path is kept only as the local-storage fallback (no
S3/presign available), where these concerns don't apply.

## Examples

### The JSON handoff (S3 path)

```http
GET /rest/front-components/d3b07384-.../a1b2c3d4.js HTTP/1.1
Host: twenty.com
Authorization: Bearer <worker-token>
```

```http
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, no-store

{"url":"https://s3.domain.com/bucket/.../checkout-widget.mjs?X-Amz-Date=20260709T091500Z&X-Amz-Expires=900&...&X-Amz-Signature=AAAA1111..."}
```

The worker then fetches that presigned URL **without** headers (the
Safari fix) and gets the bundle bytes.

### Why the browser HTTP cache can't reuse it

| | Load 1 (09:15) | Load 2 (09:30) | Same key? |
|---|---|---|---|
| Twenty handoff URL | `.../a1b2c3d4.js` | `.../a1b2c3d4.js` |  but
response is `no-store` |
| Presigned `X-Amz-Signature` | `AAAA1111...` | `ZZZZ9999...` |  |
| Effective S3 URL (the HTTP cache key) |
`...&X-Amz-Signature=AAAA1111...` | `...&X-Amz-Signature=ZZZZ9999...` |
 new key → miss |

### What the CacheStorage layer stores

```
key   = https://twenty.com/rest/front-components/d3b07384-.../a1b2c3d4.js   (stable, chosen by us)
value = <bundle JS bytes>                                                   (NOT the presigned URL)
```

Keying by the stable logical URL (not the volatile URL the bytes arrived
from) is the one thing the native HTTP cache can't express. The
presigned URL is used once and discarded.

### Invalidation

No TTL and no explicit delete — invalidation is by key change. A rebuild
changes the checksum → changes the URL → guaranteed miss on the new key.
The old entry is orphaned and reclaimed by normal browser eviction
(quota/LRU; Safari ITP after 7 idle days). Global invalidation lever:
bump the cache name suffix (`front-component-source-v1`).

## Deploy note — front/server release window

Old frontend bundles (already-open tabs) hitting the new server receive
the JSON handoff where they expect raw JS and fail to render until the
tab is reloaded. The other direction is safe: the new worker against an
old server follows the 302 transparently (the content-type check falls
through to `response.text()`). Accepted as a short deploy-window
trade-off.

## Follow-ups (not in this PR)

- The client-side cache is a bridge for the `no-store` presigned
handoff. If built components are later served from a stable, non-signed,
public-by-URL path (they are already content-addressed by checksum, so
`immutable` is safe), the browser + CDN cache natively and this custom
layer can be removed.
- `GET /file/:fileFolder/:id` presigned 302s still carry no
`Cache-Control`. An explicit policy there (bounded `private, max-age`
below the presigned TTL) was prototyped in this PR and deliberately
dropped to keep the scope on front components — the file path
authenticates via a query-param token (part of any cache key), so its
exposure differs and deserves its own PR.

## Non-goals

Per the issue, file serving keeps its query-param token + 302 model.
Native browser loads (`<img>`, downloads) cannot do a two-step fetch and
already work on Safari. The public-asset redirect is left untouched
since its caching is intentional.

## Test plan

- Renderer: `fetchComponentSource.spec.ts` covers cache miss + write,
verified cache hit (no network), poisoned-entry eviction,
checksum-mismatch (never cached), non-fingerprinted and legacy-md5 URL
bypass, and the no-`CacheStorage` / no-WebCrypto fallbacks.
`fetchComponentSourceFromNetwork.spec.ts` covers the direct JS response,
the JSON handoff follow-through (header-less presigned fetch), and error
mapping.
- e2e: the postcard front-component spec now runs on both Chromium and
WebKit against prod-parity storage (S3 + Lambda).
- `oxlint` + `oxfmt` clean; typecheck passes on changed packages.

### Reproduction proof — Safari was always broken (e2e probe)

We ran the prod-parity postcard e2e suite (S3 storage + Lambda) with
WebKit against **`main` without this fix**, via a throwaway probe PR:
twentyhq/twenty#22717.

Result — [ci-privileged run
29015624468](https://github.com/twentyhq/ci-privileged/actions/runs/29015624468):

```
1 failed
  [webkit] › card-front-component.spec.ts:61 › renders the postcard name and status badge in the record preview
2 passed (1.4m)
```

`[webkit]` times out waiting for `getByTestId('postcard-card')` to
become visible (*element(s) not found*) while the Chromium run of the
same spec passes. This confirms the front component **never rendered in
Safari** on S3-backed storage prior to this PR — it is a genuine,
browser-specific bug, not a flake. The fix in this PR is expected to
turn that same `[webkit]` assertion green.

Note: running the WebKit tests in CI requires the WebKit browser binary
and its system dependencies in the e2e job (now installed via `npx
playwright install --with-deps chromium webkit`).
2026-07-10 10:12:58 +00:00
martmull 23cae2040a Improve application asset management (#22564)
App manifests could point the logo and screenshots at either external
URLs or public folder paths, and that was handled inconsistently across
install, sync and the marketplace.

This makes assets always bundled files:

- Manifests now use `logo` and `galleryImages` (a `string[]` of public
folder paths) instead of `logoUrl` and `screenshots`. The old fields
still work but are deprecated. Gallery order comes from the array index.
Normalization (deprecated-field migration, and warning about + ignoring
external URLs) happens in `defineApplication`, so the warnings surface
at define time.
- Logo is stored as a File record (`logoFileId`).
- The registration gallery is configured via a `settings` jsonb column
on `applicationRegistration` (`{ galleryImages: string[] }`) — populated
from the manifest, read by the marketplace detail (falling back to the
legacy `screenshots` column, then the manifest). No dedicated gallery
table.
- The marketplace detail DTO and front now use `galleryImages`.

Verified against a local Postgres: the fast instance commands run with
no pending-migration diff, the schema is correct, and the server boots.
Typecheck, lint, codegen and the application unit tests pass.

Not included yet: rehosting assets into storage for npm catalog and
tarball registrations, versioned cache busting on the serving route, and
a backfill for existing installs.

<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22564?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``&lt;img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;``</a>
2026-07-10 09:18:52 +00:00
martmull d8082d86ca Add Last contact on Companies and Opportunities to last-contact app (#22720)
## After
Example with google company, 2 contacts, 2 opportunities:

<img width="1512" height="332" alt="image"
src="https://github.com/user-attachments/assets/9dac3ac1-bbbb-41a4-b6d6-5f2180e33c13"/>

<img width="1512" height="313" alt="image"
src="https://github.com/user-attachments/assets/e6e326db-942f-45c2-a696-05fa4c32619b"/>

<img width="1309" height="313" alt="image"
src="https://github.com/user-attachments/assets/64ed84ab-f7f0-47e5-b48c-42572c7d2534"/>

## What

Extends the `twenty-last-contact` app so **Last contact** is also
surfaced on **Companies** and **Opportunities**, not just People.

Feedback: Companies and Opportunities already show emails and meetings
from their related Person records on their timeline, so they should
expose the most recent touch as fields too. Terminology and mechanism
intentionally mirror what the app already does on People (no
email-specific fields).

## Changes

- **New fields**, identical in name/label/semantics to the People
headline columns:
- `Company.lastContactAt` and `Opportunity.lastContactAt` (datetime,
"Last contact")
- `Company.lastContactItemMessage` / `lastContactItemCalendarEvent` and
the same pair on Opportunity (morph relation, "Last contact item"), with
inverse `lastContactForCompanies` / `lastContactForOpportunities`
relations on Message and Calendar event
  - All app fields are read-only in the UI (`isUIEditable: false`)
- **View fields** on the All Companies and All Opportunities views (Last
contact + Last contact item, visible).
- **Live updates**: the new `updateRelatedLastContact` util propagates a
person's interaction to their company and their point-of-contact
opportunities, guarded so an older interaction never overwrites a newer
one. It is called from both the email handler (`on-email-interaction`)
and the shared calendar path (`updatePersonLastContactFromCalendar`,
used by `on-calendar-interaction` and `on-calendar-event-started`), so
emails and meetings both count.
- **Backfill** (`backfill-last-contact`): aggregates each person's last
contact up to their company, and each opportunity's from its point of
contact.

Related-record scope: a company's last contact comes from its people; an
opportunity's from its point of contact.

## Not included (deliberately)

The directional fields (`lastInboundAt`, `lastOutboundAt`,
`lastContactBy`) and the `lastEmail`/`lastMeeting` shortcuts are not
mirrored: aggregated across many people they get semantically fuzzy, and
they would double the write amplification on every synced email for
little added signal.

## Tests

- Unit tests for `updateRelatedLastContact` (email and meeting
propagation, recency guard, no-company case).
- Integration tests: a related person's email sets company + opportunity
last contact; a later meeting supersedes an email; an older interaction
does not overwrite a newer one.
- Existing unit + integration tests updated and passing; typecheck and
lint clean.
2026-07-09 18:44:00 +00:00
nitin 78a0f9ea77 feat(call-recorder): type application and server variables, make summary prompt rich text (#22685) 2026-07-08 22:39:47 +02:00
nitin 545de99476 [Twenty Fireflies] Add calendar event summary and transcript UI components (#22667)
## Summary
Add comprehensive UI components and hooks for displaying call recording
summaries and transcripts on calendar event pages. This includes
markdown parsing for summaries, diarized transcript rendering, and data
fetching hooks integrated with the Twenty SDK.

## Key Changes

### New Hooks
- `useCalendarEventSummary`: Fetches and manages summary markdown for a
calendar event's call recordings
- `useCalendarEventTranscript`: Fetches and manages transcript data for
a calendar event's call recordings

### Summary Components
- `CalendarEventSummary`: Top-level component that displays summary for
selected calendar event
- `CalendarEventSummaryContent`: Container with header and content frame
- `CalendarEventSummaryBody`: Handles loading, error, and empty states
- `SummaryMarkdown`: Renders parsed markdown with support for headings,
lists, and paragraphs
- `SummaryInlineSegments`: Renders inline text with bold formatting
support

### Transcript Components
- `CalendarEventTranscript`: Top-level component that displays
transcript for selected calendar event
- `CalendarEventTranscriptContent`: Container with header and scrollable
content frame
- `CalendarEventTranscriptBody`: Handles loading, error, and empty
states
- `TranscriptEntryList`: Renders list of transcript entries
- `TranscriptEntryListItem`: Individual transcript entry with speaker
avatar, timestamp, and text
- `TranscriptErrorBox`: Styled error state display

### Utilities
- `parseSummaryMarkdownBlocks`: Parses markdown into structured blocks
(headings, lists, paragraphs)
- `parseSummaryInlineSegments`: Parses inline markdown for bold text
formatting
- `parseTranscriptEntries`: Parses diarized transcript format into
structured entries with speaker info and timestamps
- `formatSecondsAsClockTimestamp`: Formats seconds into HH:MM:SS or
MM:SS format
- `asRecord`: Type guard utility for converting values to records

### Page Layout Configuration
- `calendar-event-summary-tab.ts`: Defines Summary tab for calendar
event page layout
- `calendar-event-transcript-tab.ts`: Defines Transcript tab for
calendar event page layout
- Front component definitions for both summary and transcript

### Types
- `TranscriptEntry` and `TranscriptWord`: Structured transcript data
types
- `SummaryMarkdownBlock`: Block-level markdown structure
- `SummaryInlineSegment`: Inline text segment with formatting

## Implementation Details

- Uses `CoreApiClient` from Twenty SDK to query call recordings filtered
by calendar event ID
- Implements proper cleanup with cancellation tokens to prevent state
updates on unmounted components
- Supports both loading and error states with user-friendly messaging
- Markdown parser handles headings (h1-h6), bullet lists, and paragraphs
with inline bold formatting
- Transcript parser validates diarized format and gracefully handles
malformed entries
- Styled with emotion and theme constants from twenty-ui for consistent
design
- Integrates with `useSelectedRecordIds` hook to track selected calendar
event



https://github.com/user-attachments/assets/0bd63590-bb1e-4f89-8b96-e3fbb659473e


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22667?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. -->
2026-07-09 00:52:20 +05:30
Charles Bochet d746909184 feat(sdk): validate graph page-layout widgets at build time (#22559)
When an app defines a graph widget (aggregate, pie, bar or line chart),
the built manifest can carry the wrong key and the server rejects it at
sync time with a confusing "aggregate field is required" error.

The SDK type already requires
`aggregateFieldMetadataUniversalIdentifier` and renames the raw
`aggregateFieldMetadataId` at compile time. But the manifest build runs
esbuild with no type checking, so a wrong or missing key slips through
and only fails later on the server.

This adds a build-time check that mirrors the server validator, with a
hint pointing at the right key when the raw one was used. It is
non-breaking since correctly authored apps already use the universal
key.

Tests: unit tests on the validator, plus a real graph widget added to
the rich-app fixture so the integration and e2e suites cover the happy
path.
2026-07-08 16:25:47 +02:00
nitin 640e6b8b33 fix(call-recorder): stream Recall media to storage to fix OOM (#22652)
## Summary

Fixes Call Recorder media ingestion OOMs by streaming Recall media into
Twenty direct uploads instead of buffering the full file in memory.

## Changes

- Opens the Recall media download stream and uses its `Content-Length`
as the direct upload size.
- Creates a Twenty direct upload target, streams the media body to it
with Node `http`/`https` backpressure, then completes the upload.
- Cleans up download/upload streams on target creation, upload, and
storage response failures.
- Keeps the media size cap for now while making it no longer required
for memory safety.
- Bumps `twenty-client-sdk` and `twenty-sdk` to `2.19.0`.

## Tests

- `yarn test:unit
src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts
src/logic-functions/flows/__tests__/put-media-download-body-to-upload-target.test.ts`
- `yarn typecheck`
2026-07-08 15:48:49 +02:00
nitin 525ed74b2f Fireflies: port app to the CallRecording standard object (#22642)
## Context

The Fireflies app predates the core `CallRecording` standard object: it
wrote transcripts and summaries as markdown into two rich-text field
extensions on `CalendarEvent`. That model has no per-call record,
silently drops orphan calls, and diverges from how the Call Recorder app
stores recordings. This is the first step of moving Fireflies onto the
CallRecording architecture; media ingestion, retry markers, and a
dedicated tab/front component iterate on top of this.

## What this PR does

Replaces the CalendarEvent field extensions with upserts into the core
`CallRecording` object:

- **One CallRecording per Fireflies call** via a deterministic UUID
derived from the Fireflies meeting id — the `meeting.transcribed`
webhook, the `meeting.summarized` webhook, and manual **Sync Fireflies
Call** runs all converge on the same row regardless of order, with a
create-race fallback to update
- **Transcript as diarized JSON** (participant + sentence-level relative
timestamps) instead of markdown, matching the entry shape the
CallRecording `transcript` field holds for other recording apps;
fetchers now request `date` and sentence `end_time`
- **Summary stays rich text** in `CallRecording.summary`, composed from
the Fireflies overview / action items / topics / keywords
- **Call metadata filled** — title, `startedAt`/`endedAt` (from `date` +
`duration`), `externalRecordingId`; transcript sync marks the row
`COMPLETED`, a summary-first sync creates it as `PROCESSING`
- **Orphan calls are kept**: when no CalendarEvent matches by
`eventExternalId` / `iCalUid`, the CallRecording is created without a
calendar event link instead of being dropped
- **Role**: now reads/writes `callRecording`; `calendarEvent` drops to
read-only; the two schema field extensions and the markdown transcript
formatter are removed

### Housekeeping

- App version bumped to **0.2.0** so installed workspaces pick up the
upgrade
- `twenty-sdk` / `twenty-client-sdk` aligned on `^2.18.0` (matches
call-recorder)
- Gallery screenshots depicting the removed CalendarEvent fields dropped
from the marketplace config
- README: "Upgrading from 0.1.x" section documenting the field removal
and the backfill path
- Integration test guarding the mirrored status constants against the
server's CallRecording select options

## Upgrade note

Upgrading removes the two app-owned CalendarEvent fields and their
stored content (app upgrades infer deletions from the manifest diff).
That data is a cache of Fireflies content and is re-derivable: any call
still in Fireflies can be re-ingested as a CallRecording via **Sync
Fireflies Call**.

A follow-up PR adds a post-install/upgrade sweep (same pattern as the
Call Recorder sweep in #22552) that pages through Fireflies history and
replays each call through the same sync flow, so history backfills
automatically on install and upgrade — the deterministic ids make
re-sweeping idempotent.



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22642?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. -->
2026-07-08 14:11:49 +05:30
martmull 435073e9c5 Display featured applications in marketplace (#22635)
## After
<img width="1060" height="589" alt="image"
src="https://github.com/user-attachments/assets/74dfadcf-8698-4404-81c6-b309cc4cbf79"
/>
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/0e1a3644-04bc-4208-aa77-3842d9db9cc8"
/>
<img width="797" alt="image"
src="https://github.com/user-attachments/assets/0456ecce-607a-4705-8a89-c77029bfb6ac"
/>

- Remove IS_MARKETPLACE_SETTING_TAB_VISIBLE feature flag
- add vetted toggle in admin app tab
- added people data labs, last contact and call recorder to default
vetted applications

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22635?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. -->

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-07-07 17:05:54 +02:00
nitin 18c10f4632 Call recorder: sweep upcoming calendar events for recording bots on install (#22552)
## Context

The Call Recorder schedules Recall bots reactively — a database event
trigger reconciles a calendar event when it is created or updated. That
misses meetings that already existed before the app was installed, and
meetings created far ahead that are never edited as they approach.
Neither gets a bot, though recording is on by default.

## What this PR does

Moves to the rolling near-term window Recall recommends for [your own
calendar
integration](https://docs.recall.ai/docs/creating-and-scheduling-bots#scheduling-bots-with-your-own-calendar-integration)
— "a daily sync of the next 7 days". Bots are scheduled only for
meetings starting within a **7-day horizon**, kept complete by three
mechanisms:

- **Horizon (policy).** `resolveCallRecorderPolicyResult` caps
scheduling at 7 days from now (`EVENT_BEYOND_SCHEDULING_HORIZON`),
measured from `startsAt` (the bot's join time). The existing reactive
trigger inherits this — far-future creates no longer schedule, and a
meeting moved out of the window has its bot canceled.
- **Daily sweep (cron).** New `sweep-upcoming-calendar-events`
reconciles the 7-day window each day, so a meeting that ages into it
without being edited still gets a bot.
- **Fresh-install seed (post-install).** The app's single post-install
hook (`start-post-install-backfills`) runs the sweep once on a fresh
install so a new workspace is covered right away instead of waiting for
the first cron; on an upgrade it relies on the cron and backfills
missing summaries instead.

The sweep runs through the authenticated
`reconcile-upcoming-calendar-events` route, which batches ids through
the existing reconciliation flow and re-invokes itself near the 900s
timeout. Deterministic recording ids keep it idempotent. App self-calls
go through a shared `postToOwnRoute` util targeting the server-injected
`TWENTY_FUNCTIONS_URL`; a failed kickoff throws so the async hook
retries instead of going silently green.

Also: fallback titles for call recordings whose calendar event is
visibility-restricted; app version → 1.0.7.

## Deferred

- Far-future bots already scheduled by the previous no-cap behavior
aren't proactively canceled — they fire naturally, or cancel if their
event is edited out of the window.
- Recall rejects an in-place `join_at` update under 10 min out; today
that logs a warning rather than delete-and-recreate.

## Test plan

- `yarn test:unit`: 407 tests / 65 files pass — new coverage for the
horizon (including a meeting that starts in-window but ends beyond it),
the 7-day query filter, the cron handler, the post-install hook's
fresh-install vs upgrade branches, and the batch/continuation flow.
- `yarn typecheck`, `yarn lint`, and `yarn twenty dev:build` (manifest
build) pass.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22552?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. -->
2026-07-07 19:03:28 +05:30
Paul Rastoin 6966af735b chore(apps): patch bump public apps moved to sdk 2.19.0-alpha.1 (#22623)
## Summary

Bumps the `version` field (patch) of the public apps that were moved to
`twenty-sdk@2.19.0-alpha.1` in #22601. That PR intentionally left
`version` untouched ("they'll be bumped at publish time") — this is that
follow-up bump.

| App | Before | After |
|---|---|---|
| `@twentyhq/call-recorder` | 1.0.6 | 1.0.7 |
| `@twentyhq/people-data-labs` | 1.0.3 | 1.0.4 |
| `@twentyhq/last-contact` | 1.0.1 | 1.0.2 |

## Notes

- `twenty-partners`, `postcard` and `self-hosting` are intentionally
**not** bumped.
2026-07-07 13:16:11 +02:00
Paul Rastoin fe442fe5fe chore(apps): bump sdk to 2.19.0-alpha.1 and require twenty server >=2.19.0 (#22601)
## 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. -->
2026-07-07 13:03:43 +02:00
nitin cabe5545ae Use SDK calendarEventRecordPageFields identifiers in call-recorder (#22618)
Replaces the hardcoded calendarEventRecordPageFields view/group
identifiers in the call-recorder preference view-field with
`STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.calendarEvent.views.calendarEventRecordPageFields`,
resolving the TODO. The published `twenty-sdk@2.18.0` (already pinned by
the app) ships these identifiers with values matching the previously
hardcoded ones.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22618?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. -->
2026-07-07 15:32:18 +05:30
martmull 07a921f8ca Add Document Generator SDK app + step-by-step tutorial (#22522)
## What & why

This adds a **guided tutorial** that teaches the Twenty SDK by building
one real, useful app end to end — plus the finished app itself, ready
for the marketplace.

The app, **Document Generator**, turns reusable templates into
personalized documents using CRM data: write a template once with
`{{placeholders}}`, then generate a filled-in document for any Person or
Company from the command menu, an AI agent, or a workflow.

## Two parts

**1. The app — `packages/twenty-apps/public/document-generator`**

Each capability maps to one tutorial chapter:
- **Data:** `documentTemplate` + `document` objects, fields, and a
bidirectional relation
- **Logic:** a single `generate-document` handler exposed as an **AI
tool**, a **workflow action**, and an **HTTP POST route**; plus a public
**HTML view route**
- **UI:** two views + sidebar navigation, a **command-menu item** (on
Person selection) that opens a **React front component**
- **AI:** an agent + skill; a default application role; marketplace
metadata + logo
- **Tests:** unit tests for the template renderer + an install
integration test

**2. The tutorial —
`packages/twenty-docs/.../apps/tutorials/document-generator/`**

A six-chapter series under **Developers › Apps › Tutorial** (Overview →
Data model → Generating documents → HTTP routes → Building the UI → AI
agent → Publishing). Minimal prose, paste-ready code, inline links to
the matching reference pages, and real screenshots. Registers a new
"Tutorial" nav group and regenerates `docs.json` + the navigation
template.

## Verification

Validated against a running Twenty instance (`twenty-app-dev` on
`:2020`):
- `twenty dev --once` installs cleanly (28 metadata objects created)
- Generated a real document from a Person — placeholders resolved (name,
job title, `company.name`, email), zero missing tokens
- Command menu → front component → generate flow works in the UI
- Public HTML view route renders the document
- App gates green: `yarn lint` (0/0), `yarn typecheck`, `yarn test:unit`
(7/7)

All screenshots in the tutorial are captured from this run.

## Notes
- Left out per-app CI workflows (`.github/workflows`) to keep scope
tight — happy to add them if wanted.

https://claude.ai/code/session_012FoC76zPbPmpgtN7MXMPwy

---
_Generated by [Claude
Code](https://claude.ai/code/session_012FoC76zPbPmpgtN7MXMPwy)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22522?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. -->

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-07 11:13:34 +02:00
Raphaël Bosi 1d3f6176b2 Use record pickers for People Data Labs enrichment workflow inputs (#22596)
Follow-up to #21494, which added record-typed logic function workflow
inputs but deferred the People Data Labs migration until the SDK
release.

twenty-sdk 2.16.0 (published) now includes the `record`/`records` input
schema support, so this types the enrichment inputs accordingly:
`records` on enrich-people/enrich-companies and `recordId` on
enrich-person/enrich-company render as record pickers bound to
Person/Company.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22596?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. -->
2026-07-07 10:41:40 +02:00
Paul Rastoin 6c40c7b91a Deterministic system field universal identifier (#22565)
# Introduction

Close twentyhq/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
2026-07-06 13:34:33 +00:00
nitin fe8ec0463a Call recorder: parse conference links from calendar event text (#22555) 2026-07-05 17:44:13 +02:00
Charles Bochet 74b3a4216e chore(apps): remove the twenty-for-twenty app (consolidated into twenty-eng) (#22549)
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. -->
2026-07-04 18:05:30 +02:00
Charles Bochet 6b2405c2e9 feat(twenty-for-twenty): bump to SDK 2.18 with local UI components (#22542)
## 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. -->
2026-07-04 12:34:41 +02:00
nitin badaae17cd Call recorder: skeleton loading state + port front components to twenty-ui (#22473)
## What this does

Three changes to the call-recorder app's recording/transcript front
component:

### 1. Fix loading flicker with a skeleton loader (`4dc3a167`)

The widget's loading state rendered an empty `<video controls>` element
at 16:9, which was torn down and replaced once the recording query
resolved — so every open flashed a video player, even for calendar
events with no video at all.

- New `RecordingSkeletonLoader`: three transcript-shaped pulsing rows
(avatar circle + speaker bar + text bars) held at the same 240px
min-height as the empty state, so nothing jumps when content arrives.
Visuals follow the twenty-front skeleton standard
(react-loading-skeleton theming: `background.tertiary` base,
`background.transparent.lighter` highlight sweep, 4px radius), rebuilt
with emotion since the sandboxed bundle can't import the host's global
skeleton stylesheet.
- `RecordingVideoPlayer` now only renders with a real URL (`src:
string`).

### 2. Port front components to twenty-ui (`cea0d6ee`)

`twenty-ui@1.0.0-alpha.1` is now published and consumable by apps
(CSS-injection build support + the renderer's style bridge), so the
"remove once twenty-ui can be imported safely" duplications are
resolved:

- Deleted `recording-theme-css-variables.ts` → all components use
`themeCssVariables` from `twenty-ui/theme-constants` (only non-1:1
rename: `accent.primary` → `accent.accent9`).
- Deleted `TranscriptSpeakerAvatar` / `TranscriptSpeakerChip` → replaced
with twenty-ui `Avatar` (`size="md"`, `type="rounded"`) and `Chip`
(`ChipVariant.Transparent`, `isBold`, non-clickable) in
`TranscriptEntryListItem`.
- Added `import 'twenty-ui/style.css'` to the front-component entry,
plus a root `css.d.ts` declaration. The import is load-bearing: the SDK
inlines the CSS into the component bundle and the renderer's style
bridge injects it into the host document — the host's own twenty-ui CSS
can't be relied on, since scoped class hashes are content-derived and
drift between builds.
- Bumped `twenty-sdk` / `twenty-client-sdk` to `^2.18.0` (needed for the
CSS-injection build support) and added `twenty-ui@^1.0.0-alpha.1`.

### 3. Fix manifest build + typecheck after the port (`baa6eb2f`)

- `yarn twenty dev --once` failed at "Building manifest..." — the
manifest build loads front-component modules with `twenty-ui`
proxy-mocked, so the named `themeCssVariables` import resolves to
`undefined` and any static `${themeCssVariables.x}` interpolation throws
at module scope. All static interpolations are now lazy (`${() =>
themeCssVariables.x}`), deferring the access to render time in the
browser.
- Added `css.d.ts` to `tsconfig.spec.json`'s `include` — its
`["src/**"]` include replaced the parent config's default file set, so
the `*.css` module declaration was never loaded and `yarn typecheck`
failed with TS2882.

## Notes for review / testing

- Worth exercising manually: the skeleton during load, speaker
chips/avatars rendering via twenty-ui, and hovering a long (overflowing)
speaker name — Chip's tooltip uses a react-dom portal, which is the one
path not previously exercised inside the front-component sandbox.
- App version intentionally left at `1.0.4`.

https://claude.ai/code/session_01WuHQFWiRocn82ejxsnmz28

---
_Generated by [Claude
Code](https://claude.ai/code/session_01WuHQFWiRocn82ejxsnmz28)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22473?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. -->
2026-07-03 23:10:28 +05:30
nitin cbbb11b774 Add configurable call recorder summaries (#22405)
<img width="2560" height="1319" alt="CleanShot 2026-07-02 at 16 58 03"
src="https://github.com/user-attachments/assets/d968bff7-4b57-4816-be9c-02e5af32ae3d"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22405?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. -->
2026-07-03 21:48:48 +05:30
martmull 25fe66565c feat(applications): add type and options to application variables (#22157)
## Before
<img width="1452" height="709" alt="image"
src="https://github.com/user-attachments/assets/cd384ffa-cbe6-49d5-a807-ca8d580f55a9"
/>

<img width="1074" height="452" alt="image"
src="https://github.com/user-attachments/assets/720d38db-3495-4032-8831-17d24ec6a7e7"
/>

## After

<img width="1421" height="865" alt="image"
src="https://github.com/user-attachments/assets/2275c996-c895-4800-8324-2aa2ddfddd43"
/>

<img width="1348" height="870" alt="image"
src="https://github.com/user-attachments/assets/3e1a891d-6db0-4cbd-870a-2a5bbde4929d"
/>


## Summary

Adds typed application variables with optional select **options**. This
is the other half of #22059, split out from the custom-settings-tab
removal.

## Changes

- **Shared types**: `ApplicationVariable` / `ServerVariables` gain an
optional `type` (a `FieldMetadataType` subset — `TEXT`, `BOOLEAN`,
`NUMBER`, `DATE`, `SELECT`, `MULTI_SELECT`, `RAW_JSON`, `RICH_TEXT`,
`ARRAY`, …) and select `options`. New
`serializeApplicationVariableValue` /
`deserializeApplicationVariableValue` helpers convert typed values
to/from the encrypted string storage.
- **Server**: `type`/`options` columns on `applicationVariable` and
`applicationRegistrationVariable` (entities + DTOs), a fast `2-17`
instance command, manifest processing via the serialization helpers, and
a `QueryDeepPartialEntity` cast where the manifest JSON column is
persisted.
- **Frontend**: a polymorphic `SettingsApplicationVariableInput` that
renders the native `Form*` field component for each type (boolean,
number, date/date-time, select, multi-select, array, raw JSON, rich
text, text); fragment/query updates to fetch `type`/`options`.
- **SDK**: `defineApplication` validates that `SELECT`/`MULTI_SELECT`
variables declare non-empty `options` at build time (since `options` is
kept structurally optional for TypeORM/SDK compatibility).

Variables default to `TEXT` when no type is given, so existing manifests
are unaffected.

## Notes

The generated GraphQL artifacts (`type`/`options` on the variable types)
are regenerated by codegen; that change accompanies this PR.

https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23

---
_Generated by [Claude
Code](https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22157?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. -->
2026-07-03 10:52:22 +02:00
nitin bc1a4cb3fb feat(call-recorder): cap media file size during ingestion to avoid OOM (#22463)
Media ingestion buffered whole Recall files in memory; long recordings
OOM'd the logic function.

- Caps the size of ingested media files, configurable via the
`CALL_RECORDER_MAX_MEDIA_FILE_SIZE_MB` app variable (default 80 MB).
- Downloads stream chunk by chunk and stop at the cap; a Content-Length
above the cap skips the download without reading the body.
- A skipped file is recorded as `video_file_too_large` /
`audio_file_too_large` in `callRecorderFailureReason`; the completion
gate treats a marked file as resolved, so the recording still completes
and bills with its remaining artifacts.
- A real failure reason always wins over the size markers when the
recording fails.

Deferred: the cap is a stopgap until core supports streaming uploads
(TODO in code).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22463?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. -->
2026-07-02 20:55:47 +05:30
martmull f7f224aa7a Fix lint (#22416)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22416?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. -->
2026-07-01 20:56:54 +02:00
martmull 05ce08ddba Add twenty-app keyword (#22415)
as title, bump version

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22415?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. -->
2026-07-01 18:01:35 +00:00
martmull efd600c12b Update app name (#22410)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22410?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. -->
2026-07-01 16:33:32 +00:00
martmull 868ae4cbfd feat(last-contact): design for Last contact by + Last contact item (#22308)
To test, go to
https://twenty-applications.twenty.com/settings/applications/6aa2ca76-fdbe-456d-89ab-c622452ef055

## After

<img width="1512" height="697" alt="image"
src="https://github.com/user-attachments/assets/5b17f94e-6e0e-400a-8291-a39ad796e423"
/>


## What

Adds the design spec for the next version of the `twenty-last-contact`
public app, extending it from a single `lastContactAt` date column to
the three-column experience in the app's cover image on the All People
view:

1. **Last contact by** — the team member who last interacted with the
person (`ACTOR` field).
2. **Last contact** — the existing `lastContactAt` field, unchanged.
3. **Last contact item** — the email or meeting that was the last
contact, as a clickable record (`MORPH_RELATION` → message |
calendarEvent).

All three columns always describe the same single most-recent
interaction (atomic "newer wins" update).

This PR contains the **design doc only** —
`docs/superpowers/specs/2026-06-29-last-contact-by-and-item-design.md`.
Implementation follows.

## Why

The app today only answers *when* you last talked to someone. These
fields also answer *who* on your team and *through which* email/meeting,
matching the product vision in the cover.

## Key design decisions

- **`lastContactBy` is an ACTOR**, with the team member resolved from
the interaction's participants (`messageParticipant` /
`calendarEventParticipant` both carry `workspaceMemberId` +
`workspaceMember`).
- **No provider (Gmail/Outlook) logo.** That data lived on
`connectedAccount`, which v2.7
(`drop-connected-account-standard-object`) removed from the
app-queryable workspace schema. Confirmed acceptable; the actor still
shows the member + an email/calendar source.
- **`lastContactItem` is a MORPH_RELATION** following the SDK pattern
used by `attachment` / `noteTarget` / `taskTarget` (shared `morphId`,
one field per target, reverse relation on each target object).

## Reviewer notes

- **Load-bearing open risk** documented in the spec: how to *write* a
morph relation through the app's GraphQL API — no app in the repo writes
morph yet. The plan starts with a spike on this; if morph writes aren't
supported from an app, the fallback is two nullable `RELATION` fields
(`lastContactMessage` / `lastContactCalendarEvent`).
- No code/behavior change yet — safe to merge or hold as the design of
record.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22308?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. -->
2026-07-01 15:40:21 +02:00
Rashad Karanouh e0ebe0e029 v1.2.0 — Client brief intake (marketplace B2) (#22290)
## 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. -->
2026-06-30 14:08:50 +02:00
Raphaël Bosi 0dc6272da5 Remove twenty-ui reexport from the SDK and use twenty-ui directly (#22326)
## 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.
2026-06-30 11:17:48 +02:00
nitin 4045f9852e feat(call-recorder): use workspace logo for Recall bot image (#22302)
workspace logo - 

<img width="433" height="465" alt="CleanShot 2026-06-29 at 20 43 11"
src="https://github.com/user-attachments/assets/946782a5-7bab-461b-a613-ce056fec8bbc"
/>


how bot appears - 

<img width="2560" height="1315" alt="CleanShot 2026-06-29 at 20 47 01"
src="https://github.com/user-attachments/assets/91daa29c-83ef-4ac1-9d7c-ffa8bcf08135"
/>



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22302?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. -->
2026-06-30 14:01:15 +05:30
nitin 996d3a7921 bump call recorder (#22318)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22318?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. -->
2026-06-30 13:29:43 +05:30
nitin f263edd122 Add call recorder oom diagnosis logs (#22315)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22315?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. -->
2026-06-29 17:14:06 +02:00
martmull 8a257e0de3 Update public app names (#22228)
- update names to comply with @twentyhq/ prefix standard
- update versions
- upgrade twenty sdks versions 

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22228?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. -->
2026-06-26 17:51:01 +02:00
martmull 1bf0d06e18 Add author to twenty apps (#22226)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22226?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. -->
2026-06-26 15:11:37 +00:00
martmull a22eb5a591 Bump call-recorder and people-data-lab apps (#22213)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22213?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. -->
2026-06-26 12:21:43 +02:00
martmull d6fca7a82e Bump call-recorder and people-data-lab apps (#22208)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22208?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. -->
2026-06-26 11:24:56 +02:00
martmull 1cf1c33899 Add command menu items to people data labs (#22180)
- Adds command menu items to enrich people and companies
- Improve readme

## After

<img width="1039" height="584" alt="image"
src="https://github.com/user-attachments/assets/9c6c3967-e905-4bf2-a3ef-307ae5bdca92"
/>


<img width="2560" height="332" alt="image"
src="https://github.com/user-attachments/assets/4ab4ffb8-fe52-4612-99c7-282d0ffa2c94"
/>
<img width="2560" height="478" alt="image"
src="https://github.com/user-attachments/assets/d496e21b-de18-4c52-acc7-6106afca0bb7"
/>
<img width="2560" height="452" alt="image"
src="https://github.com/user-attachments/assets/caea4de3-5bee-4de7-a347-c5d8540fd179"
/>
<img width="2560" height="335" alt="image"
src="https://github.com/user-attachments/assets/92bd17e3-f4b6-4fcb-84ae-d8ebc7ccce1e"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22180?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. -->
2026-06-25 15:12:30 +00:00
nitin aabffc427a Call recorder readme nitpick (#22185)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22185?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. -->
2026-06-25 17:05:41 +02:00
nitin f270fb25c8 call recorder readme billing update (#22184)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22184?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. -->
2026-06-25 20:25:58 +05:30
nitin 4f429565e1 call recorder polishes (#22170)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22170?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. -->
2026-06-25 18:27:43 +05:30
Raphaël Bosi 19bbd59b53 Support CSS imports in front-components via runtime style injection (#22150)
Front-components compile to a remote-dom worker, so a CSS import like
`import 'twenty-ui/style.css'` can't load a stylesheet and was breaking
the build at the manifest step.

This makes the build inline an imported CSS file as a runtime `<style>`
injection that flows through the existing style bridge into the host.
Because the CSS is bundled alongside that same build's hashed class
names, an app's styling matches its own twenty-ui version regardless of
which version the host ships — no server, manifest, or host changes
needed.

The manifest extractor keeps the no-op CSS loader (it executes the
bundle in Node, where `document` is undefined); the inject plugin runs
only in the real build and the dev watcher.
2026-06-25 13:31:00 +02:00
Rashad Karanouh 2af5370749 v1.1.16 — Restore Partner slug in side panel and Notes tab (#22165)
## 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. -->
2026-06-25 13:20:53 +02:00
nitin db687c7407 chore(call-recorder): drop FAILED status schema bridge (#22134)
Cleanup of the call-recorder data layer + the SDK 2.16 bump and its
fallout, now that the `FAILED_UNKNOWN → FAILED` rename shipped in
`twenty/v2.16.0` (#22062).

- **Drop the schema bridge.** `executeCurrentSchemaMutation` and the
integration-test compatibility filter existed only to work around
servers exposing `FAILED_UNKNOWN`. Deleted the bridge;
`updateCallRecording` / `completeCallRecordingIngestion` call
`client.mutation(...)` directly. Integration test iterates all
`CallRecordingStatus` values.
- **Bump SDK** `twenty-sdk` / `twenty-client-sdk` to `2.16.0`.
- **One export per file.** Move `CallRecordingUpdateFields` to its own
type file; extract the duplicated media-file shape into
`CallRecordingMediaFile`.
- **Migrate the Recall webhook to `serverRouteTriggerSettings`** (2.16
dropped `serverWebhookTriggerSettings` + its declarative
`workspaceIdResolver`). The webhook is now a **resolver**
(`recall-webhook`) that verifies the Svix signature, reads
`twentyWorkspaceId` from the Recall bot metadata, and returns `{
workspaceId, targetLogicFunctionUniversalIdentifier, payload }`; the
platform dispatches to a new **target** function
(`process-recall-webhook`) in the resolved workspace, where
`CoreApiClient` is workspace-scoped. Resolver UID/route unchanged, so
the registered Recall endpoint URL stays valid. Failures now throw →
HTTP 500 (Svix retries) instead of returning 401/400.

Verified: typecheck, 213 unit tests, oxlint, oxfmt all green. **Not yet
verified end-to-end against a live server** — call-recorder is the first
app on `serverRouteTriggerSettings`, so a real Recall webhook should be
tested through the resolver→target path before relying on it.
2026-06-25 11:06:58 +02:00