Commit Graph

4826 Commits

Author SHA1 Message Date
Harshit Raizada 01bb2f4ab2 fix: enforce strict rules for currency value handling by ai chatbot (#21470)
opportunity:
<img width="382" height="75" alt="image"
src="https://github.com/user-attachments/assets/be443c29-0bca-4537-a775-01cdbf704cdb"
/>
fix: 
<img width="382" height="289" alt="image"
src="https://github.com/user-attachments/assets/11aa9552-f3ac-4d25-b5aa-efbacfba3a13"
/>

closes #21419

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-18 10:31:13 +02:00
Etienne 39e00d5853 feat(workflow): expected output schema for runtime-output steps + validation (#21744)
## Summary

Extends the workflow validation layer (introduced in #21422) and adds a
new
"expected output schema" capability for steps whose output structure is
only
known at runtime.

Some workflow steps (HTTP Request, Code, Logic Function, AI Agent
(coming soon), Webhook
trigger) don't have a statically known output shape, so downstream steps
can't
resolve `{{step.x.y}}` variable paths or validate them. This PR lets
users
declare a **sample/expected output** for those steps, derives an output
schema
from it, and uses that schema both to power variable resolution and to
surface
validation issues at build time.

## What's included

### Expected output schema (shared schemas + types)
- New `expectedOutputSchemaShape` reused across the HTTP request, code,
logic
function and AI agent action settings schemas, plus the webhook trigger
  schema (`expectedOutputSchema` optional loose object).
- Mirrored on the server-side action/trigger settings types.

### Output schema computation (server)
- `workflow-schema.workspace-service` now computes a step's output
schema from
  the user-declared `expectedOutputSchema` sample (via
`getOutputSchemaFromValue`) when no statically computed schema is
available.

### Validation layer (server)
- `STEP_HAS_NO_VARIABLE_REFERENCE` (warning): flags steps of
`VARIABLE_CONSUMING_ACTION_TYPES` (HTTP_REQUEST, CODE, LOGIC_FUNCTION,
SEND_EMAIL, record CRUD) that reference no upstream variable.
- `LOGIC_FUNCTION_OUTPUT_SCHEMA_MISMATCH` /
`AI_AGENT_OUTPUT_SCHEMA_MISMATCH`
(warnings): compare the declared output schema against the expected
sample
using the new shared `getOutputSchemaMismatchIssues` util (missing keys,
  leaf/object mismatches, type mismatches).
- Trigger is now validated alongside steps (trigger type requirements +
  trigger variable references).
- Validation issues no longer return both `suggestions` and
`availablePaths`
  when they are identical (avoids redundant, costly payloads).

### Shared utilities
- New `getOutputSchemaMismatchIssues` (+ tests) in
`twenty-shared/logic-function`.
- Moved `agentResponseSchemaToOutputSchema` from `twenty-front` into
  `twenty-shared/ai` so it can be reused on both sides.

### Frontend
- New `WorkflowExpectedOutputBodyInput` component (JSON sample editor
with
validation) used by HTTP request, code, logic function and AI agent step
  editors.
- New `resolvePersistedStepOutputSchema` util + `useStepsOutputSchema`
update:
  resolves a step's output schema from `outputSchema`, falling back to
  `expectedOutputSchema`, with an AI_AGENT default.
- HTTP request / code / logic function editors persist
`expectedOutputSchema`
  and derive `outputSchema` from it.
- Webhook trigger default settings include `expectedOutputSchema`.


BONUS : iterator loop validation

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21744?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-18 10:31:01 +02:00
Félix Malfait f96e36d3e6 fix(ai): prevent chat thread bricking from tool parts with null input (#21752)
## Problem

Fixes #21695.

An AI chat thread became **permanently unusable** — every subsequent
message failed with `AI_APICallError: Internal server error` from
Anthropic — when the thread history contained a tool part in
`output-error` state with a **null input** (e.g. a tool call that failed
input validation before execution, so neither `toolInput` nor
`toolOutput` was ever captured).

## Validation of the reported findings

I reproduced and confirmed the root cause empirically against the pinned
`ai@6.0.97` SDK before writing the fix.

**Root cause (confirmed from SDK source).** `convertToModelMessages`
serializes every non-`input-streaming` tool part into a provider
`tool_use` block, and for errored parts it uses:

```ts
input: part.state === 'output-error'
  ? (part.input ?? ('rawInput' in part ? part.rawInput : undefined))
  : part.input,
```

When both `input` and `rawInput` are nullish, the block is built with
`input: undefined`, which `JSON.stringify` drops — so the HTTP payload
carries a `tool_use` with **no `input` field**. This matches the
reporter's minimal repro exactly (no `input` → `400 Field required`;
`input: {}` → `200`). Inside a large streamed conversation the same
malformed block surfaces as the generic `500`, and because the bad part
is replayed on every turn the thread stays bricked.

**Why #21276 didn't catch it.** `finalizeDanglingToolParts` only rewrote
`input-available` parts; a part that arrives already in `output-error`
with a null input was passed through untouched.

**Note on current `main`.** A read-path default added recently
(`mapDBPartToUIMessagePart`: `input: part.toolInput ?? {}`) already
masks the live 500 on the standard reload path. However the gap is real
and worth closing: the persist path still writes `toolInput = NULL` (the
exact malformed rows the reporter found in `core."agentMessagePart"`),
`finalizeDanglingToolParts` still doesn't normalize this case, and the
protection rested on a single implicit default with no regression
coverage. A small repro harness confirmed all of this: persisted
`toolInput` was `undefined`, and a raw (non-defaulted) `output-error`
part produced a `tool-call` whose `input` value was `undefined`.

## Fix

Defense-in-depth so the invariant *"a tool part always carries a defined
input"* holds at both the finalize and storage boundaries:

- **`finalizeDanglingToolParts`** now backfills `input: {}` for
`output-error` parts whose input is null, while preserving the original
error message. This is the natural chokepoint (it already runs
immediately before every persist).
- **`mapUIMessagePartsToDBParts`** defaults a nullish tool input to `{}`
so malformed rows are never persisted, independent of the caller.

The existing read-path `?? {}` default is kept as a third safety net.

## Tests

- Unit tests for `finalizeDanglingToolParts`: backfills `{}` for an
`output-error` part missing its input, and preserves the existing
validation error message.
- Persistence test: `mapUIMessagePartsToDBParts` stores `{}` (never
`null`) for a missing input.
- End-to-end round-trip test: after finalize → persist → reload,
`convertToModelMessages` produces a `tool-call` with a defined input and
the errored call stays resolved.

All three new core assertions were verified to **fail without the fix**
and pass with it. Full AI module suite (97 tests) passes; `oxlint
--type-aware`, `oxfmt`, and `tsgo` typecheck are clean.

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

https://claude.ai/code/session_01SpuX6Pp2yTevk1zKTRiB9G

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21752?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 21:13:51 +02:00
Weiko a1f79c4f40 fix(server): enforce lowercase universalIdentifier in sync (#21754)
## Context

App sync fails when a manifest defines an entity with an uppercase UUID
`universalIdentifier`. Postgres `uuid` columns normalize to lowercase on
write, but the sync diff matches `universalIdentifier` strings
case-sensitively. So an uppercase-defined entity never matches its
lowercased DB row and is seen as delete + create on every sync, which
trips downstream guards like "Parent navigation menu item not found".

## Change

Reject non-lowercase `universalIdentifier`s at validation time in
`WorkspaceEntityMigrationBuilderService.validateUniversalIdentifier`
(right after the existing UUID-v4 check). This lives in the abstract
base builder, so it covers every syncable entity type. App authors now
get a clear "must be lowercase" error on the first sync instead of
confusing downstream failures.

Validation is sufficient here, no normalization needed — because the DB
side is always lowercase, so rejecting uppercase input guarantees both
sides of the diff match.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21754?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 19:19:46 +02:00
Etienne d99e479be8 feat(billing) - facilitate top up in ai chat (#21645)
Today, when a trialing user hits their AI usage cap inside the Ask AI
chat, ending the trial bounces them to the Stripe billing portal (and,
for card-less users, loses their place in the conversation). This PR
makes activating a paid plan / topping up credits feel seamless from
within the chat:

Trial users with a card on file activate their subscription in place,
without leaving the app.
Trial users without a card are sent to the Stripe payment-method portal
and, on return, the trial is ended automatically and they're dropped
back into the exact Ask AI thread they came from.
Credit-exhaustion and trial banners now reflect whether a payment method
exists (Add Credit Card vs Subscribe Now / End Trial Period) and upgrade
inline via a confirmation modal instead of redirecting to Settings.


Uploading Screen Recording 2026-06-16 at 07.51.12.mov…


https://github.com/user-attachments/assets/4ea77273-da63-4b32-b6f1-5ac9e9560651



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21645?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 16:20:11 +00:00
nitin 177afde866 [BREAKING CHANGE] fix chart cache collisions with key-based data plumbing (#21743)
closes
https://discord.com/channels/1130383047699738754/1514946035317997709

This fixes Apollo cache collisions for pie slices and line series by
keeping chart bucket identity as key end-to-end, matching how bar chart
already works.


What changed -- 

- Renamed pie/line chart response identity from id to key in the chart
data path.
- Kept key through frontend chart hooks, types, stories, and
tooltip/drilldown logic.
- Only adapt key to id at actual external boundaries like Nivo and
GraphWidgetLegend.
- Added/updated tests covering cache normalization and chart data
behavior.


before - 

<img width="2600" height="844" alt="CleanShot 2026-06-17 at 20 17 14@2x"
src="https://github.com/user-attachments/assets/b9ee83e9-db4b-423e-8668-a7beb4c4c62e"
/>

after - 

<img width="2614" height="800" alt="CleanShot 2026-06-17 at 20 16 17@2x"
src="https://github.com/user-attachments/assets/674a5417-ffc2-441d-9484-e1126438254c"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21743?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 16:13:27 +00:00
Félix Malfait 02a3a3c47c fix(ai): handle dynamic-tool message parts in chat persistence (#21740)
## Summary

Fixes #20558. AI chat streams crashed with `Unsupported part type:
dynamic-tool` whenever the model emitted a *dynamic* tool call (a tool
that isn't part of the bound schema). The assistant message never
persisted, so the user saw a hard failure mid-stream.

## Root cause

The AI SDK v6 emits two flavors of tool parts:
- **Static** — `type: "tool-<toolName>"` (e.g. `tool-execute_tool`)
- **Dynamic** — `type: "dynamic-tool"`, with the name on `part.toolName`

`mapUIMessagePartsToDBParts` recognised tool parts with a homegrown
check:

```ts
part.type.includes('tool-') && 'toolCallId' in part
```

That returns `false` for `'dynamic-tool'` (it contains `-tool`, not
`tool-`), so dynamic parts fell through to `throw new
Error(\`Unsupported part type: ${part.type}\`)` during the
`handleStreamFinish` persistence step. Stack trace from the issue
matches exactly.

The same broken heuristic was duplicated in:
- `packages/twenty-server/.../mapDBPartToUIMessagePart.ts` (reverse
mapper)
- `packages/twenty-front/.../utils/mapDBPartToUIMessagePart.ts`
(frontend mirror — would also throw on a `dynamic-tool` row reloaded
from history)

Meanwhile, two other call sites in the codebase
(`finalize-dangling-tool-parts.util.ts`, `isThinkingStepPart.ts`)
already correctly use the SDK's `isToolUIPart`, which natively
recognises both flavors.

## What this PR does

1. **Switches all three mappers to the SDK's canonical check**
(`isToolUIPart` on the forward path; explicit `dynamic-tool` + `tool-`
startsWith on the reverse paths, where the input is an entity/DTO, not a
UI part).
2. **Persists `toolName`** — the column already existed on the entity,
DTO and GraphQL fragment but nothing wrote it. For static parts the name
is recoverable from `type`; for dynamic parts it's the only place the
name lives, so without it the round-trip is impossible. The shared
denormalisation also helps existing per-tool analytics
(`count-native-web-search-calls-from-steps.util.ts`).
3. **Reconstructs `dynamic-tool` parts on read** (with `toolName`) so
they survive a DB round-trip both on the server and on the frontend
history view.
4. **Adds a round-trip unit test** covering both `dynamic-tool` and a
static tool part to lock the behavior in.

## Architecture notes (called out for review)

- `mapDBPartToUIMessagePart` is duplicated frontend + backend because
the input shape differs (TypeORM entity vs. GraphQL DTO). Out of scope
to consolidate here, but they're drifting — this PR is what that drift
looked like in production. Worth a follow-up to express the shared logic
once over a unified row type.
- I left the existing renderer guard `part.type !== 'dynamic-tool'` in
`AiChatAssistantMessageRenderer.tsx` alone — it's a reasonable UI-side
decision to not attempt to render an unknown dynamic tool generically.
Persistence and history reload now work; rendering of dynamic tool calls
is a separate UX decision.
- No DB migration needed — the `toolName` column already exists. Old
static rows have `toolName: null`; the reverse mapper recovers their
name from the `type` column as before. Old dynamic-tool rows don't exist
(they all threw on write).

## Test plan
- [x] `yarn workspace twenty-server jest map-message-parts.dynamic-tool`
— 5 passed
- [x] `yarn workspace twenty-server jest
finalize-dangling-tool-parts.roundtrip` — still 4 passed (no regression)
- [x] `yarn nx typecheck twenty-server` — clean
- [x] `yarn nx typecheck twenty-front` — clean
- [x] `yarn nx lint:diff-with-main twenty-server` — clean
- [x] `yarn nx lint:diff-with-main twenty-front` — clean
- [ ] Manual: trigger an AI chat that exercises a dynamic tool (e.g. via
an MCP server returning a tool not in the bound schema) and confirm the
stream finishes and the message persists.

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

https://claude.ai/code/session_013EE11eVWtyxmdcbEHVJKoc

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21740?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 18:12:21 +02:00
Thomas Trompette 105f9565a5 feat(workflow): surface manual-trigger payload + metadata in variable picker (#21692)
## Summary

Step 2 of the manual-trigger output schema restructuring (expand →
display → migrate → contract).

Builds on the now-merged #21676 (which expanded the runtime payload to
serve `payload` and `metadata` siblings at the trigger root). This PR
**surfaces** those in the variable picker as nested, expandable nodes:

- `trigger.payload.{record fields}` — the record(s) that triggered the
run
- `trigger.metadata.workspaceMemberId` — who triggered it

The flat root fields (`trigger.id`, etc.) remain available, so existing
saved variable references keep working until a later migration phase
moves them.

### Changes
- **twenty-shared**: metadata/payload label constants +
`build-manual-trigger-metadata-node` util + barrel exports.
- **twenty-front**: `computeStepOutputSchema` MANUAL branch now nests
`payload` (RecordNode for SINGLE_RECORD, array Node for BULK_RECORDS,
omitted for GLOBAL) and `metadata`; `ManualTriggerOutputSchema` type
updated to `{ payload?; metadata }`.
- **twenty-server**: `computeTriggerOutputSchemaFromAvailability`
mirrors the same nested shape for server-side validation.

The key is `metadata` (not `_metadata`) — custom fields can't start with
`_`, so collision risk was deemed acceptable.

## Test plan
- [x] `npx nx build twenty-shared`
- [x] `computeStepOutputSchema` unit tests pass (55)
- [x] Manual: create a manual-trigger workflow (GLOBAL / single-record /
bulk), confirm the picker shows `payload` and `metadata` as expandable
folders and that selecting a field yields `{{trigger.payload.<field>}}`
/ `{{trigger.metadata.workspaceMemberId}}`

> Note: server typecheck has pre-existing unrelated failures on main
(Stripe billing mocks, gmail mocks); none touch workflow files.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21692?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:54:35 +00:00
Weiko 607d9ee6e5 fix(server): allow app-defined permission flags to be referenced by a role in the same sync (#21742)
## Context

When an application defined custom permission flags and a role
referencing them in the same sync, installation failed, first at
validation (Permission flag not found) and then at execution (Migration
action 'create' for 'rolePermissionFlag' failed).

Root cause: both the migration builder order and the runner execution
order processed rolePermissionFlag before permissionFlag, so the role's
flag assignments were validated/inserted before the flags they reference
existed.

## Changes

- Builder order: run the permissionFlag builder before
rolePermissionFlag so newly created flags are visible in the optimistic
maps when assignments are validated.
- Execution order: order the permission-flag actions so definitions are
created before assignments, and assignments deleted before definitions,
keeping the FK satisfied in both directions.
- In-use check: move the "flag still assigned to a role" guard out of
the per-entity deletion validator (order-dependent, false-positived when
a flag and its assignments were deleted together) into a new
order-independent validatePermissionFlagNotInUseCrossEntity (aligned
with existing validateObjectMetadataCrossEntity,
validateViewFieldLabelIdentifierCrossEntity, ...), run after all
builders against the migration's final state.

This fixes both the create path (define flag + reference it in one sync)
and the teardown path (delete flag + its assignments in one sync).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21742?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:44:01 +00:00
martmull 102c530d0f Add limit on view widget (#21718)
<img width="1345" height="463" alt="image"
src="https://github.com/user-attachments/assets/a5d9ac2f-6375-4956-895d-3675aa9bebc1"
/>


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

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-06-17 14:43:31 +00:00
Paul Rastoin 60b559a659 Provide custom workspace id while seeding (#21721)
# Introduction
Currently working on e2e test ci that will iterate over dedicated twenty
instance.
In order to allow multi concurrent tests to be performed we need to
isolate testing context
Allowing to provide custom workspaceId allow easy isolation and post
test cleanup on aws related account

close https://github.com/twentyhq/core-team-issues/issues/2556

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21721?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:29:27 +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
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
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
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 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
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
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
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
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
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
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
Weiko f0c3883fd1 fix(command-menu-item): persist overrides after save and add reset-to-default (#21623)
## Context
Command menu items are an overridable entity (like page-layout / FIELDS
widgets), but the override flow in layout-customization mode was broken:

- **Move / pin-unpin / hide-label didn't persist.**
`useSaveCommandMenuItemsDraft` fired the `updateCommandMenuItem`
mutations (backend persisted correctly) but never wrote the result back
into `metadataStoreState`, the source the live menu and edit panel read
from. So the UI reverted on exit and changes only showed after a hard
reload.
- **No true "reset to default".** Existing reset controls only reverted
the draft to the last-saved values (which still contained overrides).
there was no way to clear overrides back to the original values after a
save.

Notes
- removed the footer "Reset to default" button. This is not clear to me
how we want to build, let's re-implement better in the next version
- reset are done on click and not delayed on the save. This is similar
to other reset to default on page layouts where we actually usually
reload the component and this is because the FE has no idea what's
original VS override from the response itself

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21623?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 18:10:42 +02:00
Charles Bochet 2de60d7ea1 chore(server): temporary diagnostic logging for empty verification email body (#21628)
## What

Adds **temporary** diagnostic logging to
`EmailVerificationService.sendVerificationEmail` so we can capture the
real error behind the empty verification email body in deployed
environments.

## Why

Verification emails are delivered with an **empty body** (subject is
fine). The body is `<!DOCTYPE html
…><!--$!--><template></template><!--/$-->` — an **errored React Suspense
boundary**.

`@react-email/render`'s `render()` wraps the email in `<Suspense>` and
streams via `renderToReadableStream` **without an `onError` handler**,
so any throw during SSR is swallowed into the errored boundary and the
body ships empty. In production React also strips the error text from
the markup, so the cause is invisible.

This could **not** be reproduced locally on `main` (renders fine in dev,
in the production-focused `yarn workspaces focus --production` install
layout, and on the React 18 + react-email 6 dep set), so we need the
error from a deployed environment.

## What it logs

When the rendered html is empty or contains `<!--$!-->`, it logs (prefix
`EMAIL_VERIFICATION_RENDER_DEBUG`):
- locale, trigger, html length, and the first 400 chars of the html;
- the **real error + stack**, obtained by re-rendering synchronously
with `renderToStaticMarkup` (which re-throws instead of swallowing).

No behavior change on the happy path — the block only runs when
rendering already failed.

## How to use

Deploy, trigger a verification email (sign up / resend), then:

```
grep -i "EMAIL_VERIFICATION_RENDER_DEBUG" <twenty-server logs>
```

## Revert

Remove this block once the root cause is identified.
2026-06-15 17:51:08 +02:00
twenty-pr[bot] 206120677b chore: bump version to 2.15.0 (#21624)
## Summary

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

## Checklist

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

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

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-06-15 17:01:36 +02:00
Paul Rastoin fdab89ae02 Move twenty-client-sdk to dev dep (#21611)
# Introduction
The `twenty-client-sdk` is always provided and injected at runtime by
the twenty-server instance
Which mean that even if in your app locally you're using
twenty-client-sdk `1.0` installing this app on twenty instance `2.0`
will result in injecting another `twenty-client-sdk`

That's the expected behavior and tradeof

The twenty-app devdep should only be used to guide local devxp following
typesafety and so on

A user can still locally generated its own twenty-client-sdk and publish
it if necessary

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21611?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 14:46:22 +00:00
Charles Bochet 97871131a1 fix(server): mitigate integration-test OOM flakiness (#21588)
## Problem

`server-integration-test` shards have been failing intermittently across
unrelated PRs with a distinctive signature: the shard exits code 1 with
**no jest assertion failure, no `Test Suites:` summary, and no V8
`JavaScript heap out of memory` error** — the process just dies mid-run.
Failures hit random shards and clear on re-run (e.g. an unrelated branch
failed shard 6 once, then passed 3× on identical code), while the
`merge_group` gate stays green.

### Root cause

Each shard runs a **single in-band jest process** that boots one shared
NestJS app (`globalSetup` → `app.listen`) and holds it for the entire
shard, driving heavy metadata migrations + cache rebuilds in that one
process. `NODE_OPTIONS=--max-old-space-size=12288` let V8 grow to 12 GB
— *above* the `ubuntu-latest` runner's available RAM (16 GB, shared with
Postgres/Redis/ClickHouse). V8 therefore deferred aggressive GC and grew
past physical memory, so the **OS OOM-killer killed the process before
V8 hit its own ceiling** — which is why there's no heap error and no
jest summary, just a silent exit.

## Changes (CI/test-only — prod runtime untouched)

- **Lower the integration jest heap cap `12288` → `6144`** so V8
self-limits below physical headroom instead of being OS-killed.
Counterintuitively safer: a real leak now surfaces as a *visible* heap
error naming the test, rather than a silent death. (`database:reset`
keeps 12288 — it runs alone, before jest.)
- **Add `--logHeapUsage`** to the integration jest runs to expose the
per-file heap trend for confirming/pinpointing the growth.
- **Split integration tests across 16 shards (was 10)** to lower the
peak working set per shard.
- **Make perf logging a first-class `LoggerService` tool** (per
@prastoin's review): add `LoggerService.perf()` and unify the existing
`time()`/`timeEnd()` helpers into `perfTime()`/`perfTimeEnd()` (now
routed through the driver), all gated by a new `PERF_LOG_ENABLED` config
var. It **defaults on** so real environments keep emitting the
install-perf logs, and `.env.test` sets it `false` to mute the
per-action flood in integration tests. The `application-manifest` and
`validate-build` services were moved from the built-in `Logger` to
`LoggerService` to use it.

## Notes

- `--max-old-space-size` lives only in the `test:integration` nx target;
it is **not** the prod server heap setting, so prod is unaffected.
- This is mitigation. If `--logHeapUsage` shows monotonic growth across
files, there's a real accumulation in the long-lived app (retained
flat-maps / metadata cache) worth a follow-up heap-snapshot fix.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21588?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 15:54:51 +02:00
Félix Malfait 2f6a267b68 chore(server): remove stray comment in flat-entity-maps spec (#21599)
Follow-up to #21585: removes an explanatory comment in the test that
slipped through before merge.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21599?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 13:22:21 +00:00
nitin 8a866dba54 Add call recording schema and meeting bot scaffold (#21584)
## Summary
- add 2.13 upgrade commands for call recording request status and
dropping CalendarEvent recordingPreference
- remove the recording preference from the core CalendarEvent standard
object
- add a scaffold-generated twenty-meeting-bot app with logo and the
CalendarEvent meetingBotPreference field

## Tests
- yarn install
- yarn lint
- yarn twenty dev:typecheck
- git diff --check


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21584?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-15 15:33:12 +02:00
Marie 4be76e3fd1 Support morph relations in workflow record nodes (#21403)
## Support morph (polymorphic) relations in workflow record nodes

Morph relations (e.g. a polymorphic `Owner` on `Pet` targeting `Person`
or `Company`) were not selectable in the workflow **Create / Update /
Upsert Record** nodes. This PR adds full support for setting them.

### What changed

**Frontend**
- `shouldDisplayFormField`: allow `MORPH_RELATION` (many-to-one) so
morph fields appear in record forms.
- New `FormMorphRelationToOneFieldInput`: a polymorphic record picker
across the morph's target objects, storing a self-describing value `{
targetObjectMetadataId, id }`.
- Wired the morph branch into `FormFieldInput`.

**Backend**
- New `formatWorkflowRecordMorphRelationFields` util: resolves the form
value (stored under the base field name, e.g. `owner`) into the correct
per-target join column (`ownerCompanyId`), nulling siblings to keep
exactly one target referenced.
- Wired into the create / update / upsert workflow actions (update also
expands `fieldsToUpdate` to the concrete join columns).

### Permissions handling
- The picker's search is scoped to only the morph targets the user can
read (`canReadObjectRecords`), so it no longer breaks when a target
object is inaccessible.
- If an existing value points to an object the user can't read, the
field shows the reused **"Not shared"** lock display instead of an empty
field, while remaining editable when other targets are readable.

### Notes
- No data schema / migration changes — reuses the existing per-target
morph columns and stores the selection in the existing workflow step
JSON settings.

<img width="607" height="717" alt="Screenshot 2026-06-10 at 14 57 40"
src="https://github.com/user-attachments/assets/496442a1-04a5-40f8-8b56-b28e38b00d5a"
/>

Also handles the case where the selected record is not readable
<img width="596" height="737" alt="image"
src="https://github.com/user-attachments/assets/c5ffb94e-3838-4db5-853e-f8e490331f23"
/>
2026-06-15 12:56:17 +00:00
Félix Malfait 02d6e2d76f perf(server): avoid O(n²) when building flat entity maps (#21585)
## Problem

After 2.13, server CPU stepped up and stayed up. Sentry profiling of
`POST /metadata` pins it on
`addFlatEntityToFlatEntityMapsThroughMutationOrThrow` — ~26% self-time
plus a long tail, turning metadata-write requests into multi-second
(~18s observed) operations.

The hot stack is:

```
WorkspaceMigrationValidateBuildAndRunService.computeAllRelatedFlatEntityMaps
 └ getSubFlatEntityMapsByApplicationIdsOrThrow
    └ addFlatEntityToFlatEntityMapsThroughMutationOrThrow
```

Every metadata migration rebuilds the twenty-standard application's flat
sub-maps — thousands of entities, across every involved metadata type —
through this util.

## Root cause

`addFlatEntityToFlatEntityMapsThroughMutationOrThrow` maintains
`universalIdentifiersByApplicationId` and deduped each insert with
`Array.includes`:

```ts
if (!existingUniversalIdentifiers.includes(flatEntity.universalIdentifier)) {
  existingUniversalIdentifiers.push(flatEntity.universalIdentifier);
}
```

That scan is O(n) per insert, so building a map for an application with
`n` entities is **O(n²)**. The twenty-standard application groups
thousands of standard entities under one `applicationId`, so its sub-map
rebuild dominates.

The dedup is also redundant: the function throws `ENTITY_ALREADY_EXISTS`
at the top if the `universalIdentifier` is already in
`byUniversalIdentifier`, and every id pushed to the per-application list
is also written there. So reaching the push guarantees the id is new —
the `.includes()` is always `false`.

## Fix

Drop the scan and push directly → map building is **O(n)**. Behavior is
unchanged (the early throw already enforces uniqueness).

## Test

Adds a unit spec covering indexing, the no-`applicationId` case, the
duplicate throw, and a 20k-entity build that completes instantly (guards
against re-introducing the quadratic).

## Follow-up (separate PR)

This is the bleed-stopper. The deeper issue is that
`computeAllInvolvedApplicationIds` pulls the **entire** twenty-standard
application into the dependency set of every migration and rebuilds
those sub-maps per request instead of caching them. Worth scoping the
dependency set to referenced entities (or caching the standard-app
sub-maps), which I'll raise separately.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21585?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 14:22:44 +02:00
Charles Bochet 0a99f784eb fix(front): dedupe morph relation fields in view field pickers (#21580)
## Issue

Reported in quality-feedbacks: **"Issues with morph relation view
field"** — a morph relation column added to a view **disappears after
refresh** (and can be added several times).

## Root cause — the SSE metadata sync

A morph relation is stored as **one `fieldMetadata` row per target
object**, all sharing a `morphId`. Collapsing those rows into the single
field that represents the relation is a **read-time projection** in the
server's `objects.fieldsList` resolver — it is *not* a storage
invariant, and the rows are never merged.

The frontend metadata store is kept in sync with the raw rows **one row
at a time over SSE** (`MetadataStoreSSEEffect`): every metadata change
broadcasts a single created/updated record that's pushed straight into
the store. Creating a morph relation creates N rows (one per target), so
**N `create` events arrive and N raw sub-fields land in the store —
bypassing the `fieldsList` projection entirely.**

The view-field pickers read straight from that store, so they saw the
morph relation **once per target**. Each could be added as a column
referencing a different sub-field id; after a refresh the view reloads
from the projected (deduped) data, the non-survivor columns no longer
resolve, and they disappear.

## Fix & architecture note

Because the store deliberately mirrors raw rows (that's what the SSE
sync maintains), the fix applies the **same read-time projection on the
client** — deduping morph rows by `morphId` in
`useActiveFieldMetadataItems` — rather than filtering rows at each
insert path (SSE, optimistic create, …). This matches how the backend
already models morph fields and is robust regardless of which path
delivered the rows.

The survivor-selection rule (which sub-field id represents the relation)
now lives in `twenty-shared` (`pickMorphGroupSurvivor`) so client and
server can't drift.
2026-06-15 11:56:35 +00:00
martmull c848ac34fd Fix default view widget visibility (#21590)
Tim logged on the left, Phil on the right, Tim created view widget 

## Before

<img width="1512" height="938" alt="image"
src="https://github.com/user-attachments/assets/7f993f41-a244-42db-b56a-b17c15fb3409"
/>

## After Fix
Tim created a second view widget, Phil can see it

<img width="1512" height="982" alt="image"
src="https://github.com/user-attachments/assets/3908a70d-538b-4adf-95df-7373a2f6e269"
/>

## After slow migration

Phil can see first widget

<img width="1512" height="982" alt="image"
src="https://github.com/user-attachments/assets/6b30429e-1acf-4119-ba32-26db3155975e"
/>


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

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-06-15 10:36:07 +00:00
twenty-pr[bot] 5f59ae20bf chore: bump version to 2.14.0 (#21593)
## Summary

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

## Checklist

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

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

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-06-15 12:17:20 +02:00
Charles Bochet fb4608e437 chore(deps): upgrade Tier-1 deps (googleapis 173, gaxios 7, express 5, jsdom 29, date-fns 4, stripe 20) (#21570)
## What

Security-driven upgrade of the biggest-drift Tier-1 dependencies
(staying on latest = staying patched). Bundled because they share the
lockfile and the googleapis/gaxios pair must move together.

| Package | From | To | Gap |
|---|---|---|---|
| googleapis | 105.0.0 | **173.0.0** | 68 majors |
| gaxios | 5.1.3 | **7.1.5** | 2 majors |
| express | 4.22.2 | **5.2.1** | 1 major |
| jsdom | 26.1.0 | **29.1.1** | 3 majors |
| date-fns | 2.30.0 | **4.4.0** | 2 majors |
| date-fns-tz | 2.0.0 | **3.2.0** | 1 major |
| stripe | 19.3.1 | **20.4.1** | 1 major |

`yarn npm audit` reports **0 high/critical** advisories before and
after.

## Code changes

- **gaxios v7** — `GaxiosError.code` is now `string | number` (guard the
calendar network-error check by `typeof`); `GaxiosError` config/response
use `URL` + `Headers`; and crucially the v7 constructor drops
`response.data` unless `bodyUsed` is set — updated the synthetic gmail
error mocks accordingly (production gaxios sets it, so real error
parsing is unaffected).
- **google-auth-library / gaxios dedup** — `googleapis-common@8.0.2`
exact-pins `google-auth-library@10.5.0` + `gaxios@7.1.3` while
`googleapis` pulls `^10.2.0`; the two copies made
`OAuth2Client`/`GaxiosError` type-identities diverge across every
gmail/calendar service. Added two singleton `resolutions` (documented
inline in root `package.json`).
- **express 5** — no source changes. `@nestjs/platform-express@11.1.24`
already resolves `express@5.2.1` internally; the old `4.22.2` pin was
the override.
- **jsdom 29** — no source changes, but it now pulls ESM-only transitive
deps (`@csstools/*` `.mjs`, `parse5`, `entities`, `tough-cookie`,
`@exodus/bytes`). Extended the server jest `transformIgnorePatterns`
allowlist and added `.mjs` to the transform/extensions so jest can load
jsdom.
- **stripe 20** — `Subscription` gained a required `customer_account`
field; added to mocks. No runtime changes.
- **date-fns v4** — `Locale` is no longer ambient (import explicitly in
5 files); per-locale entrypoints dropped the typed `default` export (the
locale loader now reads the single named export); fixed the default
locale import in `formatTimeZoneLabel`.

## Tests

- Full suites green locally: **twenty-server 5709 passed**,
**twenty-front 4937 passed**, twenty-ui / twenty-ui-deprecated green;
typecheck + builds (swc + vite) + lint all pass.
- Added regression tests for the two runtime behaviors these upgrades
touch and that had no coverage:
  - `getDateFnsLocale` — named-export locale resolution (date-fns v4).
- `sanitizeFile` — jsdom 29 + DOMPurify still strips `<script>`/event
handlers from uploaded SVGs (security guard).

## Deliberately deferred (not in this PR)

- **stripe → 21/22**: stripe **21** bundles a runtime `Decimal` type for
money fields **and** jumps the pinned API version to `2026-03-25.dahlia`
(changes webhook/billing payload behavior) — too risky to fold into a
deps bump on billing code. stripe **22** additionally drops the
node10-resolvable `types` entry, which would force a repo-wide
`moduleResolution` change. Capped at the latest clean **20.x**.
- **openid-client → 6**: v6 is a full functional rewrite and its
passport strategy manages the OAuth `state` internally, but our SSO flow
uses `state` to carry `identityProviderId` across the shared
`/auth/oidc/callback`. That needs an auth-flow redesign (session-carried
provider id) on Enterprise SSO code with no integration harness — it
deserves its own focused PR rather than riding along here.

## Tier-1 source

Originated from a dependency-drift audit; remaining Tier-1 items
(date-fns done here) plus Tier-2/3 follow-ups tracked separately.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21570?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 10:23:42 +02:00
Manish Kumar 25b0e4d81c fix: #19173 correct labels and icons for custom object default relations (#19224)
**### Problem**
When creating a custom Data Model object, the auto-generated Note and
Task relations had incorrect labels ("Note Targets", "Task Targets") and
a wrong hardcoded icon (IconBuildingSkyscraper).

Expected behavior is to use user-friendly labels ("Notes", "Tasks") and
proper icons, consistent with standard objects like Company and Person.

**Root causes:**

* `icon` in `createFieldInput` was hardcoded to
`'IconBuildingSkyscraper'`
* `label` was derived from `targetFlatObjectMetadata.labelPlural`, which
returns system labels (e.g., "Note Targets") instead of display labels

---

**Fix**

* Added `sourceFieldOverridesByRelationObjectNameSingular` map to define
correct labels and icons for all default relation types
* Ensures consistency with standard objects

Mappings:

* noteTarget: "Note Targets" → "Notes", IconBuildingSkyscraper →
IconNotes

* taskTarget: "Task Targets" → "Tasks", IconBuildingSkyscraper →
IconCheckbox

* attachment: "Attachments" → "Attachments", IconBuildingSkyscraper →
IconFileImport

* timelineActivity: "Timeline Activities" → "Timeline Activities",
IconBuildingSkyscraper → IconTimelineEvent

* favorite: "Favorites" → "Favorites", IconBuildingSkyscraper →
IconHeart

* Added type safety using:
`satisfies Record<(typeof
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS)[number], ...>`
  This ensures new default relations must be explicitly defined

* Renamed variable:
  `icon` → `targetFieldIcon`
  for better clarity (it is only used for the target field)

---

**Limitations**

* Applies only to newly created custom objects
* Existing objects will keep incorrect labels/icons
* Requires a separate data migration to fix existing data

---

**Testing**

1. Go to Settings → Data Model
2. Create a new custom object
3. Verify:

   * Labels show "Notes" and "Tasks" (not "Note Targets"/"Task Targets")
   * Icons match those used in standard objects (e.g., Company, Person)


---

## Update (reworked while merging main)

The original approach was reworked:

- The label/icon mapping now lives in a shared
`STANDARD_RELATION_FIELD_PROPERTIES_BY_RELATION_OBJECT` constant
(`msg`-based, so labels stay translatable), used as the single source of
truth. Dropped the unused `favorite` entry.
- Standard objects now reference that same constant explicitly at each
call site (uniformization) instead of duplicating the values. Objects
that intentionally differ keep their explicit overrides: note/task →
`Relations`, person/workspaceMember → `Events`, workflow attachments →
`IconFileUpload`.
- Fixed an unrelated typo found along the way: Company's
`timelineActivities` icon was `IconIconTimelineEvent`.
- For the history (supersedes the "Limitations" above): added a `2.9.0`
workspace upgrade command
`upgrade:2-9:fix-standard-relation-field-labels-icons` that re-syncs
**standard** objects' default relation labels/icons against the source
of truth. It deliberately leaves **custom** objects untouched — their
relation fields are user-editable and must not be overwritten by an
upgrade.

## Testing / Verification

Verified locally end-to-end:

**New custom objects**
- Created a custom object via the Data Model UI and via the metadata API
— its note/task/attachment/timeline relations now show `Notes` / `Tasks`
/ `Attachments` / `Timeline Activities` with the correct icons instead
of `Note Targets` + `IconBuildingSkyscraper`.

**Standard uniformization (value-preserving)**
- Re-seeded a workspace on this branch and inspected all 25
default-relation field definitions across the 10 standard objects: every
canonical value is unchanged, every intentional variant (Relations /
Events / IconFileUpload) is preserved, and the only diff vs `main` is
the Company `IconIconTimelineEvent` → `IconTimelineEvent` fix.

**Upgrade command (existing workspaces)**
- Simulated a real upgrade: seeded a workspace on `main` (Company icon
typo present), created a custom object via the metadata API (it came out
with the old buggy labels, as expected on `main`), then switched to this
branch and ran the command.
- Confirmed via both the metadata API and direct DB inspection:
Company's standard `timelineActivities` icon healed to
`IconTimelineEvent`, while the custom object's relations were left
untouched.
- Idempotent: re-running reports "already up to date".

**CI**: typecheck, lint, server unit tests, and all server
integration-test shards green.

---------

Co-authored-by: Manish Kumar <manishkumar@Mac.lan>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-14 21:06:58 +02:00
Félix Malfait 0baa333809 feat(lint): forbid data mutations in fast instance command up() (#21547)
## Why

Fast instance commands run in the ArgoCD **PreSync** hook, before the
new pods roll. A bulk `UPDATE`/`INSERT`/`DELETE` held in the **same
transaction** as an `ADD COLUMN`/`ALTER` keeps an `ACCESS EXCLUSIVE`
lock on the table for the whole write, blocking every read of it. That
is what froze prod during the 2.13 `isUIReadOnly → isUIEditable` rename
— a bulk `UPDATE "fieldMetadata"` inside the same `up()` transaction as
the `ADD COLUMN`s → read timeouts → failed PreSync → aborted sync.

@charlesBochet already caught this exact pattern by hand on #21527
("data migration => make a slow instance command :)"). This turns that
manual review into something CI enforces.

## What

New oxlint rule **`twenty/no-data-mutation-in-fast-instance-command`**:
- Flags statement-leading `UPDATE`/`INSERT`/`DELETE`/`MERGE` passed to
`.query(...)` **inside `up()`** of a `*-instance-command-fast-*` file.
- Allows: schema DDL (`ALTER`/`CREATE`/`DROP`); `ON DELETE CASCADE` / a
column named `updatedAt` (not statement-leading, so never matched);
rollback DML in `down()`; and data migrations in **slow** commands'
`runDataMigration()`.
- The error message points the author straight at the slow-command
pattern.

Enabled as `error` in `twenty-server`.

## Grandfathering

Scoping to `up()` means **only one** existing file violates the rule:
the already-shipped 2.13 rename command. It's recorded complete in cloud
and must not be rewritten, so it's grandfathered with a documented
file-level `oxlint-disable` (the comment makes clear it's an exception,
not a precedent). The four other fast commands that contain DML keep
theirs in `down()` and are correctly unaffected.

## Tests

- 9 RuleTester cases — valid: DDL, FK cascade, `updatedAt`, `down()`
DML, slow-command DML, non-upgrade files; invalid:
`UPDATE`/`INSERT`/`DELETE` in `up()`.
- Verified end-to-end with oxlint: a throwaway violating file → 1 error;
all 141 upgrade-command files → 0 errors; full oxlint-rules suite
225/225; typecheck clean.

Part of the v2.13 deploy post-mortem follow-ups.

https://claude.ai/code/session_013Az1etaGyxWRRVhgjhPWeB

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21547?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-14 20:44:54 +02:00
Charles Bochet 7c0136b97b feat(deps): migrate frontend to React 19 (#21531)
## What

Migrates the frontend stack from **React 18.3 → 19.2**. The website,
sdk, companion and emails packages were already on React 19; this brings
the remaining holdouts (`twenty-front`, `twenty-ui`,
`twenty-ui-deprecated`, `twenty-front-component-renderer`) and
`twenty-server`'s email rendering onto 19, and pins a single React
version repo-wide.

## Why

React 18.x is now the legacy line. Staying current keeps us on the
patched/maintained branch and unblocks downstream library majors
(react-router 7, mantine 9, etc.) that require React 19 peers.

## Dependency bumps (required by React 19 peers / removed APIs)

| Package | From | To | Reason |
|---|---|---|---|
| react / react-dom | 18.3.1 | 19.2.3 | core |
| @hello-pangea/dnd | 16 | 18 | peer `^18 \|\| ^19` |
| react-datepicker | 6 | 9 | v<7 used removed `findDOMNode`; drops
`@types/react-datepicker` |
| react-data-grid | beta.13 | beta.59 | peer `^19.2`; new render API |
| graphiql (+ @graphiql/react, plugin-explorer) | 3 / 0.23 / 1 | 5 /
0.37 / 5.1 | peer `^18 \|\| ^19` |
| react-helmet-async | 1.3 | **@dr.pogodin/react-helmet** 3.2 | upstream
caps peer at `^18`; drop-in React 19 fork |

A `resolutions` pin enforces a single React (19.2.3) + `@types/react`
(19.2.14) across the monorepo to avoid duplicate copies / type-identity
splits. Versions are the aged lockfile patches (clears the
`npmMinimalAgeGate`).

## Code changes

- **Global `JSX` shim** (`react-jsx-global.d.ts` per package): React 19
moved the `JSX` namespace under `React.JSX`; several deps' published
types (notably `@linaria/react`'s `styled.d.ts`, which types every
`styled.x` via `keyof JSX.IntrinsicElements`) still reference the global
namespace. Without the shim, every styled component degrades to `any`
props.
- **Ref nullability**: `useRef<T>(null)` now returns `RefObject<T |
null>`; widened consumer prop/hook ref types accordingly (incl. the
shared `useListenClickOutside`).
- **react-datepicker v9**: `onChange`/`onSelect` accept `Date | null`,
`calendarStartDay` typing, `ReactDatePickerProps`→`DatePickerProps`,
relaxed the dynamic `selectsMultiple` discriminated union.
- **react-data-grid beta.59**: `formatter`→`renderCell`,
`editor`→`renderEditCell`, `headerRenderer`→`renderHeaderCell`,
`components`→`renderers`, `onRowClick`→`onCellClick`, object-shaped
`useRowSelection`, Set-based selection.
- **dnd style cast**: `@radix-ui/react-popper` augments `CSSProperties`
with a `--radix-*` index signature that dnd's closed `DraggingStyle`
doesn't satisfy → cast at the spread.

## Status / testing

-  `typecheck` green: twenty-front, twenty-ui, twenty-ui-deprecated,
twenty-front-component-renderer, twenty-server
-  build / lint / unit tests / storybook+argos / runtime smoke-test in
progress

Draft until local + CI verification completes. Notable behavior to QA
manually: spreadsheet import (data-grid), date pickers, drag-and-drop
boards/lists, GraphQL playground, page titles/favicon.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21531?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-14 15:42:22 +02:00
Félix Malfait 9901fa93d9 fix(server): write 2.13 UI capability flags directly, bypassing validation (#21543)
## Context

The `v1.22 → v2.13.x` cross-version upgrade test (twenty-infra) still
fails *after* #21537. This is the **same root cause from a deeper
layer**, and the fix here ends the class.

## What's actually happening

The 2.13 `SyncStandardUiCapabilityFlags` workspace command heals drifted
`isUIEditable`/`isUICreatable` on standard metadata by running a **bulk
update through `validateBuildAndRunWorkspaceMigration`** — the
validation pipeline meant for *user-initiated* metadata edits. That
pipeline has multiple "you may not mutate property X on entity Y"
guards, and `isSystemBuild: true` only bypasses **some** of them:

| guard | gated on `isSystemBuild`? |
|---|---|
| system-**field** allow-list (`flat-field-metadata-validator.ts:83`) |
 bypassed |
| system-**object** guard (`flat-object-metadata-validator.ts:63`) | 
bypassed |
| **relation-field** allow-list (`flat-field-metadata-validator.ts:143`)
|  not gated |

So the healing command fails on exactly the workspaces that have real
drift (the genuinely cross-version-upgraded ones). #21537 patched the
relation allow-list by adding `isUIEditable` to it — one guard — and the
build then failed on the next. From this run's logs: `Upgrade summary:
42 workspace(s) succeeded, 2 workspace(s) failed` (the 2 drifted
workspaces; the build returns `status=fail`, the per-workspace error
detail isn't surfaced in logs).

**Root cause:** a trusted system flag-backfill should not run through
the user-mutation validation layer at all.

## Fix (direct metadata write)

`isUIEditable`/`isUICreatable` are UI-affordance columns on
`core.fieldMetadata`/`core.objectMetadata` — changing them needs **no
workspace-schema migration**. The command now writes them **directly**
to those tables (mirroring the 2.13 slow backfill's raw `UPDATE
core."objectMetadata"`) and invalidates the flat-metadata cache,
bypassing the validation pipeline entirely. Drift detection is
unchanged. This removes the whole class of guard rejections instead of
patching guards one at a time.

## Verification

- `nx typecheck twenty-server` , `oxlint --type-aware`  (the file is
intentionally oxfmt-ignored via `**/upgrade-version-command/**`).
- ⚠️ I could **not** run a live cross-version repro from the dev
container (no Docker/Postgres available here). The fix categorically
can't hit the previous failure (the validation pipeline is gone), but
the definitive runtime gate is the twenty-infra `cross-version-upgrade`
job against a new image. Quick local repro to confirm: on a reset dev
DB, flip `isUIEditable` on a standard relation field (e.g. an
`activityTargets` `target*` field) so it drifts, run `yarn command:prod
upgrade:2-13:sync-standard-ui-capability-flags -w <workspaceId>`, and
confirm it completes (pre-fix it threw on the relation field).

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21543?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-14 08:15:50 +02:00
Félix Malfait 09f0c9e29a fix(address): coerce addressLat/addressLng to numbers in ORM result formatting (#21542)
## Fixes #21390

Saved addresses render as the **"Empty"** placeholder in the record
detail / side panel when `addressLat`/`addressLng` are populated (e.g.
after picking a Google autocomplete suggestion). The list view shows the
address correctly.

## Root cause

`addressLat`/`addressLng` are `NUMERIC` composite subfields, stored as
Postgres `numeric` columns — which the `pg` driver returns as
**strings** to preserve precision.

The ORM result formatter already normalizes this for Currency, but not
for Address. In
`packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts`,
`formatCompositeFieldValue` had a case for `CURRENCY.amountMicros`
(`parseInt`) but **no case for `ADDRESS`**, so coordinates were passed
through as raw strings.

This only breaks the **record detail**, not the list view, because:

- The **standard GraphQL (Yoga) path** masks it — the `BigFloat`
scalar's `serialize()` runs `parseFloat()` and quietly turns the string
into a number on the wire.
- The **direct-execution path** formats results itself and bypasses
scalar serialization, so the string reaches the frontend. There,
`addressFieldValueSchema` validates lat/lng with `z.number()` →
`isFieldAddressValue` returns `false` → `isFieldValueEmpty` returns
`true` → `RecordInlineCellDisplayMode` renders the placeholder. The
table cell renders the value directly with no empty-check, so the list
view is unaffected.

## Why it surfaced now

The `z.number()` constraint on lat/lng is old ("latent since the address
guard was introduced"). The trigger was **#19254 (2026-04-03) "Remove
direct execution feature flag"**, which made direct execution always-on
for workspace queries — the same PR added string→number coercion for
aggregates but not for composite subfields. **#21033** (the PR the issue
blames) only made `addressStreet1` nullable; it didn't touch lat/lng,
but by fixing the overlapping null-street1 case it isolated and exposed
this one.

## Fix

Add the `ADDRESS` case to `formatCompositeFieldValue`, mirroring
Currency. Coordinates are fractional, so `parseFloat` is used (Currency
uses `parseInt` because micros are integers). This is the exact
operation the `BigFloat` scalar already performs, so there is no
behavior change on the standard path — it just makes direct execution
consistent, and lat/lng are now numbers everywhere (matching
`FieldAddressValue`). No frontend change is needed.

## Scope check — similar bugs in other field types/composites

This bug class = a transforming scalar `serialize` that direct execution
doesn't replicate. The only scalar that changes a pg-returned type for a
real field is `BigFloat` (`NUMERIC` → number). The only `NUMERIC` fields
are the two composite subfields:

- `CURRENCY.amountMicros` — already handled 
- `ADDRESS.addressLat` / `addressLng` — fixed here 

Standalone `NUMERIC` is not user-creatable (it's in
`SettingsExcludedFieldType`). Other scalars were checked and don't
diverge: `Date.serialize` is identity; `NUMBER`/`POSITION` are stored as
`float8` and returned as numbers (and `NUMBER` is already coerced in
direct execution); `DATE_TIME` resolves to the same ISO string via both
paths. So `ADDRESS` was the last gap.

## Tests

- New `format-result.util.spec.ts`: `addressLat`/`addressLng` strings
parse to numbers, already-number coordinates pass through,
numeric-looking text subfields (e.g. `addressPostcode: "10001"`) are
**not** coerced, and the existing Currency `amountMicros` coercion still
holds.
- `npx jest format-result.util.spec` → 4 passed
- `npx nx lint:diff-with-main twenty-server` → 0 warnings, 0 errors
- `npx nx typecheck twenty-server` → pass

## Follow-ups (not in this PR)

- The cross-path parity integration test (#18972) doesn't cover a record
with address coordinates — worth adding so this class can't regress.
- `formatAddressDisplay` falls back to `ALLOWED_ADDRESS_SUBFIELDS`
(which includes lat/lng) when a field has no `subFields` configured,
unlike `getEnabledAddressSubFields` (which falls back to the text-only
`DEFAULT_VISIBLE_ADDRESS_SUBFIELDS`). Harmless now that coordinates are
numbers (filtered by `isNonEmptyString`), but a latent inconsistency.

https://claude.ai/code/session_011pf9KQn4UDZGr4V4k8rRHh

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21542?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-14 07:11:05 +02:00
Marc Bickel 5d0a4b8db4 fix(twenty-front): keep paging record board columns past the second page (#21348)
Fixes #21355

## Problem

On a Record Board (Kanban) view, columns that contain more than 20
records stop loading at exactly 20. The initial query loads the first
page, one automatic fetch brings the column to 20, and then the loading
placeholder at the bottom of the column **spins forever** — scrolling
all the way down triggers no further requests. Every column is
permanently capped at `2 * RECORD_BOARD_QUERY_PAGE_SIZE` (20) records.

### Steps to reproduce

1. Open any object in board view, grouped by a field where at least one
group has > 20 matching records.
2. Wait for the board to load — the first 20 cards in the large column
appear.
3. Scroll that column to the bottom.

**Expected:** more cards load as you approach the bottom, until the
column is exhausted.
**Actual:** the placeholder stays forever; no additional group-by
request is fired.

## Root cause

An **edge-triggered consumer reading a level signal that is stuck
high.**

The fetch-more trigger (`RecordBoardFetchMoreInViewTriggerComponent`) is
an `IntersectionObserver` sentinel that writes its `inView` state into
the board-level `recordBoardShouldFetchMoreComponentState`. Its
`rootMargin` is:

```ts
const rootMargin = `${estimatedCardHeight * RECORD_BOARD_QUERY_PAGE_SIZE * 2}px`;
```

With `estimatedCardHeight ≈ 130px` and `RECORD_BOARD_QUERY_PAGE_SIZE =
10`, that's ~2600px — roughly two pages, i.e. as tall as the entire
already-loaded board. So the sentinel reports `inView = true` across the
whole loaded board, and the boolean **latches `true` after the first
auto-fetch and never toggles back**.

The consumer in `RecordBoardQueryEffect` only reacts to the **false→true
edge** of that boolean, and `triggerRecordBoardFetchMore` is a stable
`useCallback`. Once the boolean is stuck `true` and the dependency array
stops changing, the effect never re-runs — so it fetches exactly once.
The signal is *level* ("the bottom is in view, keep loading") but it's
consumed as an *edge* ("the bottom just appeared, load once"), and the
oversized `rootMargin` guarantees the level is permanently high so the
single edge never repeats.

The large `rootMargin` is intentional prefetch buffering and is not the
bug; the consumer simply needs to keep paging while the signal is high.

## Fix

Make the consumer **re-arm** the trigger after every page that actually
returned records:

1. `useTriggerRecordBoardFetchMore` now returns a `boolean` — `true`
only once at least one column received records this round, `false` on
every early-exit / empty result.
2. `RecordBoardQueryEffect` resets
`recordBoardShouldFetchMoreComponentState` to `false` after a
**productive** fetch. The sentinel is still inside the inflated
`rootMargin`, so the observer immediately re-asserts `true`, which
re-runs the effect and fetches the next page.

The loop terminates naturally and never spins:

- **Buffer filled** — enough cards load that the sentinel finally leaves
the `rootMargin` → observer reports `false` → loop stops. As the user
scrolls, it re-arms (normal infinite scroll).
- **Columns exhausted** — `triggerRecordBoardFetchMore` returns `false`
(per-column `shouldFetchMore` flags already get set `false` when a page
returns `< PAGE_SIZE`), so the boolean is not reset and no further fetch
fires — no busy-loop on a fully-loaded board.

The existing `recordBoardIsFetchingMore` re-entrancy guard prevents any
overlapping/double fetch during the round-trip.

## Test

- `npx nx typecheck twenty-front` → passes
- `npx nx lint twenty-front` (oxlint --type-aware + oxfmt) → 0 warnings,
0 errors, formatting clean
- Manually verified on a board with columns of 38 and 74 records:
pre-fix both froze at 20; post-fix they page to completion on scroll,
and a fully-loaded board issues no extra requests.

## Notes / alternatives considered

- **Shrinking `rootMargin`** would mask the bug for tall boards but
defeat the intended prefetch buffering and reintroduce it whenever the
buffer is smaller than the loaded content. The level/edge mismatch is
the real defect.
- **Moving the loop into the trigger component** was rejected — it only
knows `inView`, not whether a fetch was productive or whether columns
are exhausted, so self-looping there would increase coupling. The query
effect is the right owner of fetch orchestration.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-06-14 06:10:24 +02:00
Félix Malfait c05f01f2a3 fix(server): repair 2.13 isUIReadOnly→isUIEditable rename fallout (#21504) (#21537)
## Context

Follow-up to #21504 ("Rename isUIReadOnly to isUIEditable, add
isUICreatable…"), which surfaced two issues:

1. **`column FieldMetadataEntity.isUIReadOnly does not exist`** on
twenty-main.com.
2. **`cross-version-upgrade` CI failure** —
`SyncStandardUiCapabilityFlags` aborts the v1.22 → 2.13 upgrade.

## Fix 1 — don't drop `isUIReadOnly` in the 2.13 rename command

The 2.13 fast instance command physically dropped `isUIReadOnly` from
`core."fieldMetadata"` and `core."objectMetadata"`. But migrations run
in an ArgoCD **PreSync** hook **before** the new pods roll out
(`charts/prod-eu/apps/twenty-server` migration Job is `hook: PreSync`,
sync-wave `2`; the api/worker Deployments are sync-wave `10`). So the
**previous** release's pods keep serving and still `SELECT
isUIReadOnly`, throwing `column ... does not exist` from the moment the
column is dropped until the rollout finishes.

This keeps the column (already hidden from the app via
`@WasRemovedInUpgrade` on both entities) and **defers the physical
drop** to a later release. Since 2.13 hasn't shipped to self-hosters
yet, the committed command is amended in place. Both tables handled;
`isUICreatable` (new, additive column) is unaffected.

The eventual physical drop + GraphQL-compat removals are tracked in
twentyhq/core-team-issues#2542.

## Fix 2 — allow `isUIEditable` updates on relation field metadata

`SyncStandardUiCapabilityFlags` re-syncs `isUIEditable` on standard
fields, including morph/relation fields (the activityTargets `target*`
relations). The flat-field-metadata validator only permits a fixed
property allow-list on relation fields, which omitted `isUIEditable`, so
the command failed with `FIELD_MUTATION_NOT_ALLOWED` and aborted the
upgrade (leaving workspaces in a FAILED state). `isUIEditable` is a
per-field UI-affordance flag that applies to relation fields too, so
it's added to the relation-field updatable properties (a constant used
**only** by that validator — no diff-engine side effects).

## Coherence notes

- Object-level is covered: the drop is deferred on **both** tables, and
`ObjectMetadataEntity` has the identical decorators.
- `isUICreatable` needs no change: object-only and additive (no drop →
no rolling-deploy hazard), and never reaches the field relation
allow-list.
- The object-metadata validator has no relation allow-list, so there's
no object-level analog to change.

## Verification

- `nx typecheck twenty-server`  (the `satisfies` guard holds —
`isUIEditable` is a `toCompare` property of `fieldMetadata`)
- `oxlint --type-aware` + `oxfmt --check`  on changed files
- Fix 2's path is exercised end-to-end by the `cross-version-upgrade` CI
that originally caught it.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-14 05:51:23 +02:00