Commit Graph

13232 Commits

Author SHA1 Message Date
Abdullah. be8102c4a4 fix: evict js-yaml 3.x via front-matter patch + scoped resolutions (#22312)
## Summary

Closes the last open Dependabot alert on `main` — [alert
1504](https://github.com/twentyhq/twenty/security/dependabot/1504)
(GHSA-h67p-54hq-rp68 / CVE-2026-53550, js-yaml merge-key
quadratic-complexity DoS; medium, dev scope).

The 4.x js-yaml copies are already pinned to 4.2.0 (the seven
`@mintlify/*` + `@verdaccio/config` resolutions). The remaining
vulnerable copy was **js-yaml 3.14.2**, held by two `^3.13.1` consumers
with no fixed upstream release:
- `@istanbuljs/load-nyc-config@1.1.0` (jest coverage)
- `front-matter@4.0.2` (mintlify docs tooling — EOL, latest is 4.0.2)

## Approach

Both are forced to **js-yaml 4.2.0** via scoped resolutions, which
evicts the 3.x copy entirely:

```jsonc
"front-matter/js-yaml": "4.2.0",
"@istanbuljs/load-nyc-config/js-yaml": "4.2.0",
```

- **load-nyc-config** already calls `yaml.load` (present +
safe-by-default in 4.x), so its pin alone works.
- **front-matter** crashed on js-yaml 4.x because its default loader
called the removed `safeLoad`, so it's also **patched**
(`.yarn/patches/front-matter-npm-4.0.2-e1cc0efa69.patch`):

  ```diff
  -  var loader = allowUnsafe ? parser.load : parser.safeLoad
  +  var loader = parser.load
  ```

On 4.x `load` is already safe-by-default and there's no full/unsafe
schema, so the `allowUnsafe` ternary is dead.

**Why the version move is a separate resolution, not folded into the
patch:** a `yarn patch` only rewrites a package's *files* — it does
**not** change the resolved dependency graph. Bumping js-yaml inside the
patched `package.json` is ignored by resolution (verified:
front-matter@patch still pulled 3.14.2 until the explicit pin was
added). This is the repo's first *transitive* patch; like every other
transitive override it lives in root `resolutions`. Documented in the
`//resolutions` note.

## Verification

- js-yaml 3.x **fully evicted** — the tree resolves js-yaml to **4.2.0
only** (no `^3.13.1` descriptor remains).
- `yarn install --immutable` passes.
- front-matter parses real docs front-matter correctly on 4.2.0 (unit
smoke test).
- **`mintlify validate` passes** — the docs toolchain builds with the
patched front-matter.
2026-06-29 20:14:17 +05:00
nitin f263edd122 Add call recorder oom diagnosis logs (#22315)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22315?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-29 17:14:06 +02:00
github-actions[bot] 2de681da72 i18n - docs translations (#22317)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22317?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-06-29 17:12:55 +02:00
souheyl gouadria 1b2f2c71c3 Move filter group destruction after filter operations (#22248)
### The current order is:

1. Create filter groups
2. Update filter groups
3. Destroy filter groups ← **happens here**
4. Clean up store (cascade)
5. Create/update/delete filters (which may reference groups just
destroyed)

The fix is to move filter group destruction after filter operations, so
filters that reference those groups get created/updated/deleted first.

### after fix : The persistence order is now:

1. Create filter groups
2. Update filter groups
3. Create/update/delete view filters (these can safely reference groups
that still exist)
4. Destroy filter groups (only after all filter mutations are done)
5. Clean up store (cascade-deleted filters

**root cause :** step 4 happened before step 3, so filter
creates/updates would reference groups that had already been deleted in
the same save cycle ; causing the backend to fail with "Migration
execution failed" when it couldn't resolve the `viewFilterGroupId
`foreign key.

this fixes the bug : #21351

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22248?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: Souheyl Gouadria <souheyl.gouadria@medius.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-29 16:54:15 +02:00
Marie 4eb28b73c7 fix(server): normalize legacy index names (command) (#22053)
## TL;DR

Adds a workspace upgrade command that normalizes index names to the
current v2 deterministic naming convention (IDX_ prefix). This will
close https://github.com/twentyhq/twenty/issues/21383 : uniqueness
constraint cannot be disabled (for users who set it before v2
determinist naming is enforced).

### Background

The deterministic index name embeds the table name, columns, uniqueness
and where clause. The naming convention changed on **2025-09-23**
(#14567 - added the `IDX_`/`IDX_UNIQUE_` prefix and folded the table
name into the hash). Index rows created before that kept their old name
in `core."indexMetadata"`, and nothing rewrites it (only targeted
phone/relation rebuilds got new names).

Code paths that locate an index by recomputing its expected name then
miss these legacy-named rows. The most visible symptom is #21383:
toggling a field's `isUnique` from `true` → `false` recomputes the
expected unique-index name, fails to find the legacy-named index, and
silently no-ops — so uniqueness can't be disabled.

### What the command does

Per workspace, for each index:
- recomputes the expected name with the same generator the app uses
(`generateFlatIndexMetadataWithNameOrThrow`);
- if the stored name differs → **rename** it (metadata `UPDATE` + a new
metadata-only `ALTER INDEX … RENAME`, which preserves the unique
constraint with no rebuild/lock);
- if a correctly-named twin already exists (the legacy +
freshly-generated duplicate case) → **drop the redundant** one (physical
+ metadata, field rows cascade) and keep the canonical;
- invalidates the metadata cache so the running app + `isUnique`
derivation reflect the new names.

Honors `--dry-run`, wraps writes in a transaction (rolls back on error),
and skips (with a warning) any single index whose name can't be
recomputed so it can't abort the whole workspace.

### Notable changes
- New `renameIndex` on `WorkspaceSchemaIndexManagerService` (`ALTER
INDEX IF EXISTS … RENAME`).
- Planning logic extracted into a pure, unit-tested util
(`planIndexNameNormalization`).
2026-06-29 14:04:40 +00:00
github-actions[bot] 6fa803edc2 i18n - docs translations (#22307)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22307?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-06-29 15:49:20 +02:00
Abdul Rahman 61d094b185 chore(docs): exclude code blocks and icon frontmatter from Crowdin translation (#22304)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22304?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-29 18:33:31 +05:30
martmull d1854bc7e9 Add marketplace catalog synchronization to admin panel (#22260)
## After

<img width="1476" height="658" alt="image"
src="https://github.com/user-attachments/assets/25a042dd-eabf-4b71-9872-d3b627104634"
/>


## Summary
This PR adds the ability for admins to manually synchronize the
marketplace application catalog from the admin panel. It introduces a
new mutation endpoint and UI controls to trigger catalog synchronization
with user feedback via snackbar notifications.

## Key Changes
- **Frontend (SettingsAdminApps component)**:
  - Added `useSnackBar` hook for user feedback on sync success/failure
- Imported `useMutation` from Apollo Client to handle the sync operation
  - Added `IconRefresh` and `Button` imports for the sync UI control
- Created `handleSyncCatalog` function that triggers the mutation,
refetches app registrations, and displays appropriate snackbar messages
- Added a new "General" section with a "Synchronize catalog" button
above the existing app registrations table
  - Button shows loading state and is disabled while sync is in progress

- **Backend (AdminPanelResolver)**:
- Added `syncMarketplaceCatalog` mutation that queues a
`MarketplaceCatalogSyncCronJob` via the message queue
- Uses `@InjectMessageQueue` decorator to inject the cron queue service
- Includes job deduplication via `id: 'marketplace-catalog-sync'` to
prevent multiple pending sync jobs
  - Protected with `@UseGuards(AdminPanelGuard)` for admin-only access

- **GraphQL Schema**:
  - Added `SyncMarketplaceCatalog` mutation type definition
  - Generated corresponding TypeScript types and mutation document

- **New Files**:
  - Created `syncMarketplaceCatalog.ts` GraphQL mutation document

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22260?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-29 12:58:34 +00:00
neo773 66edd96213 microsoft webhook ttl fix (#22300)
In dev testing it worked fine but on production the TTL is failing we
add a 1 hour buffer for safety

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22300?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-29 18:09:07 +05:30
Weiko a215d3cbdf fix: prevent crash when leaving dashboard edit mode with side panel open (#22292)
## Problem

When creating or editing a dashboard and opening a page layout side
panel (clicking the **"Add widget"** box, or editing a widget via grid
edition), clicking another nav item like **"Opportunities"** crashes the
whole app ("Sorry, something went wrong").

### Steps to reproduce
1. Go to `/objects/dashboards` and click **"+ Add new"** to create a
dashboard.
2. Click the **"Add widget"** box (or add a widget via grid edition and
open its settings).
3. Click another nav item such as **"Opportunities"**.
4. The app crashes.

## Root cause

The side panel stays mounted during its close animation, but the main
context store has already switched to the new page (the record index has
no single targeted record). The still-mounted page layout side panel
page (`SidePanelPageLayoutDashboardWidgetTypeSelect`, chart settings,
etc.) re-renders and calls `usePageLayoutIdFromContextStore`, which
throws `Error: Only one record should be selected`. With no local error
boundary, the throw propagates to the top-level boundary and crashes the
app.

## Fix

`SidePanelRouter` now skips rendering page layout side panel pages
whenever the main context store has no single targeted record — the same
condition `usePageLayoutIdFromContextStore` requires to not throw.
During navigation the panel closes cleanly instead of crashing.

This is safe because `usePageLayoutIdFromContextStore` unconditionally
throws without a single-record selection, so the guard can only skip
pages that would otherwise crash — it cannot break a currently-working
flow. The guard uses the existing `isPageLayoutSidePanelPage` helper, so
it covers all page layout side panel pages (widget type select, chart /
iframe / record table settings, record page field settings, etc.).

## Testing

- Reproduced the crash in the running app, applied the fix, and
confirmed navigating to Opportunities from both the **widget type
select** and the **chart settings** panels now lands on the
Opportunities list with no console or page errors.
- `oxlint --type-aware` and `nx typecheck twenty-front` both pass.
2026-06-29 14:14:42 +02:00
github-actions[bot] f470e271fa i18n - docs translations (#22297)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-29 14:01:10 +02:00
github-actions[bot] 73b0d55350 i18n - docs translations (#22296)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-29 14:00:00 +02:00
Raphaël Bosi db7d8172f7 Add v2 onboarding invite team page (#22229)
<img width="3024" height="1500" alt="CleanShot 2026-06-26 at 18 09
47@2x"
src="https://github.com/user-attachments/assets/e91f30a5-2763-42a0-9abf-d9fa8400870c"
/>


Adds the v2 onboarding **Invite team** page (`INVITE_TEAM`), shown right
after the create-profile step for the onboarding-v2 cohort. It renders
full-screen under `BlankLayout` via the shared `OnboardingV2Layout`,
matching the Figma (340px column, email inputs with inline remove, dark
Invite, Skip).

Reuses all v1 invite-team logic via a new `useInviteTeam` hook (v1
`InviteTeam` now consumes it too; its UI is unchanged). Routing mirrors
`SyncEmailsV2`/`CreateProfileV2`: new `AppPath.InviteTeamV2`, lazy
route, and an `isOnboardingV2`-gated branch in
`usePageChangeEffectNavigateLocation` (+ tests and a Storybook story).

No backend changes.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22229?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-29 13:53:07 +02:00
Miguel 921c528cc3 feat(helm): add nodeSelector, tolerations and DNS overrides to server and worker (#22233)
Closes #22250

## What

Adds four standard pod spec fields to both server and worker Deployments
of the Helm chart, exposed under `server.*` and `worker.*` in
`values.yaml`:

- `nodeSelector`
- `tolerations`
- `dnsPolicy`
- `dnsConfig`

## Why

Self-hosters who run Twenty on clusters with dedicated nodes, taints, or
custom DNS requirements currently need to fork the chart or patch
rendered manifests with Kustomize. These are the standard pod spec
fields supported by virtually every other community chart (Bitnami,
prometheus-community, cert-manager, etc.) and are commonly needed in
production setups.

## How

Uses the same `{{- with }}` pattern already present in the chart (e.g.
`extraEnv`, `extraVolumeMounts`), so empty defaults skip rendering
entirely:

```yaml
{{- with .Values.server.nodeSelector }}
nodeSelector:
  {{- toYaml . | nindent 8 }}
{{- end }}
```

## Backward compatibility

Fully backward compatible. Defaults are empty:

```yaml
server:
  nodeSelector: {}
  tolerations: []
  dnsPolicy: ~
  dnsConfig: {}
```

`helm template` output is bit-identical to the previous version for any
existing install.

## Schema note

`dnsPolicy` is typed as `[string, null]` and the enum includes `null` so
the empty default (`dnsPolicy: ~`) passes validation. The `{{- with }}`
guard treats null as falsy and renders nothing.

## Validation

- `helm lint` passes
- `helm template` with default values produces bit-identical output to
before this PR
- `helm template` with values set renders the four fields correctly on
both server and worker
- Live install validated on a multi-node Kubernetes cluster, pinning
Twenty to a dedicated tainted node with custom DNS (`ndots: 1`); server
and worker scheduled and started successfully

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22233?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: Charles Bochet <charles@twenty.com>
2026-06-29 13:00:42 +02:00
Parship Chowdhury 6e319283c4 fix: Vite 8/Rolldown build warnings in library packages (#22205)
Clean up Vite 8/Rolldown build warnings that showed up during yarn
start:
- `twenty-client-sdk`: `relativeImportPath.ts` now imports `node:path`,
so the generate bundle treats it as a Node external instead of stubbing
it for the browser.
- Remove rollup’s `interop: 'auto'` from CJS output options - Rolldown
don’t support it and was showing `Invalid key: Expected never but
received "interop"`.
- Replaced deprecated `inlineDynamicImports: true` with `codeSplitting:
false` in the worker config.

References:
- https://v7.vite.dev/guide/rolldown#option-validation-warnings
-
https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility

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

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-29 12:59:36 +02:00
Etienne 56deba351b feat(ai): reliable bulk data import via code-interpreter (#22209)
## Summary

Makes AI-assisted bulk data import (CSV/Excel/spreadsheets) reliable and
token-efficient by letting an entire import run inside a single
code-interpreter call, with a persistent sandbox session and server-side
bulk helpers. Also includes supporting improvements to attachment
handling, upsert reporting, and field-permission error messages.

## Changes

### Code interpreter
- **Persistent per-session kernel** in `LocalDriver`: a long-lived
Python process per `sessionId` keeps variables, imports, and files alive
across calls (matching E2B behavior). Falls back to the existing
ephemeral per-call path when no session is provided. CAN BE REMOVED,
INTERESTING FOR DEV X
- Idle watchdog that self-terminates the kernel, configurable via the
new `CODE_INTERPRETER_IDLE_TIMEOUT_MS` config variable; the process also
exits on parent shutdown (EOF on control fd). CAN BE REMOVED,
INTERESTING FOR DEV X
- New `bulk_upsert` and `lookup_by` helpers on the sandbox `twenty`
object for idempotent batched writes (≤200/batch) and bounded
relation-ID resolution.

### Records
- `upsert_many_*` now reports a `created` / `updated` / `total` split in
its result and log line (new `isFreshlyCreatedRecord` util).

### AI chat
- `replaceUnsupportedFileParts`: user-attached files whose MIME type the
model can't handle natively (and that aren't code-interpreter-supported)
are downgraded to a descriptive text note instead of being sent as
unsupported file parts. Modality→MIME mapping drives native support
detection.
- Finalize dangling tool parts before `convertToModelMessages` to avoid
malformed model messages.
- Extracted shared types/constants for code-interpreter file extraction.

### Permissions
- Field permission-denied exceptions now include the field name and
entity name for easier debugging.


### Skill docs
- Added the bulk-import recipe 

## To do in following PR
- [ ] Skill command migration

## Test plan
- [x] Unit tests for `getNativeMimeTypesForModalities` and
`replaceUnsupportedFileParts` pass
- [x] Run a bulk import (>50 rows) end-to-end through the code
interpreter and verify a single sandbox call handles read → resolve
relations → upsert → summary
- [x] Verify session persistence: define a variable in one call, use it
in the next within the same session
- [x] Verify the kernel self-terminates after
`CODE_INTERPRETER_IDLE_TIMEOUT_MS`
- [x] Verify unsupported attachments are replaced with a text note for
models lacking the modality
- [x] Verify `upsert_many_*` returns correct created/updated counts
- [x] Verify field-restricted role triggers a permission error naming
the field and entity
- [ ] Test with
[hotel_business.xlsx](https://github.com/user-attachments/files/29376307/hotel_business.xlsx)
and simple "import record" prompt

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22209?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-29 09:54:27 +00:00
Etienne 02d62c4175 fix(ai-tools): make navigate_app tool schema a valid object root for direct model binding (#22284)
## Problem

Using an AI Agent step in a workflow fails with:

> Invalid schema for function 'navigate_app': schema must be a JSON
Schema of 'type: "object"', got 'type: "None"'.

## Root cause

The `navigate_app` tool declared its `inputSchema` as a top-level
`z.discriminatedUnion('type', [...])`. When serialized via
`toToolJsonSchema`, a discriminated union produces a **root-level
`anyOf`** with no top-level `"type"`:

```json
{ "anyOf": [ { "type": "object", ... }, ... ] }

<!-- This is an auto-generated description by cubic. -->
<a href="https://cubic.dev/pr/twentyhq/twenty/pull/22284?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-29 09:03:16 +00:00
Félix Malfait 71370d9e66 fix(server): dedupe in-flight application translation catalog loads (#22285)
## Problem

After v2.17.0 shipped app-owned metadata translations (#22235), `POST
/metadata` got slower for workspaces with translation-carrying apps
installed. Sentry flagged it as an N+1
(`performance_n_plus_one_db_queries`): a single request fires **many**
concurrent identical queries:

```sql
SELECT … FROM "core"."applicationTranslation" WHERE "applicationRegistrationId" = $1 AND "deletedAt" IS NULL
```

Span aggregates since the deploy: **272** such spans, **avg 74 ms**,
**p95 556 ms**, **max 694 ms**, all on `POST /metadata`. HTTP stays 200
— it's latency, not errors.

Reported via Sentry `TWENTY-SERVER-HQY` (`twenty-v7`).

## Root cause

`ApplicationTranslationCacheService` kept a TTL value cache but had **no
in-flight de-duplication**. The object/field metadata resolvers call
`getCatalog` directly, per record. On a cold/expired (30 s TTL) cache,
many fields of the same app resolve concurrently, all miss, and each
fires its own `repository.find` — a classic cache **stampede**, which
queues on the connection pool and produces the 556–694 ms tail. Each
read also pulls the full per-locale `messages` JSON, so the redundant
reads aren't free.

## Fix

Rebuild the service on the shared **`PromiseMemoizer`** primitive — the
same one `WorkspaceCacheService` and `CoreEntityCacheService` already
use. It pairs the 30 s TTL value cache with a `pending` promise map, so
concurrent callers for the same registration **share a single in-flight
query** instead of stampeding. Per-request query count for a given app
goes from N → 1.

- Public API (`getCatalog` / `invalidate`) is unchanged — no caller
touched.
- `invalidate` now clears via `memoizer.clearKeys(...)` (clears both the
cached value and any in-flight read), matching `WorkspaceCacheService`.
- Adds a unit test asserting 10 concurrent `getCatalog` calls trigger
exactly **one** `repository.find`, plus cache-hit / empty-locale /
post-invalidation reload cases.

## Notes

- Process-local 30 s TTL behaviour is unchanged (deliberate; no
cross-process invalidation), this only removes the redundant concurrent
reads.
- Verified formatting with oxfmt locally; couldn't run the server test
suite in this environment, so relying on CI for typecheck/test.

https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22285?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-29 10:08:40 +02:00
greymoth 13f80f0d95 fix: ignore IME composition Enter in chat-thread and attachment rename inputs (#22270)
## What's this PR doing?

Two inline rename inputs run their action on `Enter` without ignoring
the `Enter` that confirms an IME composition:

- `AiChatThreadListItem` (renaming an AI chat thread). It also calls
`preventDefault()`, so the composition-commit `Enter` is swallowed and
the half-typed title gets saved.
- `AttachmentRow` (renaming an attachment). The same `Enter` saves the
unfinished name.

When you type with an IME (Japanese, Chinese, Korean), the first `Enter`
after typing confirms the candidate text rather than submitting, so
these handlers fire with text the user hasn't finished entering.

## Why

The codebase already guards this where keyboard handling goes through
`useHotkeysOnFocusedElement` (`if (keyboardEvent.isComposing ||
keyboardEvent.keyCode === 229) return`), and the inline inputs that
don't use that hook add the same check themselves: see
`SettingsAccountsBlocklistInput`, `SettingsDevelopersApiKeysNew`, and
the sign-up workspace forms. These two rename inputs were just missing
it.

## How

Add the same `isComposing || keyCode === 229` guard before the `Enter`
branch. For input without an IME, `isComposing` is `false` and `keyCode`
is `13`, so the rename-on-Enter behavior stays the same. This only skips
the action on the composition-commit key.

I checked the change against the repo's Prettier config locally. I
couldn't add a unit test because jsdom doesn't dispatch real composition
events (`isComposing` stays `false`), so it can't reproduce the
keystroke. Happy to add an e2e test if that's preferred.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22270?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-29 08:08:11 +02:00
github-actions[bot] 68f4cf269d i18n - docs translations (#22281)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22281?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-06-29 07:22:03 +02:00
github-actions[bot] 655b788e8d i18n - docs translations (#22280)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22280?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-06-29 06:48:09 +02:00
github-actions[bot] 8cee8d7212 i18n - docs translations (#22279)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-29 05:04:26 +02:00
github-actions[bot] 7b17cac772 i18n - docs translations (#22273)
Created by Github action

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22271?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-06-29 03:53:34 +02:00
Abdullah. e69c3ae5ce fix(website): bump @opennextjs/cloudflare to 1.20.0 (R2 deploy on Node 24) (#22266)
## What

The twenty-infra **"Deploy Website"** workflow has failed every run
since 06-26 — at OpenNext's R2 incremental-cache step, **not** the
build:

```
Failed to provision remote R2 bucket "twenty-website-cache-dev"
for binding "NEXT_INC_CACHE_R2_BUCKET":
Failed to check whether bucket exists: … Premature close
```

## Root cause

`@opennextjs/cloudflare`'s `ensureR2Bucket()` calls the Cloudflare SDK's
`r2.buckets.get()`. On **Node 24** (which the deploy pins) undici
truncates the **gzip-compressed** Cloudflare API response → `Premature
close`. The SDK's own retries don't help (it's systematic, not flaky),
and pre-creating the bucket doesn't help (it always `.get()`s first).

This is **pre-existing and unrelated to the multi-locale change**
(#22257): the *Build Worker* step succeeds, and the identical error
appears on 06-26 runs (two days before that merged).

## Fix

opennext **1.20.0** fixes this precisely — it passes `defaultHeaders: {
"Accept-Encoding": "identity" }` to the Cloudflare SDK client, so the
API returns **uncompressed** responses (no decompression → no premature
close). **Node 24 is kept**, and no twenty-infra change is needed — the
deploy runs `twenty`'s own `npx opennextjs-cloudflare`, so bumping the
dep here is enough.

`^1.0.0 → ^1.20.0`. **1.20.0, not the latest 1.20.1**, because the
repo's `npmMinimalAgeGate: 3d` still quarantines 1.20.1 (published
06-26); 1.20.0 (06-25) is past the gate and carries the same fix.

## Verification

- Resolved to `1.20.0`; confirmed `Accept-Encoding: identity` is in the
installed `dist/cli/utils/ensure-r2-bucket.js`.
- `nx typecheck twenty-website` green (OpenNext config API unchanged
across the bump).
- Diff is just `package.json` + `yarn.lock`.

⚠️ Full confirmation needs a **Deploy Website** run (exercises the build
+ the R2 provision step), which I can't trigger — please re-run it after
merge.
2026-06-28 16:18:31 +00:00
github-actions[bot] 0569c490fd i18n - website translations (#22264)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22264?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-06-28 16:06:10 +00:00
github-actions[bot] f8a9a2b249 i18n - website translations (#22263)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22263?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-06-28 13:31:10 +00:00
twenty-pr[bot] 627da33424 chore: bump version to 2.18.0 (#22256)
## 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/22256?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-06-28 15:00:41 +02:00
Abdullah. ddb3abfb23 fix(website): config-only Crowdin pull to match short-code catalogs (#22262)
## What

`website-i18n-pull.yaml` passed inline `source` + `translation:
'…/%locale%.po'` to the Crowdin action *on top of* the config file.
After #22257 migrated the website to short-code catalogs
(`ar.po`/`es.po`, and `crowdin-website.yml` → `%two_letters_code%`),
that inline `%locale%` resolves to **full-code** filenames
(`ar-SA.po`/`es-ES.po`) that no longer exist in the repo — so the pull
never updates the real catalogs.

## Fix

Drop the inline `source`/`translation` so the step is **config-only**,
driven by `.github/crowdin-website.yml` (`%two_letters_code%`) —
matching the front/app pull (`i18n-pull.yaml`), which is config-only and
works.

## Also (ops, not in this diff)

The pull doesn't run against `main` — its first step is `git checkout -B
i18n-website origin/i18n-website`, so it operates on the long-lived
`i18n-website` branch (same pattern as `i18n` for front and `i18n-docs`
for docs). That branch was still at the **pre-#22257** layout (full-code
`.po`, `%locale%` config, old 3-locale list), so it was reset to `main`
to carry the new short-code structure. Once this PR merges, a pull run
will land translations into the correct `ar.po`/`es.po`/… catalogs.
2026-06-28 14:43:37 +02:00
Abdullah. 012af11d77 feat(website): ship all documentation locales (multi-locale site) (#22257)
## What

The marketing site now serves every language the **documentation** ships
— 14 locales (`en, fr, ar, cs, de, es, it, ja, ko, pt, ro, ru, tr, zh`),
up from 3 (`en, es, fr`).

## How

- **Single source of truth.** `WEBSITE_LOCALE_LIST` derives directly
from `DOCUMENTATION_SUPPORTED_LANGUAGES` (`twenty-shared/constants`).
Add a documentation language → it flows to the website automatically.
- **Off `APP_LOCALES` entirely.** The website locale type is now
`DocumentationSupportedLanguage` (short codes), so a locale **is** its
URL segment — no short↔full mapping, and no `pt-BR`/`zh-CN` ambiguity to
resolve.
- **Removed the indirection this exposed** (it only existed because
`AppLocale` was a superset of the deployed set):
- `locale-to-url-segment` / `locale-by-url-segment` (locale == segment)
- `get-locale-messages` pass-through → callers read `MESSAGES_BY_LOCALE`
directly
- the `messages-by-locale` runtime guard → a total
`Record<DocumentationSupportedLanguage, Messages>` (a missing catalog is
now a **compile** error, not a runtime throw)
- `isWebsiteLocale` → a `string → DocumentationSupportedLanguage` type
guard
  - the vestigial language-code `split('-')` in `locale-display-name`

## Catalogs

- Renamed `es-ES → es`, `fr-FR → fr`; added 11 new locales (untranslated
for now → **English fallback**).
- `crowdin-website.yml` switched to `%two_letters_code%`.
- Regenerating catalogs also synced `en.po` with current source
(`Boolean` / `Date & Time` / removed `Fields widget` from the
already-merged #22249).
- `ci-website` is unchanged — no `lingui:compile` step added; catalogs
stay committed.

## Testing

- `typecheck` · `lint` (check-conventions + oxlint + oxfmt) · 347/347
tests — all green. PR CI runs exactly lint + typecheck + test.

## Follow-up (out of repo)

Enable the 11 languages on **Crowdin project 4** so `website-i18n-pull`
backfills real translations. Until then, the new locales render with
English fallback (correct behavior).
2026-06-28 14:39:59 +05:00
Félix Malfait d81b3c3fa3 feat(twenty-sdk): extract & compile app translations into the manifest (#22236)
## Summary

**PR 2/4** of the app-metadata-translations stack. Gives app developers
the authoring side, as part of the normal manifest build — and it stays
out of the way of developers who don't translate.

- `twenty-sdk` CLI i18n pipeline: collect translatable strings from the
manifest, generate value-as-key message ids (`sha256(value)` truncated,
byte-identical to the server's `generateMessageId`), a `dev
i18n-extract` command to scaffold per-locale catalog files, and a
compile step folded into `build` that emits `manifest.translations`.
- Opt-in: no `locales/` dir → `compileApplicationTranslations` returns
`undefined` → manifest is unchanged.
- Adds an optional `locale` to the front-component execution context so
components can translate against the host locale.

## Stack
Stacks on #22235 (PR 1/4). Base branch:
`claude/app-translation-1-runtime-resolution`.

## Tests
Unit (vitest): extract/compile round-trip + message-id determinism.

## Verification note
`yarn install` could not complete in the remote dev environment, so
typecheck/lint/tests were not run locally — **CI is the source of
truth**.

https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22236?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-28 07:36:48 +02:00
Félix Malfait 41c10b9ee7 feat(server): resolve app-owned metadata translations at runtime (#22235)
## Summary

First of a **4-PR stack** that lets apps built with `twenty-sdk`
translate their metadata, resolved at runtime. The standard Twenty app
is modelled as "an app like any other" — `NULL
applicationRegistrationId` ⟺ the standard app, no special-casing.

This PR adds the server foundation and wires runtime resolution for
**object** and **field** metadata:

- New `applicationTranslation` core table + entity (nullable
`applicationRegistrationId`, `locale`, `messages` jsonb), one row per
(app, locale) to avoid multi-MB rows.
- `ApplicationTranslationCacheService` (process-local, 30s TTL) +
`ApplicationTranslationSyncService` (upsert + soft-delete from a
manifest).
- Shared `translateStandardLabel` util: application catalog → i18n
bundle → source value.
- Object/field resolvers + dataloaders prefetch and apply the per-app
catalog. The new `applicationCatalog` param is **optional**, so standard
behaviour is byte-unchanged.
- Fast instance command to create the table.

## Stack
**PR 1/4**, targets `main`. Followed by: (2) twenty-sdk extract/compile
→ `manifest.translations`, (3) resolution across the remaining metadata
resolvers, (4) the per-locale standard-override editor.

## Tests
Unit: `translateStandardLabel`, `resolveObjectMetadataStandardOverride`
(including the application-catalog path).

## Verification note
The remote dev environment for this branch could not complete `yarn
install` (no package-registry egress), so typecheck/lint/tests were not
run locally — **CI is the source of truth** for this stack. Changes
follow existing patterns.

https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22235?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-28 07:36:21 +02:00
Félix Malfait 538b180824 feat(dpa): self-serve Data Processing Agreement generator (#22243)
## What

A single, region-aware DPA that serves all customers, generated
automatically from the customer's deployment. Two layers:

1. **Click-through DPA** — recorded at signup (acceptance = execution),
resolving merge fields from the deployment region. Cloud only.
2. **In-app signed-PDF generator** — Settings → Legal → Generate DPA:
preview the agreement, enter legal entity + authorized signatory,
download a PDF pre-signed by Twenty, and store the executed copy against
the workspace with its template version + timestamp. Deep-linkable at
`/dpa` (login-gated) for `twenty.com/dpa`.

## How it resolves

A typed variable matrix (`dpa-region-config.constant.ts`) maps the
deployment region to the contracting Processor entity and terms:

- **EU (default)** → Twenty.com SAS, hosting EU/Frankfurt, governing law
France, SCC section dormant.
- **US (custom)** → Twenty, Inc., hosting US, SCC section active.

Region is a deployment-wide setting (`DPA_DEPLOYMENT_REGION`, default
EU) behind a `DpaRegionService` seam so it can later become
per-workspace without touching callers. The legal text is verbatim from
the template (generated into `dpa-template.constant.ts` directly from
the source `.docx`); only the 6 merge fields are filled and the SCC
sections (7.2–7.5) stay in the document for every region per the spec —
only field values branch. Sub-processors are deferred to
trust.twenty.com (not enumerated). Billing stays decoupled (Twenty, Inc.
remains merchant of record regardless of Processor).

## UI

Standard list + create-page pattern (mirrors API keys / webhooks): a
list of executed copies (with re-download) — or the agreement preview
when none exists — and a top-right blue **Generate DPA** CTA opening a
standard create page. The "Legal" item is intentionally **not** in the
settings menu; the page is reached via the `/dpa` deep link.

## Notable implementation details

- **PDF** is rendered server-side with `@react-pdf/renderer`. The
built-in standard-14 fonts only encode ASCII and crash on the template's
curly quotes / em–en dashes / accented Latin, so Liberation Sans (OFL)
is **subset to a Latin glyph set and embedded as base64 data: URLs** —
no font files to ship or resolve at runtime (works in dev, prod-Docker
and CI).
- New `core.dpaAgreement` table via a fast instance command (FK hash
reproduced to match TypeORM).
- Self-hosted deployments (billing disabled) skip click-through
recording and stamp a prominent "not a valid agreement" banner on the
preview and PDF.

## Tests

- Unit: resolver (per-region entity/law/SCC state, EU default, no
unresolved `{{ }}`, SCC sections present in both regions, self-hosted
notice) and HTML renderer.
- Integration (`test/integration/graphql/suites/dpa`): preview has no
unresolved fields; `generateSignedDpa` renders + persists + returns a
downloadable PDF (asserted with accented input to guard the font
regression); list re-download.

## ⚠ Needs legal input before go-live (marked `TODO_CONFIRM` in
`dpa-region-config.constant.ts`)

- Registered-office addresses for Twenty.com SAS and Twenty, Inc.
- US deployment governing law (the template only specifies France).
- DPO name and the Twenty pre-signed authorized signatory name/title.

## Out of scope (flagged per spec)

Intra-group legal agreement and any Stripe/billing-entity changes. A
future e-sign provider would plug in at `DpaService.generateSignedDpa` +
the signatory input.

> Draft until the integration test passes in CI and the legal
`TODO_CONFIRM` values are supplied.

https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22243?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-27 17:46:38 +02:00
Abdullah. 77c84815ef feat(website): rework the product-stepper Layout visual (#22249)
## What

Reworks the **"Layout" step** visual of the product-page stepper (the
floating record-layout-editor scene) to match the Figma references and
read as a premium, layered composition. Data is unchanged — this is
purely the look.

## Figma alignment

- **Fields editor** — the inline-edit field row now keeps the field icon
+ a bordered input + blue **Done** + the eye / ⋮ (instead of a floating
box); **"New fields"** renders as a uniform field row; section headers
get an overflow menu; field types read `Boolean` / `Date & Time`.
Dropped the redundant "Fields widget" header label.
- **"General" widget** — header chevron, the real `anonymousFelix`
avatar on Account Owner (reusing the shared asset the other
product-feature visuals use), a **Revenue** row, and a money-bag icon
for the currency field.

## Premium composition

- The nav, record, and Fields editor read as one elevated **z-stack**
(back → front): the nav is pushed right so its edge tucks under the
record (lowest z-index), the record sits above it, and the editor stays
in front — each casting a progressively stronger shadow so the depth
ordering is unmistakable.
- The **"New record / Enrich / Edit actions"** bar is centered over the
nav–record seam (`translateX(-50%)`) to tie the layered stack together
at the top.

## Testing

- typecheck · oxfmt · oxlint · check-conventions — green.
- Verified visually with headless Playwright on `/product` (Layout
step).


<img width="697" height="734" alt="image"
src="https://github.com/user-attachments/assets/58ae7ffb-13d1-4310-bbdc-864589f8bc43"
/>
2026-06-27 17:41:34 +02:00
Félix Malfait 2a21eb46c0 perf: cap to-many relation records per parent and inline chips in table (#22206)
## Problem

Record table views that show a to-many relation column (e.g. Workflows
with a "Runs" column) get slow and janky to scroll when some records
have many related records.

Two root causes, found by profiling the page live:

1. **Backend over-fetch + unfairness.** Nested one-to-many relations
were loaded with a single flat limit of `QUERY_MAX_RECORDS_FROM_RELATION
* parentCount` shared across *all* parents in the page (`WHERE
parentColumn IN (ids) LIMIT 60*N`, no per-parent cap). A single hot
parent can consume the entire budget — returning thousands of rows for
one cell, and potentially starving sibling parents of records they
actually have. The `limit * parentCount` shape shows the original intent
*was* a per-parent budget; it was just implemented as a global limit.

2. **Frontend DOM explosion.** `ExpandableList` mounts the *entire*
child array inline (clipped with `overflow: hidden`) when unfocused, and
mounts all children for measurement when focused. A cell with 2,000+
relation chips mounts ~14k DOM nodes — one observed page reached ~55k
nodes for 43 rows, producing 100–300 ms main-thread long tasks on every
scroll.

## Fix

- **Backend:** load one-to-many relations with a true **per-parent** cap
via a `LATERAL` join — each parent runs its own indexed, `LIMIT`-ed scan
that stops after the per-parent budget. This is `O(perParentLimit ×
parentCount)` and never reads or sorts a parent's full relation set. The
per-parent query is built through the workspace query builder (so it
stays schema-qualified and keeps the soft-delete predicate) and wrapped
as a `FROM` subquery; read/row-level permissions are enforced when
records are hydrated by id, as elsewhere in the relation loader.
Many-to-one is unchanged.
- **Frontend:** add an opt-in `maxInlineCount` to `ExpandableList` so
to-many relation cells mount only a small inline preview; the expand
dropdown still renders the full fetched set. Fully backward compatible
(no cap → identical behavior).

## Why LATERAL over a window function

A windowed `ROW_NUMBER() OVER (PARTITION BY parent) <= limit` is correct
and fair too, but a window function **cannot stop early within a
partition** — it must read every matching row (and sort it). Measured on
skewed data (one parent with ~4k children, on the existing single-column
join index, PG16):

| Approach | Time | Buffers | Rows read from the hot partition |
|---|---|---|---|
| Pre-PR (`LIMIT 60×N`) | 1.6 ms | 91 | ~180 total, early-stops, but
**unfair** (starves siblings) |
| Window (`ROW_NUMBER`) | 3.7 ms | 128 | **all ~4k + sort** |
| **LATERAL (`per-parent LIMIT`)** | **0.5 ms** | **57** | **~60, index
early-stop** |

LATERAL matches the pre-PR read cost while being fair, needs no new
index, and scales independently of how large any single relation is.

## Verification

- Backend integration test (`nested-relation-per-parent-limit`): a
parent with 65 children is capped at 60 while a sibling with 3 keeps all
3 — passes.
- `EXPLAIN ANALYZE` on the generated SQL: Index Scan with the `LIMIT`
pushed into the per-parent lateral (early-stop).
- Frontend unit test for the `ExpandableList` cap.
- Manual check on a table cell with 40 related records: exactly 10 chips
mount inline (down from 40), no console errors, chips still clickable
and the overflow count reflects the true total.
2026-06-27 15:12:43 +02:00
github-actions[bot] 4840233a1c i18n - website translations (#22244)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22244?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-06-27 14:06:24 +02:00
Félix Malfait 0e22ae0521 feat: create calendar events on Google and Microsoft accounts (#22231)
## Context

Twenty can import calendar events and send emails, but cannot create
calendar events. This adds calendar event creation on connected
**Google** and **Microsoft** accounts, mirroring the existing email-send
architecture (`message-outbound-manager`).

## What it adds

The capability is exposed three ways, all backed by the same composer →
driver → persist pipeline:

- **GraphQL mutation** `createCalendarEvent` (metadata API)
- **AI agent tool** `create_calendar_event` (flows to MCP
automatically), gated by a new `CREATE_CALENDAR_EVENT_TOOL` permission
flag
- **Workflow builder node** "Create Calendar Event" in the **Core**
section, with a full settings form (variable interpolation supported)

CalDAV/IMAP is intentionally out of scope for now (different long pole).

## Design notes

- **Reuse over reinvention** — the created event is run through the
existing inbound formatters (`formatGoogleCalendarEvents` /
`formatMicrosoftCalendarEvents`) and persisted immediately via the
existing `CalendarSaveEventsService`, so it appears in Twenty right away
and is reconciled by the next provider sync (dedup on external id).
Persistence is best-effort.
- **OAuth scopes** — Google already requests `calendar.events`
(read+write), so no change there. Microsoft moves `Calendars.Read` →
`Calendars.ReadWrite`; existing Microsoft accounts must re-consent
(surfaced as a clear "reconnect" error via a missing-scope check).
- **Deliberate invitation semantics** — `sendInvitations` is off by
default. When off, the event is created with **no attendees** on either
provider, so creating an event never silently emails external people.
When on, attendees are attached and notified (Google `sendUpdates: all`,
Microsoft's default). This sidesteps Microsoft Graph having no
per-request suppression.
- **Timezone correctness** — Microsoft Graph interprets `dateTime` as
wall-clock in the supplied `timeZone` and ignores the offset, so the
absolute instant is converted to its wall-clock form before sending
(Google honors the offset directly). Both providers end up scheduling
the same instant.
- **Conferencing** — optional Google Meet
(`conferenceData.createRequest`, with a follow-up `events.get` to
resolve the async link) / Microsoft Teams (`isOnlineMeeting`).
- Attendees are a comma-separated string everywhere (tool input, GraphQL
DTO, workflow input), consistent with `send_email` recipients; the
composer parses to its internal list.

## Test plan

- **Unit**: 45 tests covering the composer (validation, all-day
boundaries, offset enforcement, timezone, scope checks, default-account
resolution), both provider drivers, the dispatcher, and the workflow
step-log builder.
- **Integration**: `createCalendarEvent` on the `/metadata` API fails
closed with a structured error for a non-existent account (the
auth/ownership/validation path that doesn't require provider mocking).
- **Manual**: verified the workflow node appears in the Core section,
the settings form renders and round-trips (edit → autosave → reload),
and the live mutation returns a structured failure for a bogus account.

## Open question for reviewers

The metadata mutation `createCalendarEvent` shares a name with the core
schema's auto-generated `createCalendarEvent(data:)` CRUD mutation for
the CalendarEvent object — they live on different endpoints (`/metadata`
vs `/graphql`) so there's no runtime conflict, but it's a potential
point of confusion for API consumers. Happy to rename (e.g.
`createCalendarEventOnConnectedAccount`) if preferred.

## Out of scope / follow-ups

- CalDAV/IMAP support
- Event update/delete and recurrence
- Existing Microsoft accounts need re-consent for the widened scope


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22231?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: neo773 <neo773@protonmail.com>
2026-06-27 14:05:58 +02:00
Félix Malfait 2662fda647 ci(preview-env): run preview environments on free ci-public Actions (#22245)
## Why

PR preview environments were broken. The dispatch half (this repo) was
fine, but the receiving `preview-env.yaml` in **ci-privileged** failed
on every run — and ci-privileged is a **private** repo, so its 5h
keepalive job bills paid Actions minutes anyway.

The intended design (started but never wired up) runs the preview env on
the **public** `twentyhq/ci-public` repo, where Actions minutes are
free, and keeps no privileged token on the runner that executes
PR-controlled code. This finishes that migration.

## What this PR does

Repoints `preview-env-dispatch.yaml` to dispatch `preview-env.yaml` on
**ci-public** instead of ci-privileged. The dispatcher app token is
simply retargeted (still `actions:write` only, via `workflow_dispatch`).

## Companion PRs (must land together)

- **twentyhq/ci-public** — converts `preview-env.yaml` to
`workflow_dispatch`; dispatches the tunnel URL back to ci-privileged for
the PR comment before any PR code runs.
- **twentyhq/ci-privileged** — adds `preview-env-comment.yaml` (posts
the PR comment with the App key) and deletes the now-dead
`preview-env.yaml`.

## Manual prerequisites (cannot be done in a PR)

- Install/authorize the `TWENTY_WORKFLOW_DISPATCHER` GitHub App on
**ci-public** with `actions:write`.
- On **ci-public**: add `vars.DOCKERHUB_RO_USERNAME`,
`secrets.DOCKERHUB_RO_TOKEN`, and `secrets.CI_PRIVILEGED_DISPATCH_TOKEN`
(fine-grained PAT, `actions:write` on ci-privileged only).

## Suggested merge order

1. ci-privileged (receiver must exist on `main` before ci-public
dispatches to it)
2. ci-public (workflow must exist on `main` before this repo dispatches
to it)
3. this PR

## Test plan

- [ ] Open an internal PR touching `packages/twenty-docker/**`; confirm
`Preview Environment Dispatch` runs and triggers `Preview Environment`
on ci-public.
- [ ] Confirm the trycloudflare URL sticky comment lands on the PR
(posted by ci-privileged's `preview-env-comment`).
- [ ] Confirm a `preview-app` label on an external contributor PR
triggers the same flow.

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

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22245?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-27 11:14:58 +02:00
Abdullah. 03f3789d13 feat(website): menu adapts to the section beneath it (#22241)
## What

The sticky menu now adopts the color scheme of whatever section sits
beneath it as you scroll, across every page. Background, logo, and
buttons ease between schemes for a smooth handoff.

## How

- **Declarative contract** — sections opt in with a `data-menu-surface`
attribute (`SectionShell`, the footer, and the product hero's mobile
sections); `useActiveSurfaceScheme` tracks which surface sits under the
menu's bottom edge (`MENU_HEIGHT_PX`) and reports its `data-scheme`.
- **One provider** — `MenuStyleProvider` lives in the `(site)` layout so
every page adapts; the Menu resolves its scheme as `override ??
activeScheme ?? prop`. SSR seeds the prop to each page's first section,
so there's no mount flash.
- **Hero handoff** — the product hero keeps the menu via a per-frame
override only while its track still covers the nav band
(`controlsMenu`); it releases to the observer *before* the track clears
the bar, so the menu stays opaque on exit and never flashes the halftone
backdrop rising behind it. The menu's `backdrop-filter` was removed (a
no-op over the opaque menu, and a GPU compositing artifact).

## Also in this PR

- **Menu folder reorg** to the `product-feature` convention:
`components/`, `effect-components/`, `data/`, and `types/` (one domain
type per file).
- **`MENU_HEIGHT_PX` token** replacing the literal `64` that was
duplicated across four files (menu row, the hero's scroll model and
component, the observer).
- **`findActiveSurfaceScheme`** extracted as a pure, unit-tested
function (inclusive top / exclusive bottom, first-match, no-match,
null-scheme).
- **Mobile AI-section fix** — the mobile hero sections now declare the
surface contract, so the menu adapts dark over the AI block on mobile
(it previously stayed light). Their color is driven from `data-scheme`
(single source) rather than a parallel prop.

## Testing

- Jest — 16 tests (scroll model + surface-selection util).
- Headless Playwright (mobile 390px) — menu `light` over the intro →
`dark` over the AI section; section colors unchanged (`#fff` /
`rgb(20,20,20)`).
- Desktop unaffected — the mobile sections are `display:none` (zero
rect), so the observer skips them and the hero override path is
untouched.
- Gates green — typecheck, oxlint, oxfmt, check-conventions.
2026-06-27 12:58:50 +05:00
github-actions[bot] f041f6dfb6 chore: sync AI model catalog from models.dev (#22242)
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/22242?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-06-27 08:59:37 +02:00
github-actions[bot] bf7a766935 i18n - website translations (#22234)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22234?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-06-26 22:29:54 +02:00
github-actions[bot] ad1ac4f3c5 i18n - website translations (#22225)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22225?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-06-26 22:17:08 +02:00
Etienne 3525187321 fix(ai) - fixes (#22227)
- ai chat author fix (before : "workflow", after : "user")
- https://discord.com/channels/1130383047699738754/1496872385687584768

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22227?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-26 17:53:29 +00:00
Abdullah. d58ec64e10 feat(website): swap product Demo for a register Signoff; make signoff headings width-driven (#22232)
## Summary

Reworks the product page's closing section, and in doing so makes every
signoff closer width-driven.

### 1. Replace the Demo section with a register Signoff (`6a5f0c34bf`)

The product page ended on a static `AppPreview` mockup ("Try it live").
Swap it for the shared `Signoff` closer — a focused register CTA, the
same component the customers / why-twenty / partners pages use — so the
page ends by driving sign-ups instead of re-showing the product.

- New `ProductSignoff`: heading "Start moving faster today.", a
supporting line, and the **Get started + Talk to us** pair (matching
`CustomersCatalogSignoff`). Uses `SITE_URLS.appWelcome` per the
site-urls rule rather than inlining the URL.
- Removes the now-unused `product-demo` section and its 192KB background
asset.

### 2. Make Signoff headings width-driven (`0365c0d2ea`)

The signoff headings forced their two-line break with a literal `\n` in
the translated string — against the site's typography principle (fluid
type + `text-wrap: balance`, no `<br>`).

- `Signoff` now carries one default **`615px`** heading measure; all
five closers drop `\n`.
- The width was **measured, not guessed**: each heading was rendered
headlessly in the real production fonts (Aleo 300 body, Host Grotesk 300
accent — each heading mixes both) at the desktop size with `text-wrap:
balance`, then I found the common window where every heading breaks
identically: whyTwenty `[565–820]`, customers `[425–800]`, partner
`[380–730]`, product `[380–660]` — `615` sits in all of them. Each was
then verified to reproduce its exact current break at 615px.
- **Affects 4 other pages** (why-twenty, customers, customers/[slug],
partners) — all verified to render identically, and headings now reflow
on narrow viewports instead of being pinned by `\n`.
- Drops the one-off `headingMaxWidth` prop (now unused).

## Test plan

- oxfmt, oxlint, check-conventions, typecheck — all green.
- Headless measurement confirms all five signoff headings reproduce
their current desktop break at 615px (no visual regression).
- Visually confirmed the product closer and the four sibling signoffs.
2026-06-26 18:45:39 +02:00
martmull e747bc3e42 Add backfill installation feature for pre-installed apps (#22199)
## After

<img width="1070" height="514" alt="image"
src="https://github.com/user-attachments/assets/9cbd2ff5-1678-4c2f-84da-074568f37c51"
/>


## Summary
Adds the ability to backfill application installations across all
existing workspaces. This allows admins to retroactively install a
pre-registered application on every active and suspended workspace
through a background job, making the feature idempotent and
non-blocking.

## Key Changes

- **Backend Service**: Added `backfillApplicationOnAllWorkspaces()`
method to `PreInstalledAppsService` that:
  - Validates the application registration exists
  - Iterates through all workspaces using `WorkspaceIteratorService`
  - Installs the app on each workspace
  - Swallows `APP_ALREADY_INSTALLED` errors for idempotency
  - Logs success/failure counts

- **Background Job**: Created `BackfillApplicationInstallationJob` to
process backfill requests asynchronously via the message queue

- **GraphQL Mutation**: Added `backfillApplicationInstallation` mutation
to `AdminPanelResolver` that:
  - Validates the application registration exists
  - Enqueues the background job
  - Returns immediately without blocking the request

- **UI Components**: Enhanced
`SettingsAdminApplicationRegistrationGeneralToggles` with:
- New "Pre-install on new workspaces" toggle for the `isPreInstalled`
flag
  - "Backfill on all workspaces" button with confirmation modal
  - Loading state and success/error snack bar feedback

- **Data Model**: Added `isPreInstalled` field to
`UpdateApplicationRegistrationPayload` input type

- **Tests**: Added comprehensive unit tests for
`PreInstalledAppsService.backfillApplicationOnAllWorkspaces()` covering:
  - Missing registration validation
  - Successful multi-workspace installation
  - Idempotent handling of already-installed errors
  - Proper error propagation for unexpected failures

## Implementation Details

The backfill operation is designed to be:
- **Idempotent**: Already-installed apps are skipped without error
- **Non-blocking**: Runs as a background job via message queue
- **Resilient**: Per-workspace failures don't block other installations
- **Observable**: Logs aggregated success/failure counts for monitoring


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22199?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: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-06-26 16:21:27 +00:00
Abdullah. fea2b8736f feat(website): rebuild dashboard visual faithful to twenty-front (#22218)
Rebuilds the product-feature **DashboardVisual** to mirror
twenty-front's dashboard widgets, with colors traced to twenty-front's
actual source rather than eyeballed.

## Widgets
- **Bar — "Deals by month"**: single-series `blue8` (twenty-front's
`GRAPH_DEFAULT_COLOR`), dashed `4 4` gridlines, value labels, nice
rounded Y-ticks. Replaces the old stacked bar (stacked bars aren't used
in twenty-front).
- **Donut — "Deals by stage"**: the real opportunity pipeline
(New/Screening/Meeting/Proposal/Customer), each segment colored by that
stage option's own color from the metadata
(`red/purple/sky/turquoise/yellow`), with a center total and a paginated
horizontal legend.
- **KPIs**: big-number cards (Revenue YTD / Avg deal size / Win rate).

## Responsive — `mediaUp('md')`
The dashboard is the full-width spotlight tile, whose frame is short
below md and grows to 420px at md+. So the layout keys off md: below it
collapses to **2-up KPIs + a full-width bar** (donut and the 3rd KPI
hidden, smaller breadcrumb), and the spotlight frame's mobile min-height
is bumped so the bar has room; at md+ the full 3-KPI + side-by-side
layout returns. The donut caps at its size and shrinks with its
container.

## Notes
- Follows the `product-feature` conventions (per-visual folder,
one-export-per-file, split types).
- `Tiles.tsx`: one-line spotlight mobile min-height bump (only the
dashboard uses the spotlight tile).
- typecheck + lint + build all green.

<img width="1148" height="638" alt="image"
src="https://github.com/user-attachments/assets/1eb8f120-88c2-45d1-adf9-ec00abe11006"
/>
2026-06-26 20:57:21 +05:00
martmull 8a257e0de3 Update public app names (#22228)
- update names to comply with @twentyhq/ prefix standard
- update versions
- upgrade twenty sdks versions 

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22228?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-26 17:51:01 +02:00
Amresh Chaurasiya e1120d38b6 fix: stamp MCP and AI Agent writes with FieldActorSource.AGENT (#22215)
## Description
Fixes #21437 MCP and AI Agent writes now correctly stamped with

### Problem
Records created through MCP server were stamped as `WORKFLOW`, making
them indistinguishable from workflow-created records. This breaks
loop-protection filters that skip workflow-originated records.

### Solution
- MCP writes now correctly stamped with `createdBy.source = AGENT`
- AI Agent execution now uses `AGENT` instead of `MANUAL`
- Added `WorkspaceCacheModule` to MCP module
- Updated tests to verify AGENT source

### Files Changed
- `mcp.module.ts`: Added WorkspaceCacheModule import
- `mcp-protocol.service.ts`: Set AGENT source in buildMcpToolSet
- `mcp-protocol.service.spec.ts`: Updated tests
- `agent-actor-context.service.ts`: Changed MANUAL → AGENT
- 

## Type of Change
- [x] Bug fix (non-breaking change)

## Checklist
- [x] Code follows project style
- [x] Tests added/updated
- [x] Issue linked

Fixes #21437

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22215?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-26 17:38:05 +02:00
Raphaël Bosi c891258f34 Add v2 onboarding create profile page (#22221)
<img width="3024" height="1498" alt="CleanShot 2026-06-26 at 15 30
23@2x"
src="https://github.com/user-attachments/assets/8b4863a9-66ed-4da1-851b-473cedf71511"
/>

<img width="3022" height="1500" alt="CleanShot 2026-06-26 at 15 29
43@2x"
src="https://github.com/user-attachments/assets/22fc0e94-f670-4638-975c-f06b2b2e25e8"
/>

Adds the v2 onboarding **Create profile** page, shown right after the
import-contacts step (`PROFILE_CREATION`) for the onboarding-v2 cohort.
It renders full-screen under `BlankLayout` via the shared
`OnboardingV2Layout`, matching the Figma (340px column, inline round
avatar uploader + First/Last row, Job Title, dark Continue). The v1
modal flow is untouched and still used for non-v2 users.

Job Title is wired end-to-end: it adds a real `jobTitle` field to the
`WorkspaceMember` standard object (shared metadata constant + flat field
metadata + entity property) and a `2-17` workspace upgrade command to
backfill the field on existing workspaces. Continue persists name +
jobTitle through the existing `updateWorkspaceMemberSettings` mutation,
whose allow-list picks up the new standard field automatically.

Routing mirrors `SyncEmailsV2`: new `AppPath.CreateProfileV2`, lazy
route, and an `isOnboardingV2`-gated branch in
`usePageChangeEffectNavigateLocation` (+ tests and a Storybook story).

Reviewer notes:
- `jobTitle` is **write-only** for now (no read-back path: core
DTO/transpiler/fragment unchanged), and the field is
`isSystem`/non-UI-editable to match its siblings. Easy to surface later
if wanted.
- New `OnboardingProfilePictureUploader` is a compact round avatar
uploader reusing the same upload mutation flow as
`WorkspaceMemberPictureUploader`.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22221?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-26 17:34:44 +02:00