Commit Graph

13649 Commits

Author SHA1 Message Date
Thomas Trompette 2dcf53619f fix(front): keep kanban header columns full-width when board overflows viewport (#22926)
## Problem

Before
<img width="1340" height="542" alt="Capture d’écran 2026-07-15 à 18 26
22"
src="https://github.com/user-attachments/assets/472dfe13-336a-43c7-8986-6cdcb04e5217"
/>

After
<img width="1340" height="542" alt="Capture d’écran 2026-07-15 à 18 26
12"
src="https://github.com/user-attachments/assets/e393a3fa-2245-42cc-b965-f16f3feaeff6"
/>

The record board (Kanban) header breaks when the screen is narrower than
the total column width. The stage header columns squish together to fit
the viewport while the cards below keep their fixed 220px width and
scroll horizontally, so the header labels no longer line up with their
columns.

Regression from #22323.

## Cause

#22323 wrapped each column header in `DragDropColumnSortableCell`. In
`fill` mode its `StyledSortableRoot` uses `min-width: 0` with the
default `flex-shrink: 1`, so the header cells collapse below their
column width when the board is wider than the viewport, instead of
overflowing into horizontal scroll like the body.

## Fix

Pin `flex-shrink: 0` in `fill` mode so header cells keep their column
width and overflow in step with the body. `fill` is board-only; the
record table header uses non-fill mode and is unaffected.

## Testing

Opportunities → "By Stage" Kanban, narrow the window below the total
column width: header stays aligned with the cards and scrolls
horizontally with them.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22926?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:43:37 +00:00
Thomas Trompette dd9763a7a0 Allow workflow run control mutations to be called with API key auth (#22924)
## Context

A customer wants to control workflow runs from an external system /
their own automation calling the API. Today the workflow mutations are
gated by `UserAuthGuard`, which requires an interactive logged-in user
(`request.user`). API-key requests set `request.workspace` but never
`request.user`, so they can't call them.

## Change

`UserAuthGuard` was applied at the class level on
`WorkflowTriggerResolver`, blanketing all five mutations even though
only `runWorkflowVersion` actually consumes the user (it looks up the
workspace member to stamp `createdBy`).

This drops `UserAuthGuard` from the class and keeps it only on
`runWorkflowVersion`. As a result, these become callable with API-key
auth:
- `stopWorkflowRun`
- `retryWorkflowRun`
- `activateWorkflowVersion`
- `deactivateWorkflowVersion`

None of these ever referenced the user, so no logic depends on it.
`runWorkflowVersion` stays user-only because it needs a workspace member
to attribute `createdBy`.

Permissioning is unchanged: `SettingsPermissionGuard(WORKFLOWS)` stays
at the class level and already resolves the permission for API keys via
`apiKeyId`, so a key still needs the WORKFLOWS permission.

## Notes / open questions

- No actor is recorded for these operations today (they only change
run/version state), so exposing them to API keys doesn't drop any audit
that existed. Attributing API-key-initiated actions would be a
follow-up.
- If there's a deliberate product stance that workflow control should
stay user-only, this is a policy change worth confirming.
2026-07-15 16:21:53 +00:00
Thomas Trompette f4b2968a74 fix(server): finalize workflow runs stuck in STOPPING (#22900)
## Context

A workflow run only reaches STOPPED via the in-flight worker execution:
`stopWorkflowRun` just flips a RUNNING run to `STOPPING` (records
intent), and the `STOPPING -> STOPPED` transition is done later inside
`computeWorkflowRunStatus`, which only runs while a worker is executing
the run's steps.

If no worker is executing the run at that point, nothing ever finalizes
it:
- the worker that owned the run crashed / was killed mid-step (e.g.
under heavy load), or
- a step legitimately sits in RUNNING awaiting an external event that
never arrives (the user stopped it).

Only `ENQUEUED` runs had a staleness sweep, so `STOPPING` (and
`RUNNING`) had no recovery path and would stay stuck indefinitely. This
has been observed in production (~150 runs stuck in `STOPPING` after
manual stops during a migration).

## Change

Extend the existing staled-runs machinery to also finalize runs left in
`STOPPING`:
- New `stuck-stopping-runs-threshold` (1h) +
`getStuckStoppingRunsFindOptions` matching `status = STOPPING AND
updatedAt < now - 1h`. `updatedAt` is a TypeORM update-date column, so
it reliably marks when the run entered `STOPPING`, and 1h stays above
any legitimate in-flight step.
- `handleStuckStoppingRunsForWorkspace` finalizes each match to
`STOPPED` via `endWorkflowRun`, so `endedAt`, step infos and the
`WorkflowRunStopped` metric stay consistent. It pages the backlog with
keyset pagination on `(createdAt, id)`, so a page whose finalizations
all fail can't stay at the front of the query and starve later runs
(failed ones are retried on the next sweep).
- Wired into the same cron (`WorkflowHandleStaledRunsCronJob`, every 10
min), per-workspace job, and the manual `workflow:handle-staled-runs`
command — so ops can also clear an existing backlog immediately. The
staled-ENQUEUED and stuck-STOPPING handlers run independently
(`Promise.allSettled` in the job, separate try/catch in the command), so
a failure in one doesn't block the other.

Stop remains manual and unchanged; this only guarantees a stopped run
eventually reaches `STOPPED`.

## Notes / scope

- No schema change (reuses `updatedAt`), so no migration.
- `RUNNING` runs orphaned by a worker crash have the same missing-net
problem; left out of scope here (this covers the user-triggered STOPPING
case).
- The new detection query scans `status`/`updatedAt` like the existing
ENQUEUED sweep; at very high `workflowRun` volumes an index on `(status,
updatedAt)` would help — same pre-existing consideration as the ENQUEUED
path.

## Tests

Unit tests for `handleStuckStoppingRunsForWorkspace`: no-op when none,
finalizes each match to STOPPED, pages through a multi-page backlog, and
advances past a fully-failed page instead of starving later runs. Plus a
unit test for the `(createdAt, id)` keyset condition in
`getStuckStoppingRunsFindOptions`. Full suite green, lint + typecheck
clean on changed files.

Manually verified on a real instance (Postgres): seeded a `STOPPING` run
aged 2h and ran `workflow:handle-staled-runs` -> transitioned to
`STOPPED` with `endedAt` set; a freshly-`STOPPING` run (updatedAt now)
was correctly left untouched by the 1h threshold.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22900?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:13:26 +00:00
github-actions[bot] 541c67d222 i18n - translations (#22923)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-15 17:58:15 +02:00
Thomas Trompette f67eb60c57 feat(workflow): soft-ref core workflow/version (backfill + dual-write) (#22821)
Replaces the shared-UUID model (core row reuses the workspace record id)
with a **soft-ref**: the workspace `workflow`/`workflowVersion` records
carry a nullable `coreWorkflowId`/`coreWorkflowVersionId` pointing to
their **own-id** core rows. This removes the assumption that workspace
record ids are globally unique - which is false, since prefilled/seeded
workflows share ids across workspaces. Supersedes #22776.

## In this PR
**Soft-ref columns (foundation):**
- **twenty-shared** `STANDARD_OBJECTS`:
`workflowVersion.coreWorkflowVersionId` + `workflow.coreWorkflowId` (+
snapshot test).
- **compute utils**: both as system, nullable UUID fields.
- **entity classes**: the bare fields.

**Version soft-ref sync:**
- Core `workflowVersion` rows get their own id, derived
deterministically from `workspaceId + record id` (uuidv5). Deterministic
so the upsert is idempotent: a failed write-back re-derives the same id
and self-heals instead of orphaning rows or colliding on the
one-active-per-workflow index.
- Sync = find-or-create keyed on the workspace record's
`coreWorkflowVersionId`, then write the core id back onto the workspace
record.
- Migrating over pre-soft-ref data: purges any core row whose id equals
the workspace record id before recreating, so old shared-UUID rows
aren't orphaned.
- Version dual-write listener reworked: delete is keyed by the core id
read off `before.coreWorkflowVersionId`.

Verified on a fresh `database:reset` (columns materialize, backfill
produces deterministic own-id rows linked back, idempotent re-run), a
simulated old shared-UUID state (stale rows purged, records re-linked),
and a simulated write-back failure (retry re-links to the same id, no
orphan, active-version index intact).

## Next steps (follow-up work, not in this PR)
1. Workflow-side soft-ref sync mirroring the version side (service,
module, dual-write listener, backfill command).
2. Workspace command to add the two columns to existing workspaces.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22821?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 17:50:00 +02:00
github-actions[bot] edd35c79d9 i18n - docs translations (#22919)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22919?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-15 17:14:17 +02:00
Charles Bochet 8e03921372 Add CREATED workspace activation status (read path + enum migration) (#22904)
## Context

Since v2 onboarding (#22303), workspaces are activated **before** the
billing plan step (now the last onboarding step). Users abandoning at
the plan step leave ACTIVE workspaces with a Stripe customer but no
subscription (~60–110/day on cloud, 935+ so far), and no cleanup
mechanism ever touches them: billing webhooks never fire (no
subscription), the suspended-workspaces cron only handles SUSPENDED, the
onboarding cron only handles PENDING_CREATION/ONGOING_CREATION.

Target lifecycle (across two PRs): `PENDING_CREATION → ONGOING_CREATION
→ CREATED → ACTIVE → SUSPENDED → deleted`.

**`CREATED`** = the workspace schema is provisioned but onboarding is
not complete — no billing subscription yet. It is **not** considered
active:

| Concern | CREATED behavior |
|---|---|
| Sign-in / invited teammates joining | allowed (invite-team step
precedes the plan step) |
| Member + metadata loading (app shell) | allowed (user must finish
onboarding) |
| Permissions | real permission checks (no PENDING-style bypass) |
| Version upgrades / workspace migrations | **included** (schema must
not drift) |
| Messaging/calendar/workflow/etc. crons | **excluded** — no background
processing until a plan is chosen |
| PLAN_REQUIRED onboarding lock | unchanged (still derived from
subscription existence) |

## What this PR does (read path only)

The enum addition ships as a **slow** instance command, which can run
after deploy — so nothing in this PR ever **writes** `CREATED`. The
write path (setting it at activation, the cleanup sweep, the backfill of
the existing zombie cohort) is a follow-up PR that ships once this
migration has run everywhere.

- **twenty-shared**: `CREATED` enum value;
`PROVISIONED_WORKSPACE_ACTIVATION_STATUSES` + `isWorkspaceProvisioned`
("schema exists": CREATED | ACTIVE | SUSPENDED), replacing
`isWorkspaceActiveOrSuspended` — all call sites (server member loading,
access-token workspace-member lookup, front metadata-store gates) meant
"has schema/members".
- **Slow instance command** (2.22.0): swaps
`core.workspace_activationStatus_enum` using the
rename→recreate→alter-column idiom. The CHECK constraints on
`core.workspace` embed casts to the enum type and would break the swap —
the command captures them from `pg_constraint`, drops them, swaps the
type, and restores them.
- **Pre-migration-safe queries**: Postgres rejects `IN ('CREATED', ...)`
when the enum value does not exist yet — even for reads, and the
instance-command runner itself queries provisioned workspaces before
migrating (a fresh database could never initialize). All
provisioned-status filters go through a new `activationStatusIn` util
comparing on `"activationStatus"::text`, valid before and after the
migration.
- **Upgrade path**: workspace iterator, command runner, upgrade-status
and workspace-version services iterate CREATED workspaces. Since they
now cover more than ACTIVE/SUSPENDED, the stale names were renamed to
`ProvisionedWorkspaceCommandRunner`, `hasProvisionedWorkspaces`,
`getProvisionedWorkspaceIds`, `loadProvisionedWorkspaces` (the
mechanical import rename in old version-command dirs is why this PR
carries the `ci:allow-previous-version-upgrade-mutation` label).
- **Sign-in**: `throwIfWorkspaceIsNotReadyForSignInUp` accepts CREATED
so invited members can join during onboarding (join authorization itself
is unchanged — enforced upstream in `checkAccessForSignIn`);
`activateWorkspace` idempotent-retry accepts CREATED as a terminal
state.
- **Transitions out of CREATED** (only write ACTIVE — safe to ship now,
dead until the write path lands): the Stripe webhook reactivation branch
also promotes CREATED, and `syncSubscriptionToDatabase` promotes
synchronously; both gated on
`WORKSPACE_ACTIVATING_SUBSCRIPTION_STATUSES` (Active/Trialing —
extracted from `shouldReactivateWorkspace`, behavior-preserving) so an
`incomplete` subscription created by the payment-intent flow before
payment never promotes the workspace.
- Deliberately untouched: all background crons, permission guards, JWT
strategy, PLAN_REQUIRED logic, admin panel (renders the raw status
string).

## Follow-up PR (after this migration has run)
1. `activateWorkspace` sets `hasWorkspaceAnySubscription ? ACTIVE :
CREATED` (billing disabled → always ACTIVE, self-hosted unchanged).
2. Cleanup: suspend CREATED workspaces older than N days (config var),
handing them to the existing suspended pipeline (warn → soft-delete →
destroy).
3. Backfill: cloud-only slow command moving ACTIVE workspaces with no
billingSubscription row (created since Jul 1) to CREATED.

## Verification
- Migration exercised against a real database via the command class: up
→ down → up; `enum_range` and `pg_get_constraintdef` checked after each
step (constraints restored against the new type, `DEFAULT 'INACTIVE'`
preserved).
- Pre-migration safety exercised for real: with the migration rolled
back (enum without CREATED), `run-instance-commands` — the exact
fresh-database CI path that failed before the `::text` fix — completes
cleanly.
- End-to-end with a workspace manually set to CREATED and the branch
server+front running: sign-in issues tokens, `currentUser` loads
workspaceMember(s), the full app loads with no console errors; GraphQL
returns `activationStatus: CREATED`.
- Workspace creation ran end-to-end locally in **both billing modes** on
this branch:
- billing disabled: signup → workspace creation → ACTIVE immediately →
onboarding completes with no plan step → app loads (unchanged behavior);
- billing enabled (Stripe test mode): signup creates the Stripe customer
eagerly → activation ends ACTIVE → subscription-less workspace is pinned
to the plan-required page → no-card trial checkout creates a `trialing`
subscription via `createDirectSubscription`/`syncSubscriptionToDatabase`
→ app loads.
- `twenty-shared` unit tests, server specs on touched services,
`lint:diff-with-main` and `typecheck` for shared/server/front all green;
full CI green.
2026-07-15 17:03:17 +02:00
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
github-actions[bot] 36a14478ae i18n - translations (#22916)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-15 16:38:28 +02:00
Weiko 25bd2897a3 Add weekly layout to record calendar (#22819)
## Summary

- Add a week layout to record calendar views and persist the selected
layout.
- Render `DATE` calendars as an all-day week and `DATE_TIME` calendars
as an hourly week.
- Add an optional end date field across calendar configuration,
metadata, persistence, and complete-view upserts.
- Use configured end values for ranged and multi-day events, with a
one-hour fallback when a `DATE_TIME` end is absent or invalid.
- Keep calendar cards consistent with the existing compact view,
including checkbox selection and whole-card record opening.
- Gate the weekly layout and end-date behavior behind the public Labs
`IS_CALENDAR_WEEK_VIEW_ENABLED` workspace feature flag.

## Week interactions

- Show overlapping timed events side by side and cap the visible records
at two per day.
- Display start and end times on timed cards, enforce a readable
30-minute minimum height, and keep today’s text contrast stronger.
- Drag timed events between days and times with 30-minute snapping while
preserving their duration, including zero-duration events.
- Show a create button when hovering a 30-minute slot; keyboard users
can focus a day, move the slot with the arrow keys, and reach the same
contextual action.
- Initialize new records with the selected slot time and a compatible
writable end value one hour later.
- Show the workspace time zone and current-time indicator in timed
weeks; date-only weeks keep the all-day section without an hourly grid.

## Configuration and data loading

- Only allow end fields that match the start field type, and prevent
selecting the same field for both boundaries.
- Load records whose ranges overlap the visible period so month and week
layouts display the same relevant records.
- Resolve and persist calendar end fields when updating existing views
through `upsert_complete_view`.
- Fall back to Month and ignore the configured end field while the flag
is disabled, without overwriting either persisted setting, so
re-enabling restores the previous configuration.
- Expose the flag in Labs and keep it default-off for workspaces without
a stored value; enable it in the development seeder.

<img width="1285" height="808" alt="Screenshot 2026-07-15 at 15 50 17"
src="https://github.com/user-attachments/assets/b7e3f7f1-ca77-492f-8cce-cca186ebca0b"
/>
2026-07-15 16:30:18 +02:00
martmull 0dbae2eda3 Address #22827 review comments and converge application file endpoints (#22868)
Follow-up to #22827, addressing the review comments left around merge
time and applying the endpoint convergence discussed afterwards.

## Review comments from #22827

- **Swallowed error in dev sync asset read**
([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571513265)):
the swallow is intentional (a missing public asset must not fail the
whole dev sync) but it now logs a warning with the asset path and error,
and the registration keeps its previously stored file for that path
instead of losing it.
- **`isAbsoluteUrl` location**
([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571524234)):
moved to `twenty-shared/utils/url`. The server, and now also
`twenty-sdk`'s `normalize-application-assets`, use the shared util.
- **Soft delete vs file cleanup**
([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571589558)):
per review, deleting a registration is now a hard delete. Stored assets
(bytes + rows) are deleted with it, dependent rows are removed by their
existing FK cascades, and installed applications keep working with their
registration link nulled. No soft-delete/cron mechanism.
- **Asset cap too generous**
([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571595745)):
lowered to 10MB per review and documented in the publishing and
public-assets docs pages.
- **One missing image retriggers a full asset sync**
([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571646243)):
`storeRegistrationAssets` now takes `skipAlreadyStoredPaths`; the
catalog sync passes it when the package version is unchanged, so only
assets missing a stored file are fetched instead of re-downloading
everything.
- **`existing.logo` already contains the new logo**
([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571667847)):
correct, `updateFromManifest` runs first, so the previous "keep fileId
when the path did not change" guard compared the new logo against
itself. The fileId preservation is now keyed on the stored server file
for the exact path (files are unique per `(applicationRegistrationId,
path)`): a changed logo path no longer inherits the old file's id, and a
transient download failure on an unchanged path still keeps the working
file. This also removed the fileId-preservation bookkeeping from
`storeRegistrationAssets`.

## Endpoint convergence

- **Path-addressed public route for registration assets**: `GET
/file/server/application-registration/:fileId` is replaced by `GET
/files/application-registrations/:registrationId/*path`, mirroring the
manifest's public-folder paths and leaving room for a future `:version`
segment. Assets stay addressable by stable ids server-side; the fileId
now only marks a path as stored. No URL is ever persisted (all are built
at query time), and the old route never shipped in a release, so there
is nothing to migrate.
- **`Application.logoUrl` resolved server-side**: new `ResolveField` on
the `Application` type builds the `/public-assets/...` display URL (or
passes absolute URLs through). `useApplicationChipData` now reads it
from `currentWorkspace.installedApplications`, and the frontend
`buildApplicationLogoUrl` util is deleted, so clients no longer
construct file URLs themselves.

## Validation

- Unit: `file.controller.spec` (route renamed, traversal case added),
`server-file-storage.service.spec` (`findServerFile`,
`deleteByApplicationRegistrationId`),
`application-registration-asset-url.service.spec` (new URL shape,
url-encoding), new `isAbsoluteUrl` test; all application/file suites
pass.
- Live against a local server: new route serves tarball and rehosted npm
assets with `public, max-age=3600` (nested paths included), 404s on
missing files, unknown registrations, traversal attempts, and the
removed old route; `findManyApplicationRegistrations` returns
path-addressed URLs for stored assets, CDN fallback for npm, absolute
passthrough; `installedApplications.logoUrl` resolves the public-assets
URL and stays null for logo-less apps. Registration hard delete verified
against the DB: file rows cascade, application rows keep a nulled
registration link.
- Typecheck + lint on twenty-server, twenty-front, twenty-shared,
twenty-sdk; metadata codegen and client-sdk regenerated.
2026-07-15 16:12:28 +02:00
github-actions[bot] 907c5cc39f i18n - translations (#22913)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-15 15:54:27 +02:00
Abdul Rahman f4ff234db8 feat: make record avatar/icon resolution data-driven via a configurable image identifier field (#22644)
## Summary

Today the avatar/icon shown for a record is hardcoded per object —
Company pulls a favicon from its domain link, Person uses `avatarUrl`,
etc. This PR replaces that hardcoding with a generic, data-driven
abstraction based on a configurable **image identifier field** on each
object's metadata (mirroring the existing **label identifier** concept).

An object's image identifier can point to:
- a **`FILES`** field → the uploaded image is used directly (rounded
avatar), or
- a **`LINKS`** field → a favicon is derived from the primary URL via
the Twenty icons service (squared avatar), gated by
`ALLOW_REQUESTS_TO_TWENTY_ICONS`.

This lets any object type (Opportunity, a custom "Listing", etc.) define
its own avatar/icon without code changes, and makes the field
configurable/overridable for standard objects.


##  Open question: also allow `TEXT` → direct image URL?
Right now the image identifier is restricted to `FILES` (uploaded file)
and `LINKS` (favicon). We deliberately left out `TEXT` → **direct image
URL** (e.g. an imported/synced photo URL stored in a text field).
There's precedent for it — Person's avatar was originally a `TEXT`
`avatarUrl`, and WorkspaceMember still is — and it's unambiguous (a
`TEXT` field has no favicon-vs-image ambiguity, and selecting it as the
image identifier is itself the declaration of intent). It's a small,
clean extension:
- add `TEXT` to the allowed image-identifier types,
- add an explicit `TEXT → raw URL` case
- `getAvatarType`: `TEXT → rounded`.
Caveats: it relies on admin assertion that the text values are image
URLs (no data-level guarantee), and external image URLs load third-party
content in the browser (IP-leak/hotlinking, same as favicons — a
proxy/cache would be the more robust long-term answer).

###  Resolution
Decision: **we will not support `TEXT` as an image identifier.** Image
identifiers stay restricted to `FILES` and `LINKS`, and any other type
fails closed (returns no avatar) on both the frontend and backend.
Instead, the legacy items that still rely on a `TEXT` avatar — Person's
deprecated `avatarUrl` and WorkspaceMember's `avatarUrl` — will be
migrated to `FILE` fields in a follow-up PR. Until then, WorkspaceMember
remains an exception (its `avatarUrl` still resolves through the
existing CorePicture path), and legacy Person `avatarUrl` values that
haven't been migrated will show initials placeholders.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22644?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 19:15:47 +05:30
github-actions[bot] e31e6c7794 i18n - docs translations (#22912)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-15 15:12:59 +02:00
Paul Rastoin a28c3a905a Route pre-2.19 upgrade commands through a legacy validate-build path (#22884)
## Problem

Since the centralized metadata side-effect engine landed in v2.19,
`WorkspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromRecord`
runs `metadataSideEffectEngineService.expandWithSideEffects(...)` before
building. As a result every historical upgrade command
(`upgrade-version-command/1-21/*` … `2-18/*`), authored before the
engine existed, now flows through it. Their operation matrix is no
longer applied literally: the engine injects/cascades companions (system
fields, `searchVector` field + GIN index, `searchFieldMetadata` rows,
unique backing indexes) and can hard-fail on reserved-identifier
collisions (`RESERVED_SYSTEM_UNIVERSAL_IDENTIFIER`).

Two hazards for already-shipped commands:

1. **Collision → hard failure**: a command declaring a companion the
engine now owns collides with the engine's deterministic
`universalIdentifier`.
2. **Silent drift**: on object/field create/delete the engine
adds/cascades companions the command author never intended, so
workspaces upgraded now differ structurally from those upgraded
incrementally before 2.19.

Suspected real-world impact: a self-hosted user upgrading v2.6.1 →
v2.21.0 hit `duplicate key value violates unique constraint
"IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE"` in
`upgrade:2-16:backfill-search-field-metadata`, because object-creating
commands now cascade and pre-create the deterministic
`searchFieldMetadata` rows the standalone backfill then re-inserts.

## Changes

- `workspace-migration-validate-build-and-run-service.ts`: extract the
shared compute-and-run tail into a private method, and add
`validateBuildAndRunLegacyWorkspaceMigration` (marked `@deprecated`)
that skips `expandWithSideEffects` and applies the matrix literally. The
existing side-effect entry points are unchanged (the live API and
application manifests depend on them).
- Repoint **all** pre-2.19 upgrade command call sites (1-21 … 2-18,
including `2-10 sync-call-recording-standard-objects`) to the legacy
method. Only the four `2-20/*` commands (target version ≥ 2.19) remain
on the side-effect path.
- `2-16 backfill-search-field-metadata`: recompute
`flatSearchFieldMetadataMaps` from the database before building the
existing-rows dedupe set. The migration runner only invalidates the
flat-maps keys a migration touched, so during a cross-version upgrade
earlier commands can leave this map stale; a stale map breaks the dedupe
and re-inserts rows, tripping
`IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE`. This is the direct fix
for the reported failure.
- Export `FlatEntityMapsBundle` so the shared tail can be typed.
- Document the side-effect vs legacy path and the selection rule in
`packages/twenty-server/docs/UPGRADE_COMMANDS.md`.

Selection rule: target version **< 2.19** → legacy path; **≥ 2.19** →
side-effect path (default). No exceptions.

## Known gap / merge ordering

The static twenty-standard definition declares all of `callRecording`'s
fields (including the `searchVector` system field) but **not** its
`searchVector` GIN index — every other searchable standard object
declares its GIN index statically. On the legacy path, workspaces
upgrading through `2-10 sync-call-recording-standard-objects` therefore
create the `searchVector` column unindexed (`searchFieldMetadata` rows
are created later in the same pipeline by the 2-16 backfill). The static
GIN index declaration plus a backfill for already-upgraded workspaces
land in a follow-up (twentyhq/core-team-issues#2672), which must ship in
the same release as this PR.

## Out of scope (separate follow-ups)

- `UpgradeMigrationService.getLastAttemptedInstanceCommand()` ordering.
- callRecording `searchVector` GIN index static declaration + backfill
(twentyhq/core-team-issues#2672, same-release dependency, see above).

## Test plan

- `nx typecheck twenty-server` passes.
- `nx lint:diff-with-main twenty-server` (oxlint + oxfmt) clean on
changed files.
- 2-20 command specs (which exercise the unchanged side-effect path)
pass.

---------

Co-authored-by: twenty <noreply@twenty.com>
2026-07-15 14:50:26 +02:00
Thomas Trompette 4e83a64f81 fix(front): read fresh metadata in SSE update path to avoid false unknown-field warnings (#22897)
## Problem

Sentry `warning`: *"SSE update event for person carried fields unknown
to this tab's metadata: lastInboundAt, pdlCertifications, pdlBirthYear,
…"* ([TWENTY-FRONT
issue](https://twenty-v7.sentry.io/issues/7610855477)).

The listed fields are all custom fields created by the **People Data
Labs app** (`packages/twenty-apps/public/people-data-labs`). The flow
that triggers this:

1. The app installs a batch of `person` fields (emits metadata SSE
events).
2. Its enrichment logic-function updates a person record with all of
those fields (emits a record-update SSE event).

Field creation already emits metadata SSE events, and the front applies
them **synchronously** to the Jotai metadata store
(`MetadataStoreSSEEffect` → `applyChanges` → `store.set`). So the store
converges.

The bug is that the SSE **record-update** handler doesn't read the
converged store. `useTriggerOptimisticEffectFromSseUpdateEvents` reads
`objectMetadataItems` from a React/Jotai closure captured at render
time. The long-lived SSE subscription in `useTriggerEventStreamCreation`
holds a metadata snapshot that lags the store, so even after the
field-create events have been applied, the record path still sees the
old field set. It then:

- flags the new fields as "unknown" and logs to Sentry, and
- **drops those field values** from the optimistic update
(`getUnknownRecordInputFields` filters them out), so already-loaded
views miss the enriched data until a refetch.

Metadata events are dispatched before record events within each SSE
message (`useTriggerEventStreamCreation` lines 126-128), and the store
update is synchronous, so a fresh store read at processing time sees
fields that converged in the same or any earlier message.

## Fix

Read `objectMetadataItems` fresh from the Jotai store at
event-processing time instead of from the render closure, and re-resolve
the object metadata item from that fresh list. This eliminates the
false-positive warnings and stops dropping legitimately-known field
values.

Follows the design from #22474: the metadata-event pipeline owns schema
convergence; the record pipeline just reads the converged store (now
actually reading the current store rather than a stale snapshot). A
genuine race where the record update truly precedes the field-create
event is still tolerated and still warns.

## Testing

- `nx typecheck twenty-front` passes
- `nx lint:diff-with-main twenty-front` passes

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22897?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 11:53:08 +00:00
martmull 055e8b5335 Add tab param to open a record side panel page on a specific tab (#22905)
## Context

`CommandOpenSidePanelPage` (and `openSidePanelPage` in the front
component SDK) could open a record in the side panel, but always landed
on the default tab. This adds an optional `tab` param to the
`ViewRecord` page params so an app command can open a record directly on
a specific tab.

## What changed

- **twenty-sdk**: `OpenSidePanelPageParams` `ViewRecord` variant accepts
an optional `tab` (a page layout tab id). Since
`CommandOpenSidePanelPage` props are `OpenSidePanelPageParams`, the
component picks it up automatically.
- **twenty-front**:
- New `setRecordPageActiveTabId` util resolves the record page layout
for the object (custom layout from the store, or the default layout id)
and presets `activeTabIdComponentState` on the tab list instance
(`${pageLayoutId}-tab-list-${recordId}`), which is shared by the side
panel and the full record page.
- `useOpenRecordInSidePanel` accepts `tab` and presets the active tab
before navigating; it also applies when the record is already open in
the side panel (tab switch only).
- `useFrontComponentExecutionContext` forwards `tab` to the side panel
open, and presets the tab when falling back to full-page navigation
(mobile, or objects that can't open in the side panel).
- **Docs**: mention the optional `tab` id in the
`CommandOpenSidePanelPage` description.

Unknown tab ids are harmless: `PageLayoutTabListEffect` falls back to
the layout's default tab when the preset id doesn't exist in the layout.
Dashboards are skipped since their layout id comes from record data, not
object metadata.

## Tests

- `useOpenRecordInSidePanel`: new test asserting the active tab atom is
preset on the correct tab list instance id.
- `useFrontComponentExecutionContext`: new tests for tab passthrough to
the side panel and tab preset on full-page fallback.
- `npx nx typecheck twenty-front`, `typecheck twenty-sdk`,
`lint:diff-with-main twenty-front`, `lint twenty-sdk` all green.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22905?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 13:39:11 +02:00
Paul Rastoin 14dacd8d35 [Slow db query] Resolve applicationId from cache in FileStorageService (#22870)
## Context

Sentry flagged a recurring slow DB query (TWENTY-SERVER-HZJ): `SELECT
... FROM core.application WHERE workspaceId = $1 AND universalIdentifier
= $2 AND deletedAt IS NULL LIMIT 1`, emitted on every file write under
`POST /graphql` (record avatar/file fields) and `POST /metadata`.

`FileStorageService` re-resolved the owning application row from
`core.application` by `(workspaceId, universalIdentifier)` on every file
write, uncached and synchronously in the request path. The row was only
used to recover `application.id`. The workspace cache already exposes
this mapping via `flatApplicationMaps.idByUniversalIdentifier`.

Closes twentyhq/core-team-issues#2668.

## Changes

- Injected `WorkspaceCacheService` into `FileStorageService` in place of
the `ApplicationEntity` repository.
- Added `resolveApplicationIdOrThrow`: resolves `applicationId` from
`flatApplicationMaps.idByUniversalIdentifier` on the normal
(already-committed) path, throwing
`FileStorageException(FILE_NOT_FOUND)` on a cache miss. When a
`queryRunner` is provided (application-creating transactions, where the
freshly created row is not yet in cache), it keeps the DB read through
`queryRunner.manager` so it can see uncommitted rows.
- Added `resolveApplicationUniversalIdentifierOrThrow` for the by-id
lookup in `deleteByFileId`, resolved from `flatApplicationMaps.byId`.
- Applied the cache path to `writeFile`, `createPendingFile`,
`deleteFile`, `deleteFolder`, and `deleteByFileId`. Only `writeFile`
carries a `queryRunner`; the others never do.
- Updated `FileStorageModule` to import `WorkspaceCacheModule` and drop
the now-unused `ApplicationEntity` repository registration.

No migration needed: a partial unique composite index on
`(universalIdentifier, workspaceId) WHERE deletedAt IS NULL AND
universalIdentifier IS NOT NULL` already exists on `ApplicationEntity`
and covers the query.

## Tests

Extended `file-storage.service.spec.ts`:
- cache hit resolves `applicationId` without a DB call,
- cache miss throws `FILE_NOT_FOUND`,
- the `queryRunner` path still reads from the DB and skips the cache.

All 95 file-storage unit tests pass; typecheck, oxlint, and oxfmt are
clean on the touched files.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22870?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 10:04:22 +00: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
Abdul Rahman 58fcb3cb0f drop nestjs-query IDField from standalone DTOs/entities (#22881)
## What

Replaces `@IDField(() => UUIDScalarType)` from
`@ptc-org/nestjs-query-graphql` with the native `@Field(() =>
UUIDScalarType)` (`@nestjs/graphql`) across 50 DTOs and entities that
are **not** wired to a nestjs-query auto-resolver.

This continues the incremental migration off `@ptc-org/nestjs-query`.

## Why

These 50 types only used `IDField` to type their `id` column as a UUID
scalar. Since none of them are attached to a
`NestjsQueryGraphQLModule.forFeature` resolver, `IDField` carries no
extra behavior here — it's a plain field decorator.

Co-authored-by: Abdul Rahman <abdulrahmancodes@users.noreply.github.com>
2026-07-14 17:16:59 +02:00
Charles Bochet 75d9e0b93a fix(front): constrain 2FA sign-in screens and dedupe their shared shell (#22886)
## Problem

On the card-less onboarding sign-in (`/welcome`), the **2FA
verification** step renders with a full-viewport-wide submit button.

The 2FA **verify** and **provision** forms hard-code `width: 100%` on
their root `StyledForm`. That was harmless while `/welcome` rendered
inside the `AuthModal` `medium` card, which bounded the width. Since
#22398 removed v1 onboarding, `/welcome` renders card-less under
`BlankLayout`, so nothing bounds those forms and they stretch to the
full viewport.

Other steps are **not** affected, which is why only 2FA looks wrong:
- Sign-in form: root sets `width: ONBOARDING_CONTENT_BLOCK_WIDTH;
max-width: 100%` -> capped at 440.
- SSO selection / workspace-scope: base container (`min-width: 240`, no
width) -> shrink-to-fit.
- **2FA verify / provision: `width: 100%` -> full viewport.**

## Fix

Cap the two 2FA forms at `ONBOARDING_CONTENT_BLOCK_WIDTH` (with
`max-width: 100%`), so they sit in the same block as the sign-in page
instead of forcing full-width.

## Refactor (same PR)

The verify and provision components (both introduced together in #13141)
duplicated their layout shell. Extracted the shared instruction-text and
main-content blocks into `SignInUpTwoFactorAuthenticationStyles.ts`. The
form container stays local to each component since the element differs
(a `div` in provision, a `form` in verification).

## Verification

- Reproduced the flex box-model at 1280px: `width:100%` root -> 1196px
full-width button; `width: 440px` -> 440px centered button.
- lint + format + typecheck pass on the changed files.
2026-07-14 17:16:01 +02:00
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
github-actions[bot] c8c587ba2c i18n - docs translations (#22865)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22865?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-13 20:31:17 +02:00
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
Joshua Freedman 9f75506896 feat(workflow-tools): add get_logic_function_source tool (#22835)
## What / why

The workflow agent tools let an AI **write** a CODE step's logic
function (`update_logic_function_source`) and **list** logic functions
(`list_logic_function_tools`), but there is no tool to **read** an
existing function's source.

That's fine for greenfield generation — the agent already has in context
whatever code it just wrote. But it's a real gap when editing a function
the agent did **not** author: to safely modify an existing CODE step it
has to see the current source first, and today the only ways to get it
are the frontend (`getLogicFunctionSourceCode` query) or the DB. So the
agent is forced to either guess or ask a human to paste the code.

This adds a small read tool that closes the loop, mirroring the existing
`update_logic_function_source` tool.

## How

- New `get_logic_function_source` tool that calls the existing
`LogicFunctionFromSourceService.getSourceCode({ id, workspaceId })` —
the same service method backing the `getLogicFunctionSourceCode` GraphQL
resolver the frontend already uses. No new service logic.
- Registered in `workflow-tool.workspace-service.ts` alongside
`update_logic_function_source` (the dependency
`logicFunctionFromSourceService` is already injected).
- Unit test covering the success and error paths, matching the `get-*`
tool test convention.

## Notes

- Read-only, additive; no schema or API changes.
- Naturally pairs with `update_logic_function_source`: read → edit →
write.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22835?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 17:41:00 +02:00
Harshit Raizada fd7935f343 fix(front): allow dismissing [Credits limit reached] banner (#22843)
Dismiss is session-only, he banner reappears on reload, because the
underlying "out of credits" condition is still true
closes #22774 

fix: 
<img width="1222" height="147" alt="image"
src="https://github.com/user-attachments/assets/082e68c1-d121-4e31-b261-386de8231896"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22843?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 17:34:09 +02:00
Priyanshu Bartwal 5426536004 Fix(Twenty-front): Junction field unable to display as table (#22844)
Fixes: #22783 

The junction field can now be displayed as a table.
<img width="1911" height="961" alt="image"
src="https://github.com/user-attachments/assets/2aad15af-baca-4563-85eb-ef0826008a7d"
/>
2026-07-13 17:32:18 +02:00
martmull 94192a2164 Resolve application registration logo and gallery image urls at query time (#22827)
## Context

Application registration logo and gallery image urls were baked into the
stored manifest and display columns at write time, with each source flow
doing it differently: npm catalog sync baked CDN urls, local dev sync
baked `public-assets` urls, and tarball uploads left raw manifest paths
that never displayed in the UI. The entity also carried a `logoUrl`
getter computed field.

This moves url generation to query time, the same way the workspace logo
works.

## What changed

**Read side**
- `ApplicationRegistrationAssetUrlService` builds display urls when
queried: stored files are served by fileId, absolute urls pass through
untouched, and not-yet-rehosted npm assets fall back to the registry CDN
from `sourcePackage@latestAvailableVersion`.
- The `logoUrl` getter on `ApplicationRegistrationEntity` is replaced by
`logoUrl` and `galleryImages` `@ResolveField`s on the metadata resolver,
the admin panel resolver, and a new resolver for
`ApplicationRegistrationSummary` (used by
`Application.applicationRegistration`).
- The marketplace detail/card DTOs and the public OAuth authorize DTO
(`findApplicationRegistrationByClientId`) go through the same url
builder.
- New public route `GET /file/application-registration/:id` streams
registration server files (these are instance-global marketplace assets,
also shown on the public OAuth authorize page).
`ServerFileStorageService.readServerFileById` now returns the mime type
alongside the stream.

**Write side**
- New `logoFileId` column on `applicationRegistration` (2.21 fast
instance command, constraint names match TypeORM naming), complementing
the fileIds already stored in the `galleryImages` jsonb.
- `ApplicationRegistrationAssetService` copies the manifest logo and
gallery images into instance-global server file storage, so all three
sources behave the same:
- **TARBALL**: from the uploaded package (previously only gallery images
were stored, never the logo).
- **LOCAL**: dev sync reads the already-uploaded public assets from
workspace storage (the CLI uploads files before syncing).
- **NPM**: catalog sync downloads the assets from the registry CDN.
Downloads are skipped when the package version is unchanged and the
files are already stored; failed or pending downloads fall back to CDN
urls at query time.
- Write-time url rewriting is removed
(`ManifestAssetUrlResolverService`, `resolveManifestAssetUrls`);
manifests now keep raw asset paths. Existing rows with baked absolute
urls keep working through the absolute-url passthrough, so no backfill
is needed.
- `updateFromManifest` and `upsertFromCatalog` preserve stored gallery
fileIds for unchanged paths, so installs and the hourly catalog sync no
longer clobber them.

## How it was verified

Against a local Postgres/Redis with the server running:
- Fresh database init runs the new instance command; column and FK/UQ
constraint names match TypeORM's generated names, and the CI
pending-migration check produces no diff.
- `findManyApplicationRegistrations { logoUrl galleryImages }` returns
fileId-served urls for a TARBALL registration (absolute urls passed
through), and null/[] for a LOCAL registration without assets.
- Ran `marketplace:catalog-sync` against the real npm registry: 14
packages synced, logos and gallery images rehosted from unpkg with
fileIds set; a second run re-downloaded nothing (version-unchanged
skip); `findMarketplaceAppDetail` for `twenty-linear` returns
fileId-served urls for the logo and all four gallery images.
- `GET /file/application-registration/:id` serves stored files with the
right content type (png and svg verified), 404s on unknown ids, and the
token-guarded generic `/file/:folder/:id` route still returns 403
without a token.
- Unit tests for the url builder and the assets-stored check; server
unit test suites for the application module pass; typecheck and lint
clean.
2026-07-13 17:09:34 +02:00
Brahm Lower 2b0b62235e fix: validation link for access-domains deeplinks to Invite tab (#22845)
The "validate domain" link sent in the email when adding an Access
Domain wasn't working because the link didn't deep link to the Invite
tab

URLs are now built to include the that hash property to deep link to the
target tab.

Before:
```
https://example.com/settings/members?wtdId=<id>&validationToken=<token>
```

After:
```
https://example.com/settings/members?wtdId=<id>&validationToken=<token>#invite
```

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22845?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 16:55:38 +02:00
twenty-pr[bot] ca437d374f chore: bump version to 2.22.0 (#22867)
## Summary

- Moves current version to previous versions array
- Sets TWENTY_CURRENT_VERSION to the new version
- Updates TWENTY_NEXT_VERSIONS with the next minor version
- Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same
version

## Checklist

- [ ] Verify version constants are correct
- [ ] Verify npm package versions match

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22867?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 Action Deploy <github-action-deploy@twenty.com>
2026-07-13 16:49:18 +02:00
Paul Rastoin de75be16e2 harden(server): backfill and enforce workspace.databaseSchema invariant with a check constraint (#22855)
## What & why

`core.workspace.databaseSchema` is meant to be set for every workspace
past the creation phase. It only started being written at creation time
in 2.x (dual-write since 2026-03-28, direct write since 2026-04-10);
older workspaces relied on the `1-21 backfill-datasource-to-workspace`
instance command, which never effectively ran on some instances. On
affected rows the column could be left `NULL`.

A null value on a post-creation workspace is a real integrity problem —
several paths trust the column:

- **REST API**: `hydrateRestRequest` throws `No data sources found` for
authenticated requests.
- **GraphQL API**: `getOrComputeSchemaSDL` returns `null`, so
`WorkspaceSchemaFactory` hands back an empty schema.
- **GraphQL introspection** (direct execution) returns `null`.

This PR makes the invariant impossible to silently violate, and repairs
any instance still lagging.

### On the original "No data source, skipping" logs

This investigation started from `BackfillActorSourceEnumValuesCommand`
logging `No data source for workspace <id>, skipping` at high volume.
**That symptom is not explained by this change, and this PR is not a fix
for it.** Findings:

- The workspace iterator only processes `ACTIVE` + `SUSPENDED`
workspaces, and on the affected instance all of those already have
`databaseSchema` set (only `PENDING_CREATION` rows are null, and those
are never iterated).
- `getGlobalWorkspaceDataSource()` never resolves to `undefined` (it
returns a value or throws), so a defined-schema workspace should never
hit the skip branch.
- The upgrade-aware repository proxy was investigated as a possible
cause (it can short-circuit `findOne` to `null` for entities marked
unavailable during an upgrade) and **exonerated**: `WorkspaceEntity` and
its `databaseSchema` column carry no
`@WasIntroducedInUpgrade`/`@WasRemovedInUpgrade` decorators, so
`resolveEntityShapeAtUpgradeCursor` always reports the entity available
and the column visible at every cursor.

In other words, current code should emit zero such skips for that
instance's data, so the root cause of the observed logs remains
undetermined and is tracked separately. See
twentyhq/core-team-issues#2666.

## Changes

- **Check constraint `workspace_requires_database_schema`** (the core of
this PR): enforces `databaseSchema IS NOT NULL` for any workspace past
creation (`activationStatus NOT IN ('PENDING_CREATION',
'ONGOING_CREATION')`). Declared on `WorkspaceEntity` and applied in the
slow instance command's `up()`. Safe against the creation flow:
`databaseSchema` is written in `WorkspaceManagerService.init` (right
after schema creation) long before a workspace becomes `ACTIVE`.
- **Defensive backfill** (`2-21` slow instance command): repopulates
`databaseSchema` where it is `NULL`/empty, deriving the schema name
deterministically from the workspace id (`getWorkspaceSchemaName`) and
only setting it for workspaces whose schema actually exists in
`information_schema.schemata` (so `PENDING_CREATION` rows without a
provisioned schema are left untouched, and stay exempt via the
constraint). No-op on instances already backfilled.
- `runDataMigration` runs before `up()`, so the backfill repairs legacy
rows before the constraint is enforced. Keeping both in the same slow
command (rather than a standalone fast command) guarantees the
constraint is never added ahead of the repair.
- `checkSchemaExists` gets an explicit `: Promise<boolean>` return type.

## Notes

- Backfill + constraint live in a **slow** instance command, so they
only apply on upgrades run with `--include-slow`.
- The constraint is added **`NOT VALID`**: the backfill repairs every
workspace whose Postgres schema exists, but some legacy active/suspended
workspaces (e.g. carried over from very old versions, as reproduced by
the cross-version upgrade from v1.22) have a null `databaseSchema` with
no schema to point at and are unrepairable. `NOT VALID` enforces the
invariant on all future inserts/updates without failing the upgrade on
that pre-existing corruption.
- No production request path was changed — the iterator and
`checkSchemaExists` keep trusting the (now backfilled + constrained)
column.

## Test plan

- [ ] Run `database:migrate:prod --include-slow` on an instance with
null `databaseSchema` rows; verify rows whose schema exists get
backfilled and `PENDING_CREATION` rows are left null.
- [ ] Verify the `workspace_requires_database_schema` constraint exists
on `core.workspace` and rejects nulling `databaseSchema` on an active
workspace.
- [ ] Verify a fresh workspace creation still succeeds (constraint does
not fight the `PENDING_CREATION` → `ACTIVE` transition).
2026-07-13 13:12:43 +00:00
Weiko 8e022d3c49 Fix stale relation table in field widget when switching records in side panel (#22829)
The relation table rendered by a FIELD widget in TABLE display mode kept
its jotai component states (loaded rows, virtualization maps, loading
guards, query identifiers) in instances keyed only by widget id and view
id. Since the side panel record pages share those instances across
records, switching to another record kept rendering the previous
record's related rows until an asynchronous catch-up reload landed, and
any race or error in that catch-up left the previous record's data on
screen permanently.

Scope the record-table widget's context store instance and record index
instance by target record id (and side panel surface), the same way
FieldsWidget already scopes its field list instances. Each record now
gets its own table state, so a record's rows can never appear under
another record, and loads that land after a record switch write into
their own instance instead of the visible one.

loadRecordIndexStates and setRecordGroupsFromViewGroups accept an
optional recordIndexId override so the widget view load effect can
populate the record-scoped instance instead of deriving the shared one
from object name and view id.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22829?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 14:50:42 +02:00
Paul Rastoin b2a4bb0e0c docs(apps): add Targeting System Fields page (#22856)
## What

Adds a docs page teaching app developers how to reference auto-created
**system fields** (`createdAt`, `updatedAt`, `id`, …) from views and
other entities, and makes the API it documents real by exporting
`generateDefaultFieldUniversalIdentifier` from the SDK.

## Why

System fields are provisioned by the server, so they're never declared
with `defineField()` and have no importable `universalIdentifier`
constant. Since 2.19 their universal identifier is derived
deterministically from the application id, the object id and the field
name. Hardcoding an invented id fails sync with `INVALID_VIEW_DATA:
Field metadata not found` (this is exactly what broke the
twenty-partners `createdAt` view column).

The twenty-partners app already imports
`generateDefaultFieldUniversalIdentifier` from `twenty-sdk/define`, but
the function was never exported from the SDK. This PR adds the export
and documents the pattern.

## Changes

- **New page** `data/system-fields.mdx` — "Targeting System Fields":
- Lists the 8 system fields (`id`, `createdAt`, `updatedAt`,
`deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`).
- Explains the deterministic derivation and the sync error from
hardcoding ids.
- Documents `generateDefaultFieldUniversalIdentifier({
applicationUniversalIdentifier, objectUniversalIdentifier, fieldName })`
with a full `defineView` example.
- Contrasts with standard objects (use
`STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<object>.fields.<field>.universalIdentifier`)
and notes that `name` is a default, not system, field.
- **SDK export** — new `generate-default-field-universal-identifier.ts`
wrapping the existing `getFieldUniversalIdentifier` from
`twenty-shared/application` (`name` → `fieldName`), exported from
`define/index.ts`.
- Registered the page in `docs.json` (Data group) and cross-linked it
from the Views doc.

## Notes

`node_modules` isn't installed in this environment, so `nx typecheck`
wasn't run. The wrapper is a signature-matched pass-through and the
`twenty-shared/application` subpath + `getFieldUniversalIdentifier`
barrel export were both verified to exist.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22856?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-13 12:39:06 +00:00
Félix Malfait 6201d06141 Preload Stripe.js before the onboarding payment step (#22858)
## Context

On the plan-required onboarding step, the card form was slow to appear
because Stripe.js is loaded lazily (`@stripe/stripe-js/pure`): the
script download only started once the payment page rendered, and the
PaymentElement iframe could only boot after that.

## What this does

- Adds `usePreloadStripeForPlanRequiredStep`, called once from
`OnboardingStepLayout` (the shared layout for the authenticated
onboarding step routes), so Stripe.js is already loaded by the time the
user reaches the payment step. The hook only triggers when billing is
enabled, the workspace has no subscription yet, and a publishable key is
configured, so self-hosted instances still never contact Stripe.
- Moves the memoized loader to
`settings/billing/utils/getStripePromise.ts`, shared by
`useStripePromise` and the preload hook.
- Stops caching failed script loads: previously a rejected `loadStripe`
promise stayed in the cache forever, which would have made a failed
preload permanently break the payment form. Now a later call retries
(stripe-js re-injects the script tag on retry).
- Extracts the plan-required predicate into
`onboarding/utils/getIsPlanRequired.ts`, now shared with
`useSetNextOnboardingStatus`.

The in-app add-credit-card modal is intentionally left untouched: it has
no preceding step to preload from.

## Tests

- `getStripePromise.test.ts`: dedup per publishable key, retry after a
failed load.
- `usePreloadStripeForPlanRequiredStep.test.ts`: preloads when billing
is enabled and no subscription exists; skips when billing is disabled, a
subscription exists, or the key is missing.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22858?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 14:30:38 +02:00
neo773 9f5d17d1f0 Add receipt metrics and logs to connected account sync webhooks (#22853)
Webhook deliveries from Google and Microsoft were invisible at the app
level: successful notifications produced no logs and no metrics, so
webhook-triggered syncs could not be told apart from cron polling.

Add two counters, connected-account-sync-webhook/received/messaging and
/received/calendar, mirroring the sync-job metric umbrellas, and log a
line whenever a notification triggers a sync. Unmatched subscriptions
keep their existing warn logs.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22853?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 14:28:21 +02:00
Paul Rastoin 1b168ac1f7 fix(server): gate workspaceDiscoverability behind upgrade decorator (#22818)
## Context

Needs to be patched on 2.20, will craft a 2.19 equivalent with fallback
asap ( be it won't be merged unlike this one )

Fixes #22662. Follow-up to #22423, which introduced
`workspaceDiscoverability`.

A clean 2.18.x to 2.19 upgrade breaks on login with:

```
QueryFailedError: column workspaceDiscoverability does not exist
```

`workspaceDiscoverability` was added to `WorkspaceEntity` (in #22423) as
a plain, always-selected, non-nullable column, so any workspace query
(including the auth-path `findAvailableWorkspacesByEmail` lookup) fails
as soon as the ORM selects it, before the `2.19` upgrade command that
creates the column has run. Because the Docker entrypoint is fail-open,
the API starts even if the upgrade is delayed, and users hit this on
their first login.

## Changes

- Add `@WasIntroducedInUpgrade` to `workspaceDiscoverability`,
referencing the existing `2.19.0` fast instance command that creates the
column. The upgrade-aware ORM then skips the column until the command
has actually added it, keeping login working during the upgrade.
- Keep the GraphQL `@Field` non-nullable and add a
`workspaceDiscoverability` `@ResolveField` that falls back to
`WorkspaceDiscoverability.PUBLIC` while the column is hidden, so the
resolver never returns `null` for the non-nullable field during the
upgrade window.

This mirrors the existing pattern already applied to `FileEntity.status`
and `FileEntity.applicationRegistrationId`, and the resolver-default
pattern already used for `fastModel` / `smartModel` / `logo`.

## Cherry-pick

This fix needs to be cherry-picked onto both the **2.19** and **2.20**
release branches, since affected instances are upgrading into those
versions.

## Test

- `validate-upgrade-aware-entity-decorators` and
`resolve-entity-shape-at-upgrade-cursor` unit tests pass (the referenced
upgrade command name resolves correctly).
- `upgrade-aware-repository.proxy` and
`upgrade-aware-entity-metadata.adapter` specs pass.
- `typecheck` and lint pass for `twenty-server` and `twenty-front`.
- Regenerating the GraphQL schemas produces no diff (the field stays
non-nullable).
2026-07-13 12:20:28 +00:00
github-actions[bot] 8e66411203 i18n - translations (#22861)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22861?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-13 14:15:41 +02:00
martmull 7381038452 Paginate admin panel app registrations list (#22734)
## Context

The `findAllApplicationRegistrations` query on the admin panel Apps page
(`/settings/admin-panel#apps`) loaded every application registration at
once, with search and filtering done client-side.

## Changes

**Server**
- `findAllApplicationRegistrations` now takes `limit` / `offset` /
`searchTerm` / `isPreInstalledOnly` args and returns a
`PaginatedApplicationRegistrations` object (`registrations`,
`totalCount`, `hasMore`), following the same pattern as `getQueueJobs`.
- `ApplicationRegistrationService.findAll` uses `findAndCount` with
`take`/`skip`, and moves the search (name, source package, universal
identifier via `ILIKE`) and the pre-installed filter into the SQL query,
mirroring how `getInstalledWorkspacesGlobal` filters installed
workspaces.

**Frontend**
- `SettingsAdminApps` passes the page, the debounced search term (300ms,
like the installed workspaces table), and the pre-installed toggle as
query variables instead of filtering client-side.
- Adds a Previous / Next pagination footer (25 per page) matching the
queue jobs table, shown only when there is more than one page.
- The "unconfigured first" ordering is kept within each page
(`isConfigured` is a dataloader-resolved field, so it can't be sorted in
SQL).

## Notes
- Regenerated `generated-admin/graphql.ts` follows in a subsequent
commit.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22734?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: Weiko <corentin@twenty.com>
2026-07-13 14:07:11 +02:00
Abdullah. b06f3c7f8f fix(website): query isVetted so the apps marketplace renders again (#22859)
## What

Production `/apps` renders no apps. The marketplace queries still
request `isFeatured`, but #22674 renamed that flag to `isVetted` across
the server schema (2-20 instance command renames the DB column — same
flag, same data, trust-signal semantics). Production (2.21) rejects the
query:

```
Cannot query field "isFeatured" on type "MarketplaceApp"
```

The transport throws on GraphQL errors, `fetchMarketplaceApps` catches
and falls back to `[]`, so the page renders the empty state.
`/apps/[slug]` detail pages degrade the same way.

## Fix

Rename `isFeatured` -> `isVetted` across the website marketplace module:
both queries, the API response types, the `MarketplaceApp` domain type,
and the vetted-first sort. 4 files, no behavior change beyond restoring
the data (same column, same values).

The catch-all `[]` fallback is intentionally left in place.

## Verification

- Corrected query run against `https://api.twenty.com/metadata`: returns
the 3 live apps (People Data Labs, Last contact, Call Recorder),
`isVetted: true`.
- Rename provenance confirmed: #22674 is a pure rename (paired diff,
symmetric column rename, `previousName: 'isFeatured'` marker on the
entity).
- `nx typecheck twenty-website` + `nx lint twenty-website` green.

Takes effect on the next website deploy (`force-dynamic` route, 300s
revalidate).
2026-07-13 13:56:35 +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 6185c74786 Bypass corrupted cached front-component responses with a cache-bust query parameter (#22854)
## Context

Follow-up to #22672. Users' browsers hold corrupted cached responses for
front-component request URLs from before the fix. #22672 fixed serving
and caching for newly built front components, but the corrupted entries
already sitting in browsers keep being served and need to be bypassed
programmatically.

## What changed

- `fetchComponentSourceFromNetwork` appends a constant `cacheBust=v2`
query parameter to the component request (`GET
/rest/front-components/:id/:cacheKey`). This changes the cache key, so
any corrupted response cached under the old URL is never served again
and the bundle is refetched.
- Existing query parameters on the URL are preserved; if the URL cannot
be parsed, the request falls back to the original URL unchanged.
- The presigned S3 URL from the JSON handoff is left untouched: adding a
query parameter there would invalidate its SigV4 signature.
- The `CacheStorage` layer keeps using the logical component URL as its
key, so its checksum-verified entries and hit behavior are unchanged.

## Test plan

- `fetchComponentSourceFromNetwork.spec.ts`: assertions updated to
expect the cache-busted component URL, plus a new test that existing
query parameters are preserved and one that the presigned fetch stays
unmodified; full renderer suite passes (226 tests).
- `npx nx typecheck twenty-front-component-renderer` and `npx nx lint
twenty-front-component-renderer` pass.
2026-07-13 11:34:53 +02:00
Paul Rastoin 652adc3c03 fix(server): backfill isSystemSideEffect on system fields provisioned before 2.15 (#22850)
## Context

The `isSystemSideEffect` column was introduced in **2.15** via a fast
instance command that added it with `DEFAULT false`. That stamped
`false` onto every pre-existing `fieldMetadata` row — including the 8
engine-owned system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`,
`createdBy`, `updatedBy`, `position`, `searchVector`) of every object
provisioned before 2.15, regardless of the creation path (API metadata
**and** manifest sync).

The per-workspace backfill that should have re-flagged those existing
rows was explicitly deferred as out of scope in #21673 ("PR 2") and
never shipped for `fieldMetadata`. Because `isSystemSideEffect` is
configured with `toCompare: false`, no later sync ever repaired the
stale value either.

Since **2.20** the SDK no longer declares system fields in manifests. On
an up-to-date instance, `twenty plan` against an unchanged app therefore
diffs those stale-`false` system fields as **missing from the
manifest**, and they fall through the `isSystemSideEffectFlatEntity`
exclusion in
`buildAllFlatEntityOperationRecordByMetadataNameFromFromTo`. Deletion
inference then emits them as deletes, which the validator rejects:

```
Sync failed with 144 errors
fieldMetadata: 144 errors
  1..144. FIELD_MUTATION_NOT_ALLOWED: System fields cannot be deleted
```

(144 = 8 system fields × 18 custom objects, as reported on a production
2.20 instance.)

## What this PR does

Adds a **2.21 workspace command**
(`upgrade:2-21:backfill-system-field-is-system-side-effect`) that
iterates active/suspended workspaces and flags the 8 system fields as
`isSystemSideEffect: true`.

- **Resolution by deterministic universal identifier**: for each object
× reserved system field name it recomputes
`getFieldUniversalIdentifier(applicationUID, objectUID, name)` and looks
the row up in the flat maps. This is safe (and preferable to matching by
`name`) because the 2.19 backfill already took over system field UIDs
for every application, so an author-declared field reusing a reserved
name keeps its own identifier and is never touched. An extra `isSystem`
guard warn-and-skips any mismatch.
- **All applications** are covered (installed apps, workspace custom
app, twenty-standard): the stale flag is a function of *when* a row was
provisioned, not *how*. Installed/custom apps are the acute `twenty
plan` delete trap; twenty-standard has no trap today but flagging is a
zero-diff no-op (`toCompare: false`) and a prerequisite for the
end-state ownership invariant.
- **`name` is intentionally excluded**: the 2.20 slow instance command
deliberately flipped it to `false` (caller-provided default, not
engine-owned); re-flagging it would undo that migration.
- Supports `--dry-run`, updates only the collected rows, and invalidates
the `flatFieldMetadataMaps` workspace cache after the write (a raw
repository update does not invalidate it).

## Related

- Resolves the pre-2.15 regression tail of
twentyhq/core-team-issues#2635
- Follow-up to twentyhq/core-team-issues#2642 (system field side-effect
engine migration)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22850?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: Weiko <corentin@twenty.com>
2026-07-13 08:55:54 +00:00
Paul Rastoin b94e38c2d9 fix(server): stabilize flaky app-install workspace-version gate test (#22851)
## What

The integration test
`failing-app-installation-workspace-version.integration-spec.ts` is
flaky depending on the shape of the upgrade sequence, especially right
after a version bump. It intermittently fails at upload time with:

```
App requires Twenty server >=2.21.0 but this server is 2.20.0.
(SERVER_VERSION_INCOMPATIBLE)
```

## Why

The test mixed two different version sources:

- The upload-time check (`validateServerCompatibility`) compares the
app's required version against the **instance** inferred version, i.e.
the last attempted instance command (`workspaceId IS NULL`, via
`getInferredVersion`).
- The test's `beforeAll` instead derived the required version from the
**workspace** cursor.

These agree most of the time but diverge right after a version bump
whose newest upgrade segment ends in workspace-scoped commands and adds
no new instance command. In that state the seeded workspace cursor sits
at the new version while the instance is still at the previous one. The
test then uploads an app requiring `>=newVersion`, which fails the
instance gate at upload time before the workspace gate under test is
ever reached.

## How

Derive the gate version in `beforeAll` from the last attempted instance
command, mirroring exactly what `getInferredVersion()` uses. The
required version is then always `>=` the instance's own version, so the
upload passes; injecting that same command as a failed workspace attempt
drops the workspace to the previous completed version, so the install
reliably hits the workspace gate and returns
`WORKSPACE_VERSION_INCOMPATIBLE` as the snapshot expects. This holds
regardless of whether the newest version's segment ends in an instance
or workspace command.

No production code changed; the fix is confined to test setup logic.


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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22851?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 10:33:45 +02:00
github-actions[bot] bbe9886274 chore: sync AI model catalog from models.dev (#22842)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22842?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: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-07-11 08:42:36 +02:00
github-actions[bot] 983f03adbe i18n - translations (#22833)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-10 20:36:28 +02:00
neo773 b0dc637dbd Throw proper error on duplicate emailing domain (#22790)
Adding an emailing domain that already exists blew up with a raw
QueryFailedError and the client just saw a generic "An error occurred".
The unique index on domain is global, so the workspace-scoped existence
check never caught rows owned by another workspace.

Now the check is unscoped and throws an EmailingDomainException mapped
to CONFLICT with a proper user-facing message, in both the
createEmailingDomain mutation and the email group channel flow. Also
dropped the hardcoded catch-all snackbar on the new channel page so
server messages actually reach the user.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22790?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 20:28:34 +02:00
Paul Rastoin cb95410a51 ci: test twenty-apps install against latest dockerhub and local server + new trigger (#22636)
## Why

App installability can silently regress from two directions, and today
CI only covers one of them:

1. A **server** change (about to merge from the monorepo) breaks the
ability to install the **current public apps** — a
backward-compatibility regression users would hit on upgrade.
2. An **app** change breaks against a server **built from the current
monorepo files** (not just the last published image), so the app and the
upcoming server drift apart before either ships.

Both are compatibility guarantees between the server and the app
catalog. Today they are only tested from the app side, against the
latest published image. This PR makes CI enforce the contract from both
sides:

- Any server PR must keep **every** current public app installable.
- Any app PR is exercised against both the **released** server (its
integration suite — what users run today) and the **upcoming**
(monorepo) server (integration plus deploy + install).

## What

Shared building blocks so both CIs exercise the same paths instead of
duplicating them:

- **`spawn-twenty-server`** (composite action) — returns a running
server (`server-url` + `api-key`) from either the latest published
Docker Hub image or a server built from the monorepo. Both sources
expose the same contract, so callers never branch on how the server came
up.
- **`test-twenty-app`** (composite action) — exercises one app against a
given server, delegating deploy + install to the shared
`deploy-twenty-app` / `install-twenty-app` actions.
- **`discover-apps`** (reusable workflow) — the single source of truth
for the app matrix. Parameterized by `scope` (`public` vs
`internal-and-public`) and `changed-only`, so both CIs derive their
matrix from the filesystem instead of a hand-maintained list. Discovery
stays automatic: a newly added public app is picked up with no CI edit,
which is what keeps the "every public app" guarantee honest.

Wired in:

- **CI Server** gains a `server-apps-install-smoke` matrix that installs
every public app (`discover-apps` with `scope: public, changed-only:
false`) against the about-to-merge server, gated in
`ci-server-status-check` so a regression blocks merge.
- **CI Twenty Apps** discovers changed apps (`scope:
internal-and-public, changed-only: true`) and runs each against both
server sources — the released image and the monorepo build.

## Why the coverage differs per side (not "always everything")

`test-twenty-app` has three explicit modes —
`installation-and-integration-test` (integration + deploy + install),
`integration-test-only` (suite only), `installation-only` (deploy +
install only) — because the useful signal depends on what actually
changed:

- **App PR against the monorepo server →
`installation-and-integration-test`.** The app changed, so run its whole
suite against the upcoming server, install included.
- **App PR against the released server → `integration-test-only`.**
Checks the app's own suite against what users run today; install against
the released image is left to the SDK e2e path.
- **Server PR → `installation-only`, across all apps.** The apps did not
change; the only question is "can each one still be installed." Running
every app's full integration suite on every server PR would be far
slower and largely redundant. Installation-only keeps this broad (the
whole catalog) and cheap enough to always run and block merge.

The tradeoff is deliberate: broad but shallow where nothing in the app
changed, deep where it did.

## Notes / trade-offs

- On app-only PRs the `local` source pays a full server build per app
(the `server-build` cache is only warm on server PRs). Could be
optimized later with a shared warm-up job.
- SDK-local (Verdaccio) install testing stays in
`ci-create-app-e2e-minimal`; this PR's `local` source targets the server
build.

<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22636?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 19:18:53 +02:00
Paul Rastoin 3614183200 fix(server): stabilize app install version-gate integration test (#22826)
## Why

The `failing-app-installation-workspace-version` integration suite fails
on CI (e.g. [this
run](https://github.com/twentyhq/twenty/actions/runs/29105697459/job/86405891168)):

```
App requires Twenty server >=2.21.0 but this server is 2.20.0.
subCode: SERVER_VERSION_INCOMPATIBLE
```

The test uploads an app requiring `>=${TWENTY_CURRENT_VERSION}` and
expects the install to be rejected by the **workspace** version gate.
But after the `2.21.0` version bump, `TWENTY_CURRENT_VERSION` (`2.21.0`)
moved ahead of the latest instance upgrade command (`2-20`, so
`getInferredVersion()` returns `2.20.0`). The tarball upload runs the
**instance** server-compat check first, which rejects `>=2.21.0` against
a `2.20.0` server before the workspace gate under test is ever reached.

The sibling sync test is unaffected because sync only validates
workspace compatibility, not the upload-time instance check.

## What

Derive the required version range from the version the instance actually
reached (the workspace upgrade cursor via
`extractVersionFromCommandName`) instead of the drifting
`TWENTY_CURRENT_VERSION` constant. This way:

- The upload passes the instance server-compat check (server satisfies
`>=<current version>`).
- The workspace, which resolves one version behind after the injected
failed cursor, still fails the workspace gate, producing the expected
`WORKSPACE_VERSION_INCOMPATIBLE` error.

The error assertion keeps using the normalized snapshot
(`scrubSemverVersions`), so the concrete version numbers do not leak
into the snapshot and future version bumps won't churn it.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22826?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 16:33:25 +00:00
Raphaël Bosi c9d84ba7f0 Disable install button in app install onboarding when no app is selected (#22822)
In the app installation onboarding step, the Install button was always
clickable, even with no app selected. Clicking it with an empty
selection ran the completion flow with zero apps, which is equivalent to
skipping.

Now the button is disabled until at least one app is selected (in
addition to staying disabled while completing). The Skip button remains
available for users who don't want to install anything.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22822?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 17:55:00 +02:00
github-actions[bot] c0f5a28d5d i18n - docs translations (#22824)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22824?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-10 17:23:57 +02:00