Commit Graph

12804 Commits

Author SHA1 Message Date
Raphaël Bosi 66c860574f Point New UI visual regression at the twenty-ui Argos project (#21728)
The `twenty-new-ui` Argos project is being renamed to `twenty-ui` (and
the old `twenty-ui` / `twenty-ui-vs-new-ui` projects removed).

Updates the New UI visual-regression flow to target `twenty-ui`: the
dispatch project mapping and the screenshot artifact name (which must
match `argos-screenshots-${project}`), plus the internal storybook
artifact name for consistency.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21728?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-17 14:27:39 +00:00
Thomas Trompette 8130fa1c45 feat(workflow): use workspace member as variable sender for emails (#21582)
## Summary

Lets the email workflow node sender be driven by a variable: the
connected-account field accepts a `{{variable}}`, and the backend
resolves it to a connected account at run time.

<img width="436" height="195" alt="Capture d’écran 2026-06-16 à 11 59
27"
src="https://github.com/user-attachments/assets/18eee21e-aed6-4447-9bf4-5cb0e2cfc371"
/>

### Email sender by variable
- The connected-account field now accepts a `{{variable}}` via the
variable picker (uses `FormSelectFieldInput` with
`WorkflowVariablePicker`), with a hint to pick a connected account or
set a workspace member as a variable.
- The email workflow action resolves the stored sender value explicitly:
if it is a `workspaceMemberId` (a UUID matching a workspace member), it
resolves that member's first connected account; otherwise the value is
used directly as a `connectedAccountId`.
- Resolution lives in `EmailWorkflowActionBase` and applies to both
`SEND_EMAIL` and `DRAFT_EMAIL`. If a matching member has no connected
account, the run fails fast with a clear message (no silent fallback).
- `DRAFT_EMAIL` also fails fast when the resolved connected account is
missing the required OAuth scopes (`gmail.compose` / `Mail.Send`), via a
server-side `getMissingDraftEmailScopes` util that mirrors the front-end
check.
- Existing workflows with a hardcoded `connectedAccountId` keep working
unchanged (no migration needed).

> Note: exposing the running workspace member as a manual-trigger
variable (`_metadata.workspaceMemberId`) is split into a follow-up PR.

## Test plan
- [x] Backend unit tests for `draft-email-tool`,
`get-missing-draft-email-scopes`, and the `send-email` / `draft-email`
workflow actions (incl. workspace-member sender resolution)
- [x] Lints clean on all changed files
- [x] Manual: configure an email node with a workspace-member variable
sender and confirm it resolves and drafts/sends
- [x] Manual: confirm a member lacking compose permission fails the run
with the permission message

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


---

### Update — scoped to Draft Email only

The sender variable picker is now exposed **only on the Draft Email
node**. The Send Email node keeps a plain account select (no variable
picker, no variable hint) until we enable it there in a follow-up.
Backend resolution still lives in `EmailWorkflowActionBase` and remains
generic, so enabling the picker for Send Email later requires no backend
change.
2026-06-17 13:57:22 +00:00
Weiko 3ee93b5ec9 feat(server): add isSystemSideEffect & merge createOneObject/createOneField side-effect migrations (#21673)
## Context

When an object is created via the metadata API, `createOneObject`
creates its side-effect entities (INDEX view + viewFields, indexes,
navigation menu item, "go to" command menu item, record-page fields
view, page layout/tabs/widgets) across **three separate
`validateBuildAndRunWorkspaceMigration` calls**, purely because the
protection behavior (mutations → overrides, delete → deactivate, reset →
reactivate) was keyed on *"owned by the standard app"*, forcing the side
effects into batches with different application owners. This
misrepresents ownership and breaks atomicity.

This PR separates two orthogonal concepts:

- **Ownership** (`applicationId`), the true owner: the caller's
application (the workspace custom app today, 3rd-party apps later).
- **Protection** (`isSystemSideEffect`), the row was generated by the
system, so user mutations route to overrides, deletion becomes
deactivation, and reset restores defaults.

Once side effects are re-owned to the caller, the old `applicationId ===
standardApp` check can no longer tell an original side-effect row from a
user-added one so a dedicated `isSystemSideEffect` flag carries the
protection instead.

This is **PR 1 of 2** (forward-only). It makes newly created objects and
fields correct; existing workspaces are handled by a follow-up backfill
(see *Out of scope*).

## What this PR does

- **`isSystemSideEffect` column** on the 8 affected entities (`view`,
`viewField`, `indexMetadata`, `commandMenuItem`, `pageLayout`,
`pageLayoutTab`, `pageLayoutWidget`, `fieldMetadata`), with
`@WasIntroducedInUpgrade` + an entry in the flat-entity property
configuration (`toCompare: true`, read-only).
- **Single atomic migration in `createOneObject`**: the three
`validateBuildAndRunWorkspaceMigration` calls are merged into one, owned
by the caller (`resolvedOwnerFlatApplication`) and the record-page
view/fields, page layout, and navigation command item are re-owned to
the caller and flagged `isSystemSideEffect: true`.
`buildNavigationFlatCommandMenuItem` is parameterized with
`applicationUniversalIdentifier` (no longer hardcoded to the standard
app).
- **Field-creation side effects** (`createManyFields`/`createOneField`
already run as a single caller-owned migration, so no re-ownership/merge
was needed): the auto-created viewField is flagged `isSystemSideEffect:
true`, and a new field now also propagates to the object's **INDEX/table
view** (added there as a **hidden** column, `isVisible: false`) in
addition to
the record-page FIELDS widget. The INDEX view is targeted directly by
`key = INDEX` (it is not a page-layout widget), de-duplicated per
`(viewId, fieldMetadataUniversalIdentifier)` to respect the per-view
unique index.
The unique-field index is likewise flagged the inverse relation field
stays unflagged (`isSystem: false`).
- **Protection predicate** extended: `isCallerOverridingEntity` and the
removal/reset split strategies now treat `isSystemSideEffect` rows as
protected even when caller-owned (route to
overrides / deactivate / reset) and the page-layout-reset guards allow
resetting flagged entities.
- **Standard compute maps** set the flag consistently so a re-sync
produces no diff (standard-object side effects stay `false`; per-object
nav command items and custom-object base fields are `true`).
- **Read-only GraphQL exposure** of `isSystemSideEffect` on the view /
view-field / page-layout / tab / widget / command-menu-item DTOs (not
exposed on create/update inputs). => Todo: needs to take this new flag
into account. This is fine for now because isSystem remains on
object/field.
- **Fast instance command** (`2-14`) adding the 8 columns (`NOT NULL
DEFAULT false`).

## Scope decisions

- **`pageLayout` is not an `OverridableEntity`**, its own row has
nothing user-overridable (all customization lives on tabs/widgets). It's
dual-purpose (`RECORD_PAGE` side-effect vs. user `DASHBOARD`), so it
gets `isSystemSideEffect` for protection only, no `overrides` jsonb.
- **`navigationMenuItem` is out of scope.**: Those are side effects only
for the metadata API and not marked as "system" (they can be
deleted/updated etc...)
- **`viewFieldGroup` is not a side effect**, it's only created via the
explicit view-field-group API, never by object/field creation, so it
gets no flag.

## Out of scope (follow-ups)

**PR 2** — slow per-workspace backfill (re-own + flag existing side
effects, recreate missing ones) and deterministic v5 identifiers for
base fields / pageLayout / tab.


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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21741?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-17 15:51:50 +02:00
neo773 91050dba8d docs: update calendar-email page (#21719)
Adds 3 changes
1.) Add IMAP mention
2.) Clarifies disabling SSRF for self hosters running air gapped
systems, (we have received this question several times ) 3.) Toggling
Syncing internal emails

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21719?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-17 13:07:04 +00:00
Thomas Trompette 1f9c4cf5b9 fix(front): home redirect honors first object of the navigation menu (#21626)
https://github.com/user-attachments/assets/db11f02c-4502-4adc-8cf7-55d3dc0211a6

## Summary

Fixes [#21166](https://github.com/twentyhq/twenty/issues/21166). Going
to the workspace root (`/`) always redirected to the
alphabetically-first object.

Following [the review
discussion](https://github.com/twentyhq/twenty/issues/21166#issuecomment-4611570420),
this redirects to the **first object of the navigation menu** instead of
the last-visited object. "Last-visited" had too many edge cases (which
page? records vs. index?), whereas the first item of the user's menu is
unambiguous and matches what they see at the top of the left sidebar.

## Changes

- New `getFirstObjectNavigationMenuItemLink` util walks the workspace
navigation menu items in display order (sorted by `position`) and
returns the link of the first object-backed item (`OBJECT`/`VIEW`) the
user can read.
- `useDefaultHomePagePath` uses it as the primary target. It stays on
`AppPath.Index` until navigation menu items have loaded (they load
*after* the minimal-metadata fast path, so resolving earlier would land
on the wrong object during the post-login window), then falls back to
the first readable object if the menu has no object item.

Scope is intentionally minimal: the last-visited tracking is left in
place and will be cleaned up separately now that the redirect no longer
reads it.

## Test plan

- `npx jest useDefaultHomePagePath
--config=packages/twenty-front/jest.config.mjs` — covers menu order
honored over alphabetical, `VIEW` item links, the loading deferrals
(object metadata + navigation menu items), the readable-object fallback,
and the no-readable-object profile-settings fallback.
- Manually: visit a non-first object, navigate to `/`, confirm the
redirect goes to the first object of the navigation menu.
2026-06-17 12:03:34 +00:00
Félix Malfait ece7a384df fix(front): wait for viewFields + fieldMetadataItems before opening the metadata gate (#21713)
## Problem

On twenty-main, loading a record-index/standalone page for the first
time renders the page chrome (title, view chip with record count) but
the table body stays blank. A subsequent reload fixes it. Regression
introduced by the cache-first `currentUser` bootstrap (#21532);
follow-up to #21592, which already mentioned the experiment "should be
reviewed."

## Root cause (concurrency)

The metadata loader runs in two phases and the gate opens between them:

1. **`loadMinimalMetadata`** fast-paths `objectMetadataItems` and
`views` to `status: 'up-to-date'` with only their *minimal* fields.
`viewFields` and `fieldMetadataItems` stay `'empty'`.
2. **`IsMinimalMetadataReadyEffect`** opens the gate as soon as those
two are `'up-to-date'` — before viewFields exist.
3. The page mounts. `viewsSelector` joins views with an empty
`viewFields` collection, so `view.viewFields = []`.
`RecordIndexLoadBaseOnContextStoreEffect` calls
`loadRecordIndexStates(view, …)` with the empty viewFields and pins
`loadedViewId === contextStoreCurrentViewId`.
4. `loadStaleMetadataEntities` later populates viewFields; the selector
recomputes, but the effect bails out on the `loadedViewId` guard.
`currentRecordFields` stays empty.
5. `visibleRecordFields` stays empty →
`RecordTableVirtualizedInitialDataLoadEffect` hits its
`isEmpty(visibleRecordFields)` guard and never fetches → empty body. The
"300" count visible in the screenshot comes from
`useGetRecordIndexTotalCount`'s separate aggregate query, which doesn't
depend on viewFields.

**Why the gate close/reopen self-heal doesn't work reliably:**
`replaceDraft → applyChanges` happen in the same microtask chain. React
18 automatic batching collapses both into a single render where status
goes `'empty' → 'up-to-date'` without an intermediate `'draft-pending'`
observable to React. The gate never closes, children never unmount,
`loadedViewId` is never reset.

**Why it surfaced after #21532:** Before, `currentUser` was loaded only
after `GetCurrentUser` returned — by which time
`loadStaleMetadataEntities` had typically also completed and viewFields
were populated when the gate opened. Now the cached `currentUser` lets
the gate open the moment `loadMinimalMetadata` finishes.

## Fix

Extend `IsMinimalMetadataReadyEffect` to also require
`fieldMetadataItems` and `viewFields` to be `'up-to-date'` before
opening the gate. Both are joined into the data the record-index page
reads on first paint (`objectMetadataItemsWithFieldsSelector` reads
fieldMetadataItems; `viewsSelector` reads viewFields), so the page can't
render correctly without them.

- **Warm cache** (all entities hydrated `up-to-date` from IndexedDB):
unaffected — gate opens immediately.
- **Cold cache and the first load post–IndexedDB-migration**: the gate
stays closed until `loadStaleMetadataEntities` + `applyChanges` finish,
then opens with full metadata. The page mounts once with a populated
view; no race.

## Tests

- [x] `nx typecheck twenty-front` clean (file change passes `oxlint` on
the touched file).
- [ ] Manual on twenty-main: cold reload + first navigation to a
record-index page renders the table body without needing a second
reload.

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

https://claude.ai/code/session_01DD3469JAWYURa2sKUTJ85e

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21713?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: Claude <noreply@anthropic.com>
2026-06-17 11:40:55 +00:00
github-actions[bot] 1400c6e952 i18n - docs translations (#21724)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21724?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-17 13:52:41 +02:00
Thomas des Francs ca7ffb97b9 Added page card box-shadow (#21688)
## Summary
- add a subtle left-side page card shadow in light and dark mode
- preserve the existing border-ring box shadow
- give the page card wrapper enough left padding for the shadow to
render



<img width="904" height="1116" alt="image"
src="https://github.com/user-attachments/assets/2948fe4a-81ff-4045-ad6f-c0bc9b58be26"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-17 12:45:31 +02:00
Charles Bochet 34362de7b7 fix(route-trigger): distinguish user vs platform logic function execution errors (#21715)
## What

Splits route trigger logic-function failures into two cases instead of
one catch-all:

- **User error** — the function's own code threw an uncaught error.
Returns `500` and is **not** sent to Sentry.
- **Platform error** — an infrastructure/execution failure on our side.
Returns `500` and **is** sent to Sentry.

A disabled logic function now returns `403`.

## Why

User-code failures were flooding Sentry: a single workspace's function
hitting a transient upstream error generated tens of thousands of
events. #21656 stopped the flood by muting the entire route-trigger
execution error bucket — but muting everything also silenced genuine
platform failures we *do* want to be alerted on.

Splitting the bucket keeps the user-code noise out of Sentry (the
original goal) while making sure real platform errors still surface.

Users who want to return a specific status/body when their function
fails can still catch the error and return a `Response` — that path is
unchanged.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21715?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-17 12:45:04 +02:00
Rashad Karanouh a7760c04ab Partners app: profile picture (additive file field), derived region & deployment, scope cleanup (0.5.4) (#21709)
## Summary (twenty-partners app, v0.5.4)

- **Profile picture upload (additive)**: `profilePicture` stays a URL
(LINKS) — existing partners keep their picture — and a new
`profilePictureFile` (FILES) field is added for uploads. The read logic
functions (`list-available-partners`, `get-partner-by-slug`) select both
and **prefer the uploaded file, falling back to the legacy URL**,
returning the existing `{ primaryLinkUrl }` shape so the public
directory and the website are unchanged.
- **Region** auto-derived from the partner's country on application
creation (static lookup).
- **Deployment expertise** derived: defaults to `CLOUD`, adds
`SELF_HOST` when the partner covers Hosting & Infrastructure.
- **Partner.website** now set from the submitted domain.
- Removed 5 unused `partnerScope` categories (0 production usage); seed
remapped.
- Removed one-off data scripts (`import-from-tft`,
`migrate-partner-scope`, `partner-scope-map`).

Rebased on `main` (includes #21615 company-reuse).

## Why additive, not a field-type change

Twenty treats a field's `type` as **immutable**: an app upgrade silently
ignores a LINKS→FILES change (`fieldMetadata.type` is `toCompare: false`
in the server's flat-entity config). An in-place flip would leave the
column LINKS on prod while the display queries asked for a FILES `url`,
**breaking the partner directory**. The additive `profilePictureFile`
upgrades cleanly with no data loss; existing URLs keep working via the
legacy field + fallback. Removing the 5 unused enum options is also a
clean upgrade (0 records use them).

## Deploy notes

- Version `0.5.4`. Fully additive schema change → installs in place, no
data migration required.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21709?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-17 10:04:23 +00:00
Raphaël Bosi 9c9c34fccf Remove twenty-ui-deprecated and migrate frontend to twenty-ui (#21596)
Migrates `twenty-front`, `twenty-sdk`, and
`twenty-front-component-renderer` from `twenty-ui-deprecated` to
`twenty-ui` (mechanical import swap — the packages have API parity) and
deletes the deprecated package along with its workspace/CI/config
wiring.

Also adds `@linaria/react`/`@linaria/core` as direct deps of
`twenty-front` (it used them transitively via the deprecated package).

Note: move the required status check from `ci-ui-status-check` to
`ci-new-ui-status-check`.

Argos: the Storybook box-model/button-reset baseline shift (the bulk of
the visual diffs) is isolated in #21665 — Storybook now loads
twenty-ui's global `reset.scss`, which the production app already ships.
Once #21665 merges and this branch is rebased, the remaining Argos diffs
are component-level visual-parity items only.
2026-06-17 09:41:11 +00:00
Thomas des Francs 079040f1c0 Fix selectable list arrow focus (#21679)
## Summary

Tested on all select fields one by one

- Keep searchable selectable-list inputs focused while ArrowUp/ArrowDown
moves the selected item.
- Remove the old global "grid focused" mode and blur/refocus recovery
path.
- Scroll the selected item into view with `block: 'nearest'`, which
restores keyboard scrolling in long relation pickers without forcing the
row to the top.
- Add focused regressions for command-menu input focus and selected-item
scrolling.

## Root Cause

`SelectableList` hotkeys blurred the active input before arrow
navigation and stored a global grid-focused state. That let ArrowDown
move selection, but focus could fall back to the underlying page/table
instead of remaining in the command menu input.

## Recording

### Before


https://github.com/user-attachments/assets/a802cbc3-4cfd-4466-bc22-274935a77715

### After


https://github.com/user-attachments/assets/9c315d2e-dcb1-424d-80c9-a5942eb1b6bd

## QA Note

I checked all inputs one by one with a 2h30 agent in goal mode.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21679?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-17 08:46:07 +00:00
Charles Bochet 257f130fff feat(sdk): let docker:start choose the server version (#21690)
## What

Makes `yarn twenty docker:start` version-selectable. Same core feature
as #21686 — but here scaffolded apps default to `latest` (pinning is
**opt-in**) rather than being pinned to the scaffolder's version.

> Alternative to #21686. Pick one; the difference is only the scaffolded
default.

Two layers of resolution:

1. **Explicit flag** — `yarn twenty docker:start [version]`, mirroring
the existing `docker:upgrade [version]`.
2. **App-pinned default** — when no version is passed, `docker:start`
reads `twenty.serverVersion` from the app's `package.json`, falling back
to `latest`.

Generated apps ship `twenty.serverVersion: "latest"`, so default
behavior is unchanged. To make the local server reproducible as code,
set a version:

```json filename="package.json"
{
  "twenty": {
    "serverVersion": "2.2.0"
  }
}
```

## Changes

- `twenty-sdk`: new `getAppServerVersion()` util reads
`twenty.serverVersion` from the cwd's `package.json`; `serverStart`
gains a `version` option and resolves `option → app pin → latest`,
building the image via `getImageForVersion()`; `docker:start [version]`
(and the deprecated `server start [version]` alias) wired up.
- `create-twenty-app`: template `package.json` ships
`twenty.serverVersion: "latest"`. (`create-app` and the scaffolder are
otherwise untouched.)
- Docs: `local-server.mdx` documents version selection and the opt-in
pin.

## Behavior notes

- Default with no pin and no flag is `latest` — same as today.
- Version only matters when **creating** a fresh container — an existing
container keeps its image until `docker:upgrade` / `docker:reset`.

## Testing

- New unit tests for `getAppServerVersion` (5 cases).
- Extended the `app-template` scaffolding test to assert the `latest`
default.
- `twenty-sdk` cli vitest suite (273) and `create-twenty-app` jest suite
(9) pass; oxlint + oxfmt clean on changed files.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21690?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-17 10:55:02 +02:00
Félix Malfait eeed998c9e Let users pick their workspace subdomain during sign-up (#21641)
## What & why

During onboarding the workspace subdomain was auto-generated at sign-up
and only editable later in Settings. This adds a subdomain picker to the
workspace-creation flow, with **live availability checking** and
**name-driven auto-fill**.

The subdomain is chosen **on the central sign-up domain, before the
redirect onto the workspace subdomain** — so there's no mid-onboarding
domain switch (which would otherwise force a re-auth, like the Settings
"this logs everyone out" flow). It works uniformly for credentials and
SSO, since workspace creation is a post-auth mutation.

## Flow

Authenticate → **Create a workspace** → new step (workspace name +
address with live availability + auto-fill, seeded from the work email)
→ workspace is created with the chosen subdomain → the single redirect
lands on the final subdomain → onboarding modal (name pre-filled).

## Changes

**twenty-shared**
- `getSubdomainSlugFromDisplayName` — friendly slug from a display name,
built on the existing `transliteration` package (also transliterates
non-Latin names, e.g. 日本語 → `ri-ben-yu`).

**twenty-server**
- `checkWorkspaceSubdomainAvailability(subdomain)` query
(workspace-agnostic, `UserAuthGuard`) → `{ isValid, available,
suggestedSubdomain }`.
- `SubdomainManagerService`: availability + suggestion logic with
friendly numbered suffixes (`acme`, `acme-2`, …) instead of random hex;
`generateSubdomain` reuses it.
- `signUpInNewWorkspace` accepts an optional `{ displayName, subdomain
}` input (validated; falls back to auto-generation when omitted —
backward compatible, so existing callers are unaffected). Concurrent
same-subdomain sign-ups return a clear "already taken" error instead of
a generic DB error.

**twenty-front**
- New `SignInUpStep.WorkspaceCreation` step +
`useWorkspaceSubdomainField` hook (debounced, stale-response-safe;
auto-fills from the name until the user edits it, with a one-click "use
suggested" when taken; ignores Enter during IME composition; surfaces a
clear error if the availability check fails).
- Onboarding modal name pre-filled from the chosen name.

## Testing

- Unit tests: shared slug util, the `useWorkspaceSubdomainField` hook
(real auto-fill/availability flows via `MockedProvider`), and the
workspace-creation component; existing sign-up tests still pass.
- Typecheck, lint, and format green across twenty-shared / twenty-server
/ twenty-front.

## Notes / out of scope

- No DB migration — the `subdomain` column already existed.
- Self-hosted single-workspace sign-up is unchanged; the step is gated
to multi-workspace (global scope).
- Low-priority follow-ups: length bounds on the subdomain / displayName
inputs, and an integration test for the availability query.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:52:51 +02:00
Raphaël Bosi 4b3f38f8f2 Show app logo and name in workflow step side panel header (#21689)
Show app logo and name in workflow step side panel header

## Before

<img width="798" height="106" alt="CleanShot 2026-06-16 at 17 59 39@2x"
src="https://github.com/user-attachments/assets/a8881d1c-355c-4e3a-9379-0c3a7cfbf42f"
/>

## After

<img width="800" height="102" alt="CleanShot 2026-06-16 at 18 05 27@2x"
src="https://github.com/user-attachments/assets/43820d1a-6445-42d8-8f45-963e8741cbb7"
/>

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21689?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-17 08:41:03 +00:00
Félix Malfait bb6da7b7d1 feat(code-interpreter): reuse a warm sandbox per conversation (E2B) (#21664)
## What

The E2B code-interpreter driver created a **fresh sandbox on every
execution** and killed it in `finally`, so every call in a conversation
paid full cold-start and started blank. This PR keeps **one warm sandbox
per conversation** and, on idle, **pauses** it rather than killing it.

## How

- **Discovery without a registry:** the sandbox is tagged with the chat
`threadId` (scoped `workspaceId:threadId`) via E2B **metadata**, found
with `Sandbox.list({ query: { state: ['running','paused'], metadata }
})` and resumed with `Sandbox.connect()` (which auto-resumes a paused
sandbox). E2B is the source of truth — no Redis/DB mapping.
- **Pause/resume (E2B 2.x):** session sandboxes are created with
`lifecycle: { onTimeout: 'pause', autoResume: true }`. When idle they
**pause** — compute billing stops, filesystem **and** kernel/memory
state are preserved — and resume in ~1s on the next call. This replaces
the earlier keepalive approach.
- **No premature pause mid-run:** the sandbox is kept alive for
`max(execution timeout, idle window)`, so a long execution is never
paused underneath itself.
- **Tenant isolation:** discovery filters by the `twentySessionId` tag
and **re-checks it client-side**, so a loose server-side match can never
hand one conversation's warm sandbox (with its files, kernel state,
token) to another.
- **Concurrency:** executions sharing a session are serialized
in-process (one active stream per thread, run as a single job — the chat
resolver queues concurrent messages), so parallel tool calls can't race
the shared kernel.
- **Output isolation:** `/home/user/output` is reset at the start of
each reused run, so a call only returns the artifacts it actually
produced; durable state lives elsewhere and persists.

## SDK upgrade

`@e2b/code-interpreter` **`^1.0.4` → `^2.6.0`** (pulls `e2b@2.x`). The
typed pause/resume API, `lifecycle`, and the `state`/`metadata` list
filter only exist in the 2.x line; 1.x exposed them only as untyped
OpenAPI internals. `Sandbox.list()` is now a paginator (handled).

## Config

| Var | Default | Purpose |
|---|---|---|
| `CODE_INTERPRETER_TIMEOUT_MS` | `300000` | Max single-execution
duration. |
| `CODE_INTERPRETER_IDLE_TIMEOUT_MS` | `300000` | Idle window before the
warm sandbox auto-pauses. |

Reuse is always-on when a session id is present (chat path). The
workflow-agent path and the dev-only `LocalDriver` are unaffected.

## ⚠️ Open item before merge: paused-sandbox GC

E2B retains paused sandboxes **indefinitely** (no TTL). Unlike the old
keepalive path (which auto-killed on idle), pause means a conversation's
sandbox persists after the chat ends — so without garbage collection,
paused sandboxes accumulate (≈ one per historical conversation) and
consume storage. A GC policy is required; the approach + retention
window are being decided (see PR discussion). Also: the E2B runtime path
can't run in CI, so this still needs a **live smoke test** (reuse hit,
idle→pause, resume) and confirmation of paused-storage pricing before
rollout.

## Tests / checks

- Resolver unit tests (`getOrCreateSessionSandbox`): reuse+extend,
create-when-absent, duplicate reaping, connect-failure fallback,
keep-first-connectable-when-earlier-dead, **ignore cross-tenant
metadata**, and **kill-on-timeout-refresh-failure**.
- `nx typecheck twenty-server` (against e2b 2.x), `oxlint --type-aware`,
`oxfmt --check` all clean.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-17 08:12:24 +00:00
Félix Malfait 74b56ba66c fix(onboarding): show the connect step after workspace creation (#21701)
## Problem

Creating a new workspace as an existing user (e.g. someone who already
has other workspaces) skips the **Connect account / Sync emails** step
entirely — the connect modal never appears.

## Root cause (regression from #21640)

`#21640` moved the `CONNECTED_ACCOUNTS` permission gate onto the
`WORKSPACE_ACTIVATION → SYNC_EMAIL` transition in
`getNextOnboardingStatus`:

```ts
// before #21640: WORKSPACE_ACTIVATION → PROFILE_CREATION (no permission read)
// after  #21640:
if (WORKSPACE_ACTIVATION) {
  return isAccountSyncEnabled ? SYNC_EMAIL : PROFILE_CREATION;
}
```

That transition fires from `CreateWorkspace.tsx` immediately after
`activateWorkspace()` + `loadCurrentUser()`. But
`setNextOnboardingStatus` is a memoized callback that captured
`isAccountSyncEnabled` at render time — *before* activation, when the
brand-new workspace has no roles/permissions yet, so
`currentUserWorkspace.permissionFlags` is empty and the flag reads
`false`. `loadCurrentUser()` refreshes the atoms, but the executing
callback still holds the stale `false`. So it optimistically routes to
`PROFILE_CREATION`, skipping `SYNC_EMAIL` and overriding the backend
status (which correctly says `SYNC_EMAIL`). For an existing user the
profile step is also a no-op (name already set), so they sail straight
into the app.

Before #21640 the same permission gate lived on the `PROFILE_CREATION →
SYNC_EMAIL` transition, which fires from the profile step — long after
activation, when permissions are loaded — so it never misfired.

## Fix

The backend (`OnboardingService.getOnboardingStatus`) decides the
connect step purely from `ONBOARDING_CONNECT_ACCOUNT_PENDING` and never
consults the permission. Drop the permission gate from the frontend
transition so the two agree — `WORKSPACE_ACTIVATION` always advances to
`SYNC_EMAIL`. The `WORKSPACE_ACTIVATION` branch only ever runs for
workspace creators (who are admins with the permission), so the gate was
only ever reachable via the stale read.

Removes the now-unused `isAccountSyncEnabled` / `usePermissionFlagMap`
plumbing and the obsolete "skip SyncEmail when account sync is disabled"
unit test.

## Test plan

- [ ] Create a new workspace as an existing user (with other workspaces)
→ the Connect account / Sync emails step now appears
- [ ] Fresh signup → onboarding still flows `Workspace activation → Sync
emails → Create profile → Invite team`
- [x] `useSetNextOnboardingStatus` unit tests updated (the "after
workspace activation → SYNC_EMAIL" case is retained and now
unconditional)
- [x] `npx nx typecheck twenty-front` — clean for changed files (only
the pre-existing, unrelated `idb-keyval` module-resolution errors remain
in this environment)
- [x] `npx nx lint:diff-with-main twenty-front` — changed files clean

> Note: the unit test file couldn't be executed in my sandbox because
`idb-keyval` (a declared dependency, pulled in transitively via
`jotaiStore`) isn't installed here; it runs normally in CI.

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

https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21701?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: Claude <noreply@anthropic.com>
2026-06-16 23:37:51 +02:00
Félix Malfait 35c2a24afb perf(onboarding): compute invite suggestions on-demand (#21696)
## Summary

Follow-up to #21640. In production, invite suggestions took ~1 minute to
appear because `FetchOnboardingInviteSuggestionsJob` ran on the shared
`calendarQueue` behind heavy calendar-sync jobs.

- **Drop the background job entirely.** `getInviteSuggestions` now
resolves the connected account from the authenticated
`@AuthUserWorkspaceId()` and computes suggestions on demand:
cache-first, with a bounded calendar fetch + cache write on a miss.
Removes the Google/Microsoft enqueues, the
`shouldComputeInviteSuggestions` threading through the auth controllers,
and the now-unused `shouldComputeInviteSuggestionsOnConnect` /
`isOnboardingConnectAccountPending` helpers.
- **Prefetch one step earlier.** New `usePrefetchInviteSuggestions` hook
fires the query from `CreateProfile` so the server cache is warm by the
time the invite step renders. `InviteTeam` switches from `network-only`
→ `cache-first`. If the profile step is skipped, the invite step still
computes on-demand (~1–3s, no queue) — no more minute-long waits.

No GraphQL schema change.

## Test plan

- [ ] Connect Google calendar in onboarding → invite step renders
prefilled teammates with no perceivable wait
- [ ] Connect Microsoft calendar in onboarding → same
- [ ] Onboard with workspace name already set so profile step is skipped
→ invite step still prefills (just with a brief on-demand fetch instead
of 1 min)
- [ ] Connect a non-work-email account → invite step renders empty form
(no suggestions)
- [ ] `npx nx typecheck twenty-server` 
- [ ] `npx nx lint:diff-with-main twenty-server` 
- [ ] `npx nx lint:diff-with-main twenty-front`  (changed files clean)
- [ ] `google-apis.service.spec.ts` + `microsoft-apis.service.spec.ts`
pass

https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21696?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: Claude <noreply@anthropic.com>
2026-06-16 21:25:02 +02:00
neo773 e50ec75cd0 fix(server): default timeline thread visibility to METADATA (#21669)
Orphaned messageChannelMessageAssociation rows (channel deleted in core,
association left behind when cleanup cron was down) made visibility
unresolvable, so formatThreads emitted null for the non-nullable
TimelineThread.visibility field and 500'd the whole timeline query.

Fail closed to METADATA (most restrictive existing tier) so a missing
channel hides subject/body instead of breaking the page.

/closes TWENTY-SERVER-FM6

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21669?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-16 18:48:06 +02:00
neo773 b076c35848 fix(messaging): pin Google OAuth2 client to native fetch (#21668)
/closes TWENTY-SERVER-HFH

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21668?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-16 18:47:30 +02:00
Raphaël Bosi d4c1fc86e3 Stabilize flaky Argos stories in twenty-front storybook (#21691)
Three twenty-front stories rendered non-deterministically and
intermittently tripped Argos as false positives (flaky on `main`, not
caused by any recent change). Each is now deterministic:

- **SettingsDataModelFieldSettingsFormCard › WithRelationForm**: the
relation preview was screenshotted mid-settle — it briefly shows the
fallback object/record before the form default and the sample record
load. Added a `play` that waits for the settled state so Argos captures
it consistently.
- **MultiSelectInput › SingleSelection**: the final deselect click left
a transient hover/tooltip. The play now moves the pointer off the option
and waits for the tooltip to disappear.
- **Breadcrumb › Default**: ambiguous intrinsic width made the last
crumb flip between "New" and "N…". Gave the story a fixed container
width.

Verified the three stories pass in the Storybook vitest runner across
repeated runs.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21691?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-16 16:37:53 +00:00
neo773 1ad919955a Support variables file email attachment (#21613)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21613?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-16 18:11:37 +02:00
neo773 d8d5991977 fix(messaging): honor IMAP/SMTP encryption setting instead of inferring it from the port (#21562)
This pull request makes the IMAP and SMTP encryption setting actually
honor what the user selects.

As per spec there's 3 modes: SSL/TLS (implicit TLS from the start),
STARTTLS (it will attempt TLS but if the server doesn't support it, it
gracefully falls back to plaintext), NONE (plaintext)

Current implementation had a boolean flag for this, this replaces it
with the 3 modes

Upgrade command to migrate all existing accounts, to not risk breaking
anyone's existing account in production we map each account to the mode
that matches its current behavior, so nothing changes on the wire

/closes #21300

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21562?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-16 18:09:04 +02:00
Thomas Trompette ff03e935ef feat(workflow): expand manual-trigger runtime payload with payload + _metadata (#21676)
## Summary

Step 1 (**expand**) of restructuring the manual-trigger output so record
fields live under a `payload` key and trigger-level metadata (the
running workspace member) lives alongside it. This step is **additive
and runtime-only** — no behavior changes for existing workflows, and
nothing new is surfaced in the variable picker yet.

The manual-trigger runtime payload now additively carries:
- `payload`: a mirror of the incoming record fields, reachable at
`{{trigger.payload.*}}`
- `_metadata.workspaceMemberId`: the member who ran the workflow,
reachable at `{{trigger._metadata.workspaceMemberId}}`

Record fields are still served at the trigger root, so existing
`{{trigger.id}}` references keep working unchanged. The output schema /
variable picker is intentionally left untouched here.

### Why `_metadata` (underscore)
During the transition, record fields still sit at the `trigger` root
next to the injected keys. Field API names can't start with `_`, so
`_metadata` is collision-proof against any record field; picking the
name now avoids a later variable-path rename migration.

### Phasing
- **Step 1 (this PR):** write `payload` + `_metadata` at runtime; keep
using direct `trigger.*`; don't display the new paths.
- **Step 2:** surface `payload` + `_metadata` in the variable picker.
- **Step 3:** migrate existing variables to `trigger.payload.*` and
contract the root record fields.

## Test plan
- [x] `twenty-shared` builds, `twenty-server` typechecks, lint clean on
changed files
- [x] Manual: run a manually-triggered (SINGLE_RECORD) workflow and
confirm the run's trigger payload contains `payload.*` mirroring the
record and `_metadata.workspaceMemberId`
- [x] Manual: confirm existing `{{trigger.id}}` references still resolve

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21676?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-16 15:49:12 +00:00
Félix Malfait 61309c45e6 feat(onboarding): prefill the invite step with teammates from the connected calendar (#21640)
## What & why

Implements core-team-issues#1414: move the calendar/email connection
earlier in onboarding and use the freshly connected calendar to prefill
the **Invite your team** step with likely teammates, so users don't
start from an empty form.

## Approach

Everything is behind the feature flag
`IS_ONBOARDING_INVITE_SUGGESTIONS_ENABLED` (off by default).

**Reorder** — onboarding becomes `Workspace activation → Connect account
→ Create profile → Invite team`. Connecting before profile gives the
calendar sync a head start; connecting *before* the workspace exists
isn't possible (a connected account requires an activated workspace +
workspace member + OAuth transient token). Gated in both
`OnboardingService.getOnboardingStatus` (backend) and
`useSetNextOnboardingStatus` (frontend) so the two agree.

**Fast teammate lookup** — on Google/Microsoft connect *during
onboarding*, a background job (`FetchOnboardingInviteSuggestionsJob`)
runs a single bounded calendar fetch (recent events, attendees inline),
keeps same-work-email-domain colleagues (excludes self + aliases;
personal mailboxes yield nothing), ranks by meeting frequency, and
caches the top 5. The invite step reads the cache via a new
`getInviteSuggestions` query and prefills the form — polling briefly
while the cache warms, and never overwriting input the user has already
typed.

Providers: **Google** (Calendar `events.list`) and **Microsoft** (Graph
`calendarView`), routed by a `CalendarAttendeesService` dispatcher
(mirrors the existing `CalendarGetCalendarEventsService`). Any fetch
failure (missing scope, API error) degrades to today's empty form via
the orchestrator's best-effort catch.

## How to enable

Turn on `IS_ONBOARDING_INVITE_SUGGESTIONS_ENABLED` for a workspace
(admin panel).

## Notes

- New-workspace creators only (invitees never see the connect/invite
steps). Skipping the connect step, or signing up with a personal email,
falls back to the current empty form.
- The "We found teammates from your calendar" subtitle only shows once
suggestions are actually prefilled.
- i18n: the new `<Trans>` strings are extracted on merge to `main` by
the existing Crowdin workflow.

## Testing

- Frontend unit tests for the reorder state machine (both flag states).
- `npx nx typecheck` and `npx nx lint:diff-with-main` green for
`twenty-front` and `twenty-server`.
- Server boots with the new DI wiring (no circular dependency);
`getInviteSuggestions` / `InviteSuggestion` present in the live metadata
schema.
- Not exercised in CI: live Google/Microsoft OAuth end-to-end (requires
real accounts + calendar data).

https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21640?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 <62795688+neo773@users.noreply.github.com>
Co-authored-by: neo773 <neo773@protonmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-06-16 15:45:11 +00:00
Raphaël Bosi 5b1cfa4cc0 Show app name on workflow logic function nodes (#21675)
Workflow logic-function nodes from installed integration apps now show
the owning app's name as the node label (e.g. "People Data Labs")
instead of the generic "Action". The descriptive title below it (e.g.
"Enrich Person") is unchanged.

This only applies to installed integration apps — plain custom-code
functions and standard functions keep showing "Action".

A new `useWorkflowNodeLabel` hook resolves the logic function's
`applicationId` and returns the app name for integration apps, falling
back to the node type otherwise. It's used by the editable, read-only,
and run node renderers.

## Before

<img width="572" height="766" alt="CleanShot 2026-06-16 at 16 26 42@2x"
src="https://github.com/user-attachments/assets/4ca9cf2f-5469-437d-a4dc-084057730589"
/>

## After

<img width="460" height="706" alt="CleanShot 2026-06-16 at 16 25 46@2x"
src="https://github.com/user-attachments/assets/504ed2ea-49a6-49c6-830e-2fdd214b8c61"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21675?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-16 14:30:23 +00:00
Raphaël Bosi 637ae65e9f Hide Input and Test tabs for app logic function nodes (#21671)
When opening a logic function node that comes from an app (e.g. "Enrich
Company" from People Data Labs), the node detail panel was showing
**Input** and **Test** tabs. App-sourced functions should only expose
their input fields, with no test surface.

This hides the tab list when the logic function has an `applicationId`,
rendering the input fields directly. The test tab is also never treated
as active for these nodes, which guards against a stale "test" tab state
leaking in from a previously-opened custom function (the tab list shares
one component instance id).

## Before

<img width="800" height="546" alt="CleanShot 2026-06-16 at 15 11 32@2x"
src="https://github.com/user-attachments/assets/c4582e27-9a41-4ebd-a436-49404cefb515"
/>

## After

<img width="800" height="454" alt="CleanShot 2026-06-16 at 15 11 09@2x"
src="https://github.com/user-attachments/assets/2756e07b-fa28-410a-be14-ad5f49e2d7ed"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21671?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-16 14:24:12 +00:00
Mani bharadwaj 9001078cb2 fix(cli): detect expired token on deploy and offer interactive re-auth (#21335)
## I have read the CONTRIBUTING.md file.

YES

## What kind of change does this PR introduce?

Fix (CLI) — `twenty deploy` now detects an expired/invalid API key on
the active remote and offers an interactive re-auth flow (TTY only). In
non-TTY contexts the behavior is unchanged: a clear error and a non-zero
exit.

Fixes #20197

## What is the current behavior?

After a workspace DB reset, key revocation, or workspace deletion, both
`twenty deploy` and (effectively) `twenty dev` fail with:

```
Upload failed: Token has expired.
```

The message is technically correct but gives the user no way forward.
They have to know to mint a new key from **Settings → Developers** and
re-run `twenty remote add --local --api-key <NEW_KEY>`. This came up
while testing PR #20181 and is the same friction on any DB reset, key
revocation, or workspace deletion.

## What is the new behavior?

Two changes, layered:

### (1) Better error message + remediation hint

When the upload returns a 401 or its message matches a token-expired
pattern (`/token has expired|unauthori[sz]ed|invalid api key/i`),
`appDeploy` now prints:

```
Your API key for remote "local" is no longer valid
(the workspace may have been reset, or the key was revoked).

Re-authenticate with:
  twenty remote:add --as local --api-key <NEW_KEY>

Generate a new key at: <SERVER_URL>/settings/developers
```

### (2) Interactive re-auth prompt (TTY only)

If the process is attached to a TTY, after the hint is printed the user
is prompted:

```
Re-authenticate now? (Y/n)
```

- **Yes** → re-validate the token (it may have been refreshed
externally), and if still invalid, instruct the user to re-run
`remote:add`. The original `appDeploy` is then retried once.
- **No** → the original `DEPLOY_FAILED` error is surfaced (same code,
better message).
- **Non-TTY (CI, scripts, redirects)** → the prompt is suppressed
entirely. The user gets the hint and a non-zero exit, preserving
scriptable behavior. **No change** to existing CI scripts.

## Acceptance criteria

| Scenario | Before | After |
|---|---|---|
| Happy path deploy |  works |  works (no change) |
| Deploy with expired key (TTY) | generic error, exit 1 | hint + prompt,
retry on Y, error on N |
| Deploy with expired key (CI / no-TTY) | generic error, exit 1 | hint +
exit 1 (no prompt, scriptable) |
| Deploy with unrelated error (e.g. 500) | generic error, exit 1 |
unchanged (no false positive on the matcher) |

## Reproduction

1. Spin up Twenty, mint an API key, run `twenty deploy` — confirm the
happy path.
2. Reset the DB (`core.appToken` cleared) and re-run `twenty deploy` —
confirm the new hint + prompt fire and the retry succeeds.
3. Repeat step 2 in a non-TTY context (e.g. `twenty deploy < /dev/null`
or via `script -qc ''`) — confirm the prompt is suppressed and the
scriptable exit-1 behavior is preserved.

## Implementation notes

- **`FileApi.uploadAppTarball`** now tags 401 responses with an
`isAuthError: true` flag on the failing `ApiResponse`. The existing
`error` string is still populated so callers that don't check the flag
continue to work — **additive, no breaking change**.
- **`FailingApiResponse<TError>`** gained an optional `isAuthError?:
boolean` field. The other `ApiResponse` call sites in the SDK don't need
to set it.
- **`@/cli/utilities/auth/reauth-helper.ts`** is new. It owns:
- `isTokenExpiredMessage(...)` — pure matcher, easy to unit-test, used
as a backstop if a non-401 message still says "expired" (GraphQL returns
200 with errors in some cases).
- `promptForReauthentication(remoteName)` — TTY-gated `inquirer.confirm`
prompt that re-validates the token and either returns
`'reauthenticated'`, `'declined'`, or `'non-interactive'`.
- **`@/cli/operations/deploy.ts`** is the single call site that wires
the helper. The helper is structured so it can be reused from the dev
orchestrator's upload step (a follow-up) without changes.
- **New unit test** at `__tests__/reauth-helper.test.ts` covers the
matcher: positive cases, negative cases, case-insensitivity, and nullish
input.

## Out of scope (per the issue)

- Long-lived dev tokens for `--local` remotes.
- Web-based OAuth login flow for the CLI (the existing
`authenticate(...)` flow in `remote.ts` is fine; the prompt here just
tells the user to re-run it).

## Files changed

```
packages/twenty-sdk/src/cli/operations/deploy.ts                              | 33 ++++++++
packages/twenty-sdk/src/cli/utilities/api/api-response-type.ts                |  1 +
packages/twenty-sdk/src/cli/utilities/api/file-api.ts                         |  8 +++
packages/twenty-sdk/src/cli/utilities/auth/__tests__/reauth-helper.test.ts    | 34 ++++++++++
packages/twenty-sdk/src/cli/utilities/auth/reauth-helper.ts                   | 61 ++++++++++++++++++
5 files changed, 137 insertions(+)
```

Happy to address feedback and split this into two PRs (hint-only first,
prompt-on-top) if the maintainers prefer a smaller first cut.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-16 14:14:17 +00:00
github-actions[bot] b8329e3e79 i18n - docs translations (#21681)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21681?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-16 16:24:19 +02:00
martmull 306a1454aa Update Connection provider path (#21678)
## Before

After connecting to oAuth linear app connection:

<img width="1512" height="851" alt="image"
src="https://github.com/user-attachments/assets/39b94aaf-648f-46a6-8f4d-deb1cb7e22c5"
/>

## After

Redirects to Linear

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21678?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-16 13:53:13 +00:00
Etienne 16a92f52a4 feat(admin-panel) - add billing/usage section (#21672)
Add billing/usage section

<img width="740" height="763" alt="Screenshot 2026-06-16 at 14 39 51"
src="https://github.com/user-attachments/assets/42db4fe4-3158-4ab8-aee5-28121ea530cd"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21672?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-16 13:18:43 +00:00
Paul Rastoin d5c7b735d2 ci: migrate cross-repo dispatch senders to workflow_dispatch (actions:write) (#21648)
# Introduction
Getting rid of the fine grained PAT used to dispatch to internal
repositories.
Repo dispatch requires the contents write permissions which is too wide
for such use
Refactored all senders and target to pass through a workflow dispatch
instead
Creating a centralize app that forges a token with actions: write only
provided permissions to mitigate any token exfiltrations
2026-06-16 15:18:43 +02:00
Félix Malfait 1e8169ca3e feat(ai-agent): suggest similar tool names when tool discovery misses (#21654)
## Context

When the in-product AI agent guesses a tool name that doesn't exist, the
discovery tools dead-end it with no way to recover.

The most common failure is a singular/plural slip — e.g. the agent tries
`group_by_cloud_user` when the real tool is `group_by_cloud_users`
(read/bulk tools are always plural; only `find_one_*` is singular).
Today both `learn_tools` and `execute_tool` reply with a flat `Could not
find: <name>` and no suggestion, so the agent burns turns guessing or
gives up.

## Change

- Add a `findSimilarToolNames` util that ranks catalog tool names
against the missed name by Levenshtein distance (reusing the existing
`getEditDistance`), with a small bonus for a shared `<operation>_`
prefix so the correct same-operation plural is ranked first rather than
a closer-but-different operation (e.g. `find_many_person` →
`find_many_people`, not `find_one_person`).
- `learn_tools`: when names aren't found, include `suggestions` in the
structured result and inline them in the message — `Could not find:
group_by_person (did you mean: group_by_people?).`
- `execute_tool` (via `ToolRegistryService.resolveAndExecute`): append
`Did you mean: …?` to the not-found error, reusing the catalog it
already fetched (no extra lookup).

The heuristic mirrors the existing workflow variable-path suggestion
util (same edit-distance threshold), so behavior is consistent with that
prior art.

## Tests

- Unit tests for `findSimilarToolNames`: plural recovery, prefix-aware
ranking, distance threshold, 3-suggestion cap, empty catalog.
- `learn_tools` tool tests: suggestions surfaced on a miss; no
suggestion lookup when all names resolve.

`nx typecheck twenty-server` passes; `oxlint --type-aware` and `oxfmt
--check` are clean on the changed files.

https://claude.ai/code/session_01GMjZkJYkqTJogJJTM6AAV8

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21654?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-16 13:06:57 +00:00
Raphaël Bosi e515f174a4 Load twenty-ui global styles in twenty-front Storybook (#21665)
Loads `twenty-ui/style.css` in twenty-front's Storybook preview,
matching the real app — `src/index.tsx` already imports it, but
`.storybook/preview.tsx` only loaded the deprecated stylesheet, so
Storybook rendered without twenty-ui's global `reset.scss`
(`*{box-sizing:border-box}` + a native `button` reset) that the
production app has.

This isolates the resulting box-model/button-reset baseline shift ahead
of the `twenty-ui-deprecated` removal PR. The Argos diff here is exactly
that global shift (Storybook catching up to how the app already renders)
and is expected — bulk-approve. Once this lands, the removal PR's Argos
will show only genuine deprecated→twenty-ui component diffs instead of
being buried under ~240 reset-driven diffs.

Only the reset applies in this PR: twenty-ui's component CSS targets
hashed module classes that don't exist in the deprecated-rendered
Storybook, and `reset.scss` is the only global selector twenty-ui emits.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21665?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-16 12:12:49 +00:00
Thomas des Francs a67fdf2dac Clean up MCP Monaco editor (#21643)
Rebuild the MCP setup block on the shared Monaco editor with theme-driven colors, padding, and auto-height; add the settings description line-height token; revert the clipboard util to the shared navigator.clipboard implementation; and consolidate the code editor's auto-height to a single disposed, reactive effect.
2026-06-16 13:57:44 +02:00
Thomas des Francs ce92a1f1e1 Update GitHub banners and remove unused assets (#21655)
saves 24mb
duly checked
2026-06-16 10:59:59 +00:00
nitin b7c68e082e Enable inline field editing in calendar event details (#21666)
before - 


https://github.com/user-attachments/assets/b6645bb5-0ffc-4281-b054-2035c865ba77




after - 


https://github.com/user-attachments/assets/a62a1134-c959-4281-aefe-b04e310b4926



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21666?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-16 10:52:54 +00:00
Charles Bochet b327999b04 fix(server): run 2-14 standard relation label/icon heal as a system build (#21667)
## Problem

The `2-14:fix-standard-relation-field-labels-icons` workspace upgrade
command aborts the upgrade, failing for every workspace that has drift
to heal with:

```
FIELD_MUTATION_NOT_ALLOWED: System fields only allow updating: universalSettings, isActive. Forbidden properties: icon
```

The default relation fields it heals (note/task/attachment/timeline) on
standard objects are **system-owned**. The flat-field-metadata validator
forbids mutating any property other than `universalSettings`/`isActive`
on a system field **unless the migration runs as a system build**
(`buildOptions.isSystemBuild`). The command omitted `isSystemBuild`, so
it defaulted to `false` and the heal was rejected.

## Fix

Pass `isSystemBuild: true` when building the heal migration — consistent
with every other standard-metadata upgrade command (2-3, 2-5, 2-7, 2-8,
2-9, 2-10, 2-13, other 2-14 commands).

## Notes

- Follow-up to #21658, which added the error-surfacing diagnostics that
revealed this root cause but did not include this fix.
- `label` and `icon` are both in
`FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE`, so the
relation-field validator (not gated on `isSystemBuild`) already permits
them — the system-build flag was the only blocker.
- Dry-run returns before the build step, so this only manifests on real
runs.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21667?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-16 12:59:26 +02:00
Charles Bochet 8205c97b5b fix(route-trigger): return 422 instead of 500 for logic function execution errors (#21656)
## What

Return HTTP 422 instead of 500 for `LOGIC_FUNCTION_EXECUTION_ERROR` in
the route trigger exception filter.

## Why

When a logic function's user code fails (e.g. an HTTP call inside the
function returns a 502 from an upstream service), the exception was
mapped to HTTP 500. This caused two problems:

- **Sentry noise**: `shouldCaptureException` captures all 5xx responses,
so every user-code failure was reported as a platform error. This
generated ~56k Sentry events over 2 months for a single workspace's
logic function hitting a transient upstream 502.
- **Webhook retry loops**: Webhook senders like GitHub auto-retry on 5xx
responses, amplifying the event count.

`LOGIC_FUNCTION_EXECUTION_ERROR` is a user-code error, not a platform
error. A 422 (Unprocessable Entity) correctly signals that the request
could not be processed due to the logic function's own failure, without
triggering Sentry capture or webhook retries.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21656?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-16 10:45:54 +00:00
nitin 5cb8a091fc Add Recall webhook status handler to the meeting bot app (#21659)
Adds a `recall-webhook` logic function (`POST /webhook/recall`,
unauthenticated) to the `twenty-meeting-bot` app. It verifies the
Recall/Svix `whsec_` signature over the raw body, parses bot lifecycle
events, matches the corresponding `CallRecording` (by
`twentyCallRecordingId` metadata, falling back to `externalBotId`), and
updates lifecycle fields — `status`, `externalBotId`,
`externalRecordingId`, and `startedAt`/`endedAt` (only when unset) —
guarded against stale out-of-order events that would move the status
backwards. Adds the required `RECALL_WEBHOOK_SECRET` server variable.

This opens the real provider test path: install the app → schedule a bot
through the existing calendar-event flow → point a Recall webhook
endpoint at Twenty → bot lifecycle events update the matching
`CallRecording`.

Unit tests cover signature verification, status mapping, the downgrade
guard, metadata/bot-id matching, and timestamp fill.

Deferred to later PRs:
- transcript/media ingestion, file uploads, and the completion charge
(so `COMPLETED` is never set here)
- repair/reconcile cron jobs

Also flips `DEFAULT_RECALL_REGION` to `eu-central-1` (separate commit)
to match the Recall account region.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21659?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 10:38:15 +00:00
Charles Bochet ee6c9db33a fix(server): surface validation errors in 2-14 fix-standard-relation-field-labels-icons upgrade command (#21658)
## Problem

The \`2-14:fix-standard-relation-field-labels-icons\` workspace upgrade
command threw a generic error on migration build failure:

\`\`\`ts
if (result.status === 'fail') {
throw new Error(\`Migration failed for workspace \${workspaceId} while
healing standard relation field labels/icons\`);
}
\`\`\`

This discarded \`result.report\` entirely — the structured per-field
validation failures (\`code\`, \`message\`, \`value\`, offending field)
— making real-world upgrade failures impossible to diagnose from logs.
On a recent staging/app-main upgrade, 13 workspaces failed here with no
actionable detail.

## Change

Flatten \`result.report\` into both the logged error and the thrown
message, so failures now print the actual validation errors per field,
e.g.:

\`\`\`
[fieldMetadata] <universalIdentifier> -> SOME_VALIDATION_CODE: <real
reason>
\`\`\`

No behavior change beyond logging/error content — the command still
aborts on failure as before.

## Notes

- Dry-run still returns before the build step, so this only surfaces on
real runs (unchanged).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21658?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-16 12:02:25 +02:00
Félix Malfait c3835a839f fix(ui): default Monaco CodeEditor to scrollBeyondLastLine: false (#21657)
## Problem

In the AI chat "Python Code Execution" panel (`CodeExecutionDisplay`),
scrolling the code goes far past the last line, leaving a large empty
area below the code.

The root cause is in the shared `CodeEditor`: Monaco's
`scrollBeyondLastLine` option **defaults to `true`**, which lets the
viewport scroll roughly a full editor height past the last line. The
shared component never set this option, so the Monaco default leaked
through to every consumer.

This affected every read-only viewer built on `CodeEditor` — the code
interpreter, `WorkflowReadonlyActionCode`,
`WorkflowStepExecutionResult`, and
`SettingsLogicFunctionTriggerPayloadFormat` — and each one had to
remember to disable it (only `WorkflowEditActionCode` did).

## Change

Set `scrollBeyondLastLine: false` in the shared `CodeEditor` defaults,
in both the active `twenty-ui-deprecated` copy and the `twenty-ui` copy
(kept in sync). Since `options` is spread last, any editor that
genuinely wants scroll-past-end can still opt back in with
`scrollBeyondLastLine: true`.

## Impact

- **Auto-fixed** the read-only viewers that showed dead scroll space
(code interpreter, workflow readonly action, step execution result,
trigger payload sample).
- The editable editors that previously inherited Monaco's default
(`SettingsLogicFunctionCodeEditor`, `RawJsonFieldInput`,
`SettingsLogicFunctionTestTab`, `ConfigVariableDatabaseInput`) now also
stop at the last line — consistent with `WorkflowEditActionCode`, which
already opted out. Any of these can re-enable scroll-past-end via
`options` if desired.

## Testing

Behavior verified by inspection against Monaco's option semantics and
existing usages. Note: dependencies were not installed in the authoring
environment, so `lint`/`typecheck` were not run locally —
`scrollBeyondLastLine: false` is a standard, type-safe Monaco option
already used with this component elsewhere. Worth a CI check.

https://claude.ai/code/session_01CWzyw1spKF8Dog9E5tcdj4

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21657?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: Claude <noreply@anthropic.com>
2026-06-16 11:52:05 +02:00
Charles Bochet d4b597e14e fix(docker): run twenty-server in production mode (NODE_ENV=production) (#21635)
## What

Set `ENV NODE_ENV=production` in the `twenty-server` Docker stage
(inherited by `twenty-server-aws` and `twenty`).

## Why — root cause of the empty verification emails

Verification emails have been delivered with an **empty body** in
deployed environments — the subject renders ("Bienvenue chez Twenty :
Veuillez confirmer votre email") but the HTML is `<!DOCTYPE
…><!--$!--><template></template><!--/$-->`, an **errored React Suspense
boundary**. Prod logs showed:

```
TypeError: dispatcher.getOwner is not a function
```

Chain:
1. The prod server stages never set `NODE_ENV`, so at runtime it's
**unset** → React loads its **development** builds.
2. Since the **React 18→19 upgrade** (#21531), React 19's dev JSX
runtime calls `dispatcher.getOwner()` (React 18's did not — which is why
this is a recent regression).
3. That call throws in the server runtime → the email React tree throws
during SSR → `@react-email/render` (which streams without an `onError`)
swallows it into an errored `<Suspense>` boundary → the body ships
empty. The subject is unaffected because it's built without React.

Production React never tracks owner, so `jsx()` never calls `getOwner` —
setting `NODE_ENV=production` removes the failing code path entirely.
It's also simply the correct prod configuration (dev React builds are
slower and emit dev-only behavior).

## Scope / safety

- Only the prod runtime stages are affected. The dev image
(`twenty-app-dev`) keeps its own `NODE_ENV=development`.
- Verified the deployed image currently bakes `NODE_ENV=[]` (unset) and
has a single, matched `react`/`react-dom` 19.2.7 — so this is a mode
issue, not a duplicate-React issue.

## Related
- Mitigation already in place: twentyhq/twenty-infra#729 disables email
verification on prod-eu meanwhile (revert once this ships).
- Diagnostic logging: #21628 (can be reverted after this lands).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21635?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-16 11:16:09 +02:00
Charles Bochet 14d8105f22 fix(front): remove runtime default-view creation fallback (#21652)
## Problem

`useCreateDefaultViewForObject` was a temporary runtime fallback that
created a view + one view field per field (each with a fresh `v4()` id)
whenever `RecordIndexLoadBaseOnContextStoreEffect` found no view for the
current view id. Because the created view got a fresh id that never
matched the requested `contextStoreCurrentViewId`, the next load missed
again and re-created another duplicate — leaking `core.view` /
`core.viewField` rows without bound (notably during the 2.13.0
cache-first bootstrap window).

#21592 made the fallback idempotent as a stop-gap, but the mechanism is
no longer needed at all: standard/index views are created server-side at
object creation and during standard app installation, so the client
never needs to mint them.

## Change

Remove the fallback entirely:
- Delete `useCreateDefaultViewForObject`.
- In `RecordIndexLoadBaseOnContextStoreEffect`, when no view resolves
for the current id, do nothing and let the loaded views settle (the
effect re-runs once the view is present and loads it).

## Note

This removes the leak at the source for any client running the new
bundle. Clients still on old cached JS will keep creating duplicates
until they reload; the already-leaked rows are being cleaned up
separately via SQL.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21652?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-16 11:08:48 +02:00
Raphaël Bosi e83fa2108d Add single-record People Data Labs enrich functions (company & person) (#21650)
Adds two single-record enrichment logic functions to the People Data
Labs app — `enrich-company` and `enrich-person` — that call PDL's
single-record Enrichment endpoints (`/company/enrich`,
`/person/enrich`). Each function declares both a workflow-action trigger
and an AI-tool trigger, so the same function is usable as a workflow
step and as an AI tool. They take a single `{ recordId,
overrideExistingValues? }` and return a single `EnrichResult`.

The new functions replace the previous `enrich-company-tool` /
`enrich-person-tool` AI-tool functions (which delegated to the bulk
endpoints), avoiding duplicate near-identical tools for the LLM. The
bulk `enrich-companies` / `enrich-people` workflow actions are
unchanged.

Implementation reuses the existing enrichment machinery: the
single-record adapters spread the existing company/person adapters and
only override `enrichBatch`, so identifier extraction, TTL guard, field
mapping (fill-only-if-empty), billing, and error backoff all carry over.
A new `post-pdl-single-enrich` util posts params directly and classifies
the response via the existing `parsePdlItem`.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21650?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 08:54:38 +00:00
Etienne ceb7698689 fix(ai) - workflow tool outputs optim + display fix (#21500)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21500?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-16 08:50:16 +00:00
nitin 12b1dba986 Add call recording scheduling backend (#21629)
This PR adds the backend scheduling slice for call recording. It wires
the `twenty-meeting-bot` internal app to reconcile calendar events,
calendar-channel associations, and workspace member auto-record
preference changes, then schedule, cancel, or reschedule Recall bots
based on the resulting policy.

It also adds the needed calendar-channel owner lookup support, generated
metadata updates, app config/default role updates, unit tests, and CI
for the internal app.

Coming next:
- Recall webhook handling and signature validation
- Stale-state convergence for failed Recall cleanup/recreate cases
- Media, transcript, audio, and video ingestion
- Billing charge flow
- Frontend/settings UI for recording controls

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21629?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 08:47:09 +00:00
Rashad Karanouh 065b6efe11 fix(twenty-partners): reuse existing company by domain in partner-application handler (#21615)
## Problem

Partner applications **502** for any applicant whose company is already
in the CRM.

The `submit-partner-application` logic function dedupes applicants
**only by person email**. When no person matches that email, it takes
the create path and calls `createCompany` unconditionally. But
`Company.domainName` has a **UNIQUE index**, so whenever a company with
the applicant's domain already exists — which is common, since the **TFT
import seeds companies** — the mutation throws `"duplicate entry"`. The
handler's `catch` returns `{ ok: false }`, and the website
`/api/partner-application` route surfaces it as a **502**. The applicant
can never be submitted.

Real case that surfaced this: an applicant whose company (`BKG
Integration UG`, domain `bkg-integration.de`) was already present from
the TFT import with no Partner/Person attached.

## Fix

Extract `findOrCreateCompanyId`:
- Look the company up by **exact domain** (`domainName.primaryLinkUrl
eq`) and **reuse** it when found.
- Only `createCompany` when no domain matches.
- The matched company is **never renamed** — the existing CRM name wins
over the applicant's free-text `companyName`.

Person-email dedup is unchanged (already handled upstream in the
handler).

### Known limitation
Matches **active** rows only. A *soft-deleted* company still holds the
unique index and would re-collide; clear those with `yarn purge:prod`.
Noted inline.

## Tests
Adds an integration test: pre-seed a company by domain → submit an
application with the same domain → assert the partner reuses the same
company id and the company name is untouched.

## Version
`twenty-partners` 0.5.1 → **0.5.2** (patch: bug fix, no schema change).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21615?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 07:17:29 +00:00
Raphaël Bosi 504eaa5600 Fix relation-traversal filters showing no options on select fields (#21616)
Relation-traversal filters (e.g. *Company → Industry*) on
`SELECT`/`MULTI_SELECT` fields rendered an empty option picker, so the
filter couldn't be configured.

The value inputs resolved the select options from the **source relation
field** (which has no options) instead of the **relation target field**.

Fixed in both filter UIs:
- Dashboard chart filters and workflow "Find Records"
(`AdvancedFilterSidePanelValueFormInput`)
- Record-index simple & advanced filters and the role-permission filter
builder (`ObjectFilterDropdownOptionSelect`)

Both now resolve the value-input field and its options from
`relationTargetFieldMetadataId` when a filter traverses a relation,
falling back to the source field otherwise.

## Before

<img width="802" height="702" alt="CleanShot 2026-06-15 at 17 27 35@2x"
src="https://github.com/user-attachments/assets/032b0875-8a5a-4e09-a2ad-4c4a8a319f49"
/>

## After

<img width="804" height="796" alt="CleanShot 2026-06-15 at 17 27 05@2x"
src="https://github.com/user-attachments/assets/88248409-5781-49c7-aa9e-3502ff8c68e4"
/>

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