Commit Graph

10554 Commits

Author SHA1 Message Date
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 ecd90b78b9 fix(front): stop impersonation from corrupting the impersonator's profile name (#21757)
## Problem

A customer reported that after impersonating another user, **their own
account's first/last name had been permanently changed to the
impersonated user's name** (persisted in the DB, surviving logout), and
their actions showed up under the wrong "Updated by".

The "Updated by = impersonated user" part is expected (while
impersonating you genuinely act as that user). The real bug is the
**durable overwrite of the impersonator's profile name**.

## Root cause

About a week ago `currentUserState` became localStorage-backed with
`getOnInit: true` (`currentWorkspaceMemberState` already was).
Impersonation swaps the auth token and does a full reload, but — unlike
sign-out — it never cleared those cached identity keys.

So after the reload:
1. The atoms hydrate **synchronously from localStorage with the previous
identity** (e.g. the impersonated user, on stop), and because
`currentUser` is now non-null the authenticated UI renders immediately
with that stale identity.
2. The network `loadCurrentUser` then corrects `currentUser` /
`currentWorkspaceMember` **in place**.

If **Settings → Profile** was mounted across that in-place identity
flip, `NameFields` — which seeds local `useState` from
`currentWorkspaceMember` once and auto-saves on change — read the stale
name as a pending edit and debounce-saved it onto the **now-current**
workspace member, persisting one user's name onto another.

Read-only caches (object metadata, permissions) tolerate the same
staleness because nothing writes them back — they're only ever
overwritten by the network. `NameFields` is the one consumer that
*persists* a cached identity value, which is what turns a transient
stale read into a durable write.

## Fix

Two small, complementary layers:

- **`useImpersonationSession`** — clear the cached session identity
(`clearSessionLocalStorageKeys()`) on both `startImpersonating` and
`stopImpersonating`, before the reload. The reload then re-bootstraps
from a clean slate for the correct user (and the brief
stale-**permissions** flash goes away too). The admin's token stash
lives in `sessionStorage` and is untouched; `tokenPairState` has its own
key and is not in the cleared set.
- **`NameFields`** — re-seed the inputs when the workspace-member
**identity** changes, so an identity swap is never mistaken for a user
edit. This closes the underlying footgun regardless of how the identity
changes.

## Testing

- Added `NameFields.test.tsx`: swapping `currentWorkspaceMemberState` to
a different member must **not** trigger `updateWorkspaceMemberSettings`,
while a genuine user edit still saves. Verified the test **fails
without** the `NameFields` fix (it writes the previous member's name
onto the new member) and **passes with** it.
- `nx typecheck twenty-front`, `oxlint --type-aware`, and `oxfmt
--check` all pass on the changed files; full `twenty-front` Jest suite
green.

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

https://claude.ai/code/session_015MVW3gg7CVq5oR572ctisx

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21757?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 23:01:30 +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
nitin d4e0b78d20 Add stale Recall bot reconciliation (#21720)
This PR adds a scheduled reconciliation pass for the Twenty Meeting Bot
app so call recording state does not depend only on event-driven updates
from calendar changes and Recall webhooks.
Why we need this -- 

- A call recording row can be created, but the process can fail before
the Recall bot id is written back.
- Recall webhooks can be missed or delivered late, leaving Twenty stuck
in an older local state.
- A bot can disappear from Recall, leaving Twenty with a stale
externalBotId.
- A cancellation can fail locally, leaving an app-managed Recall bot
that would still join the meeting.

What this adds -- 

- A cron logic function that heals botless scheduled call recordings.
- A convergence pass that pulls Recall bot state for stale local rows,
including SCHEDULED rows.
- Orphaned bot cleanup for app-managed Recall bots that are no longer
claimed by an open call recording.
- Guards so destructive bot cleanup does not affect bots claimed by
another app registration.
- Tests for the new stale-state, missed-webhook, and orphan cleanup
behavior.

Not included -- 

- Media ingestion.
- Transcript pipeline.
- Billing.
- Marking call recordings as COMPLETED.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21720?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:42:03 +00:00
github-actions[bot] 9c249c766b i18n - docs translations (#21746)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-17 17:20:35 +02: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
github-actions[bot] 57d15fa73a i18n - docs translations (#21741)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-17 15:51:50 +02:00
neo773 91050dba8d docs: update calendar-email page (#21719)
Adds 3 changes
1.) Add IMAP mention
2.) Clarifies disabling SSRF for self hosters running air gapped
systems, (we have received this question several times ) 3.) Toggling
Syncing internal emails

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21719?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-17 13:07:04 +00:00
Thomas Trompette 1f9c4cf5b9 fix(front): home redirect honors first object of the navigation menu (#21626)
https://github.com/user-attachments/assets/db11f02c-4502-4adc-8cf7-55d3dc0211a6

## Summary

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

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

## Changes

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

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

## Test plan

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

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

## Root cause (concurrency)

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

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

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

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

## Fix

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

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

## Tests

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

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

https://claude.ai/code/session_01DD3469JAWYURa2sKUTJ85e

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-17 11:40:55 +00:00
github-actions[bot] 1400c6e952 i18n - docs translations (#21724)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-17 13:52:41 +02:00
Thomas des Francs ca7ffb97b9 Added page card box-shadow (#21688)
## Summary
- add a subtle left-side page card shadow in light and dark mode
- preserve the existing border-ring box shadow
- give the page card wrapper enough left padding for the shadow to
render



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

---------

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

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

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

A disabled logic function now returns `403`.

## Why

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

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

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21715?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-17 12:45:04 +02:00
Rashad Karanouh a7760c04ab Partners app: profile picture (additive file field), derived region & deployment, scope cleanup (0.5.4) (#21709)
## Summary (twenty-partners app, v0.5.4)

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

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

## Why additive, not a field-type change

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

## Deploy notes

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

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

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

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

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

Tested on all select fields one by one

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

## Root Cause

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

## Recording

### Before


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

### After


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

## QA Note

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21679?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-17 08:46:07 +00:00
Charles Bochet 257f130fff feat(sdk): let docker:start choose the server version (#21690)
## What

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

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

Two layers of resolution:

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

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

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

## Changes

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

## Behavior notes

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

## Testing

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21690?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-17 10:55:02 +02:00
Félix Malfait eeed998c9e Let users pick their workspace subdomain during sign-up (#21641)
## What & why

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

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

## Flow

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

## Changes

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

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

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

## Testing

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

## Notes / out of scope

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

---------

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

## Before

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

## After

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21689?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-17 08:41:03 +00:00
Félix Malfait bb6da7b7d1 feat(code-interpreter): reuse a warm sandbox per conversation (E2B) (#21664)
## What

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

## How

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

## SDK upgrade

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

## Config

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

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

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

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

## Tests / checks

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

---------

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

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

## Root cause (regression from #21640)

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

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

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

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

## Fix

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

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

## Test plan

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

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

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

https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY

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

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-16 23:37:51 +02:00
Félix Malfait 35c2a24afb perf(onboarding): compute invite suggestions on-demand (#21696)
## Summary

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

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

No GraphQL schema change.

## Test plan

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

https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY

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

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-16 21:25:02 +02:00
neo773 e50ec75cd0 fix(server): default timeline thread visibility to METADATA (#21669)
Orphaned messageChannelMessageAssociation rows (channel deleted in core,
association left behind when cleanup cron was down) made visibility
unresolvable, so formatThreads emitted null for the non-nullable
TimelineThread.visibility field and 500'd the whole timeline query.

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

/closes TWENTY-SERVER-FM6

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21669?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 18:48:06 +02:00
neo773 b076c35848 fix(messaging): pin Google OAuth2 client to native fetch (#21668)
/closes TWENTY-SERVER-HFH

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21668?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 18:47:30 +02:00
Raphaël Bosi d4c1fc86e3 Stabilize flaky Argos stories in twenty-front storybook (#21691)
Three twenty-front stories rendered non-deterministically and
intermittently tripped Argos as false positives (flaky on `main`, not
caused by any recent change). Each is now deterministic:

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

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21691?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 16:37:53 +00:00
neo773 1ad919955a Support variables file email attachment (#21613)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21613?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 18:11:37 +02:00
neo773 d8d5991977 fix(messaging): honor IMAP/SMTP encryption setting instead of inferring it from the port (#21562)
This pull request makes the IMAP and SMTP encryption setting actually
honor what the user selects.

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

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

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

/closes #21300

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

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-16 18:09:04 +02:00
Thomas Trompette ff03e935ef feat(workflow): expand manual-trigger runtime payload with payload + _metadata (#21676)
## Summary

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

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

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

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

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

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21676?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 15:49:12 +00:00
Félix Malfait 61309c45e6 feat(onboarding): prefill the invite step with teammates from the connected calendar (#21640)
## What & why

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

## Approach

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

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

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

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

## How to enable

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

## Notes

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

## Testing

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

https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY

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

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

---------

Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: neo773 <neo773@protonmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-06-16 15:45:11 +00:00
Raphaël Bosi 5b1cfa4cc0 Show app name on workflow logic function nodes (#21675)
Workflow logic-function nodes from installed integration apps now show
the owning app's name as the node label (e.g. "People Data Labs")
instead of the generic "Action". The descriptive title below it (e.g.
"Enrich Person") is unchanged.

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

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

## Before

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

## After

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


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21675?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 14:30:23 +00:00
Raphaël Bosi 637ae65e9f Hide Input and Test tabs for app logic function nodes (#21671)
When opening a logic function node that comes from an app (e.g. "Enrich
Company" from People Data Labs), the node detail panel was showing
**Input** and **Test** tabs. App-sourced functions should only expose
their input fields, with no test surface.

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

## Before

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

## After

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


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21671?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 14:24:12 +00:00
Mani bharadwaj 9001078cb2 fix(cli): detect expired token on deploy and offer interactive re-auth (#21335)
## I have read the CONTRIBUTING.md file.

YES

## What kind of change does this PR introduce?

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

Fixes #20197

## What is the current behavior?

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

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

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

## What is the new behavior?

Two changes, layered:

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

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

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

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

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

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

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

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

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

## Acceptance criteria

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

## Reproduction

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

## Implementation notes

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

## Out of scope (per the issue)

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

## Files changed

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

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

---------

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

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-16 16:24:19 +02:00
martmull 306a1454aa Update Connection provider path (#21678)
## Before

After connecting to oAuth linear app connection:

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

## After

Redirects to Linear

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21678?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 13:53:13 +00:00
Etienne 16a92f52a4 feat(admin-panel) - add billing/usage section (#21672)
Add billing/usage section

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


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21672?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 13:18:43 +00:00
Félix Malfait 1e8169ca3e feat(ai-agent): suggest similar tool names when tool discovery misses (#21654)
## Context

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

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

## Change

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

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

## Tests

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

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

https://claude.ai/code/session_01GMjZkJYkqTJogJJTM6AAV8

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21654?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 13:06:57 +00:00
Raphaël Bosi e515f174a4 Load twenty-ui global styles in twenty-front Storybook (#21665)
Loads `twenty-ui/style.css` in twenty-front's Storybook preview,
matching the real app — `src/index.tsx` already imports it, but
`.storybook/preview.tsx` only loaded the deprecated stylesheet, so
Storybook rendered without twenty-ui's global `reset.scss`
(`*{box-sizing:border-box}` + a native `button` reset) that the
production app has.

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

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


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21665?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 12:12:49 +00:00
Thomas des Francs a67fdf2dac Clean up MCP Monaco editor (#21643)
Rebuild the MCP setup block on the shared Monaco editor with theme-driven colors, padding, and auto-height; add the settings description line-height token; revert the clipboard util to the shared navigator.clipboard implementation; and consolidate the code editor's auto-height to a single disposed, reactive effect.
2026-06-16 13:57:44 +02:00
Thomas des Francs ce92a1f1e1 Update GitHub banners and remove unused assets (#21655)
saves 24mb
duly checked
2026-06-16 10:59:59 +00:00
nitin b7c68e082e Enable inline field editing in calendar event details (#21666)
before - 


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




after - 


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



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21666?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 10:52:54 +00:00
Charles Bochet b327999b04 fix(server): run 2-14 standard relation label/icon heal as a system build (#21667)
## Problem

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

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

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

## Fix

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

## Notes

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


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