39e00d5853a76f2dff47fcc85c2008d2a60f6fa7
12819 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. --> |
||
|
|
b063c7850b |
Twenty app e2e app prod parity dispatch (#21750)
# Introduction On main merge or if PR is labelled with specific label, dispatch the app prod parity check Note: for the moment not optimal for PR context as it will only set a status check on the related commit which is not blocking anything for the moment related https://github.com/twentyhq/core-team-issues/issues/2557 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21750?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. --> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
495907c781 |
ci: remove merge queue, run e2e on push to main (#21722)
## What Removes the GitHub merge queue and runs what the queue used to gate — the E2E (Playwright) suite — directly on push to `main`. If that run fails on `main`, we ping engineering via webhook. ## Why In the queue, only the `e2e-test` job in `ci-merge-queue.yaml` ran real work — every other CI workflow's `merge_group` path skipped its `changed-files-check` and tests, so the queue's status checks for those were effectively green no-ops. The expensive thing actually gated was E2E. Moving it to `push: main` validates the merged state post-merge without the queue's batching overhead. ## Changes - **Rename** `ci-merge-queue.yaml` → `ci-e2e-main.yaml` (`name: CI E2E Main`). - `e2e-test` now triggers on `push` to `main` (the `run-merge-queue` PR label is kept as a manual opt-in for running E2E on a PR). - Status-check job renamed `ci-e2e-main-status-check`. - New `notify-main-ci-failure` job: on a failed **main push**, `POST`s to `https://engineering.twenty.com/s/main-ci-failing` with the commit SHA, actor, and run URL. - **Strip dead merge-queue config** from the other CI workflows: removed the `merge_group:` triggers and the now-unreachable `if: github.event_name != 'merge_group'` guards from `ci-server`, `ci-shared`, `ci-sdk`, `ci-front-component-renderer`, `ci-test-docker-compose`, `ci-website`, and the `merge_group:` trigger from `ci-front`, `ci-ui`, `ci-new-ui`. ## Required follow-up (not in this PR) The merge queue itself is a **repo setting**, not code. After this merges, disable **"Require merge queue"** on the `main` ruleset/branch protection (Settings → Rules), otherwise GitHub keeps batching. Required status checks tied to the old queue should also be dropped/updated. ## Behavior change E2E now runs **after** merge rather than blocking it in the queue — a bad change lands on `main` and then alerts (the webhook is the mitigation), instead of being held back pre-merge. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21722?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. --> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
66c860574f |
Point New UI visual regression at the twenty-ui Argos project (#21728)
The `twenty-new-ui` Argos project is being renamed to `twenty-ui` (and
the old `twenty-ui` / `twenty-ui-vs-new-ui` projects removed).
Updates the New UI visual-regression flow to target `twenty-ui`: the
dispatch project mapping and the screenshot artifact name (which must
match `argos-screenshots-${project}`), plus the internal storybook
artifact name for consistency.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21728?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
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.
|
||
|
|
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. --> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
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. -->
|
||
|
|
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. |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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>
|
||
|
|
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. --> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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> |
||
|
|
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. -->
|
||
|
|
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> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
d5c7b735d2 |
ci: migrate cross-repo dispatch senders to workflow_dispatch (actions:write) (#21648)
# Introduction Getting rid of the fine grained PAT used to dispatch to internal repositories. Repo dispatch requires the contents write permissions which is too wide for such use Refactored all senders and target to pass through a workflow dispatch instead Creating a centralize app that forges a token with actions: write only provided permissions to mitigate any token exfiltrations |
||
|
|
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. --> |
||
|
|
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. -->
|