23f5ba9ebfb5bca64918dd22393a018fd9d5eefb
2447 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
23f5ba9ebf |
feat: add resizable kanban column width (#21828)
## What & why Lets users resize the columns of a Kanban (record board) view. Requested by a user; the design avoids the "ragged board" problem by making the width a **single shared value**. ## Behaviour - A drag handle appears on the right edge of every column header. - Because all columns read **one** width value, dragging any handle resizes **every** column together — they can never end up mismatched. - Width is clamped between **150px** and **400px** (default **200px**). - The width is **persisted per view** and restored on reload. ## Approach **Backend** — a new nullable `View.kanbanColumnWidth` field, threaded through the existing view-level setting pattern (the same one `kanbanAggregateOperation` / `shouldHideEmptyGroups` use), so it gets create/update/manifest/override support for free: - entity column + `ViewOverrides` + `@WasIntroducedInUpgrade` - `CreateViewInput` / `UpdateViewInput` (`Int`, `@Min(150)`/`@Max(400)`) + `ViewDTO` - flat-view editable properties, entity-properties config, compare-type, standard-view + manifest converters - a fast instance command adding the `core.view` column **Frontend** — the value hydrates into a view-scoped atom and drives a single CSS variable set on the board container, which both column headers and bodies read. Live dragging only writes that CSS variable (no per-move React re-render); the final width is committed to the atom and persisted via `updateView` on pointer-up. ## Nullability / defaults `kanbanColumnWidth` is nullable — `null` means "never resized" and the UI falls back to the 200px default, so existing rows need no backfill. ## Validation - `nx typecheck twenty-server` ✅ and `nx typecheck twenty-front` ✅ - `nx lint:diff-with-main twenty-server` ✅; frontend lint fixes applied (split constants to one-per-file, removed `useRef`-for-state in favour of `useState`). - Draft pending a final green CI run (the dev container reclaimed `node_modules` mid-session; re-running locally). ## Test plan - [ ] Drag a kanban column edge → all columns resize together, clamped 150–400px - [ ] Reload → width persists for that view; other views unaffected - [ ] A view that was never resized still renders at 200px https://claude.ai/code/session_016Qe6oDBkhVbrq2QkXBJ5nE --- _Generated by [Claude Code](https://claude.ai/code/session_016Qe6oDBkhVbrq2QkXBJ5nE)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21828?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> |
||
|
|
98c35ea804 |
App uninstall lambda, layers cleanup (#21749)
# Introduction On a logic function deletion also remove the driver entry On a app uninstall also remove the sdk layer ( keeps the dep one as it can be shared across several lambda ) | Resource | Scope | Before this PR | After | |---|---|---|---| | DB metadata (functions, objects, fields…) | per-app | deleted | deleted | | Source folder (`FileFolder.Source`) | per-function | deleted | deleted | | Built handler file (`FileFolder.BuiltLogicFunction`) | per-function | deleted | deleted | | **Lambda function** | per-function | **leaked** | **deleted** (driver `delete`) | | **SDK layer** `sdk-<wsId>-<appId>` (all versions) | per-app | **leaked** | **deleted** (driver `deleteApplicationResources` → `deleteSdkLayer`) | | Deps layer `deps-<checksum>` | shared across apps/workspaces | not deleted | **intentionally not deleted** (content-addressed, GC'd) | ## What I don't like about all that Right now there's some non reversible side effect inside the workspace migration transaction - If the transaction fails we're facing data loss - It also slows down everything I'm about to create a new PR that allow population post transaction commit side effect / cleanup to be run later <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21749?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. --> |
||
|
|
705caab2b0 |
fix(onboarding): refresh stale workspace in currentWorkspace field resolver (#21839)
## What & why
After sign-up, users are redirected to `/sync/emails` and the page loads
forever; a full refresh fixes it. This blocks production deploy.
**Root cause** — a GraphQL field resolver returns an unrefreshed (stale)
workspace:
- `@AuthWorkspace()` (`request.workspace`) is read from the per-instance
core entity cache and can still be `PENDING_CREATION` /
`ONGOING_CREATION` right after `activateWorkspace`.
- The `currentUser` query resolver and the `onboardingStatus` field
already guard against this by calling
`refreshWorkspaceIfPendingOrOngoingCreation(...)`.
- But the `currentWorkspace` `@ResolveField` returned the raw
`@AuthWorkspace()` workspace. Because a field resolver takes precedence
over any value the query resolver attaches to its returned object, the
client receives that stale workspace.
So right after activation the client got an inconsistent payload:
- `onboardingStatus: SYNC_EMAIL` (fresh — computed from a direct DB
read)
- `currentWorkspace.activationStatus: ONGOING/PENDING_CREATION` (stale)
On the frontend, metadata loading is gated on
`isWorkspaceActiveOrSuspended(currentWorkspace)`, and
`MinimalMetadataGater` does **not** exclude `/sync/emails`. So the
workspace looked inactive → metadata never loaded (no metadata GraphQL
request was even issued) → the gater's loader showed indefinitely. A
full refresh worked because the cache had since refreshed to `ACTIVE`.
## Why it surfaces on staging but isn't caught by tests
The stale window only opens on a real fresh sign-up followed by
immediate activation, against a workspace cache that hasn't refreshed
yet (multi-instance / cache TTL). Single-instance local dev and the
existing `successful-user-and-workspace-creation` integration test
exercise `activateWorkspace` + `getCurrentUser` against one consistent
cache, so `currentWorkspace` already looks `ACTIVE` and they pass —
which is why this reproduces on staging/production but not locally, and
why a manual refresh recovers.
## How
Refresh the workspace in the `currentWorkspace` field resolver too, so
it is consistent with `onboardingStatus`. For active workspaces this is
a no-op (no extra DB read).
```ts
async currentWorkspace(@AuthWorkspace({ allowUndefined: true }) workspace) {
if (!isDefined(workspace)) return workspace;
return this.userService.refreshWorkspaceIfPendingOrOngoingCreation(workspace);
}
```
This is preferred over the frontend alternative (excluding
`/sync/emails` from `MinimalMetadataGater`), which would only hide the
symptom while every other consumer still received a wrong
`activationStatus`.
## Verification
- `nx typecheck twenty-server` and `nx lint:diff-with-main
twenty-server` (oxlint + oxfmt) are green.
https://claude.ai/code/session_018c1X6CwDgttMXA5tB797yS
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
26db3f5735 |
Deprecate legacy encryption (#21831)
# Introduction Still preserving the cross-upgrade flow close https://github.com/twentyhq/core-team-issues/issues/2465 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21831?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. --> |
||
|
|
8ff494e5e8 |
fix(security): bump tar to 7.5.16 in seed-dependencies (PAX file smuggling) (#21829)
## fix(security): bump tar to 7.5.16 in seed-dependencies (PAX file smuggling) Resolves [Dependabot Alert #1500](https://github.com/twentyhq/twenty/security/dependabot/1500). ### What `tar` (`node-tar`) `<= 7.5.15` applies a PAX size override to intermediary GNU long-name/long-link headers, causing a tar-parser interpretation differential (file smuggling) — [GHSA-vmf3-w455-68vh](https://github.com/advisories/GHSA-vmf3-w455-68vh). Patched in `7.5.16`. This is the **seed-dependencies holdout** deferred from the main tar PR (#21813): that lockfile + its checksum constants were also touched by the form-data PR, so it was carved out to avoid a conflict. The form-data PR has since merged, unblocking it. ### How - Refreshed `tar` `7.5.13 -> 7.5.16` in `seed-dependencies/yarn.lock` (transitive via `^7.5.4`, which already permits it) — an in-range refresh, no override. - Regenerated `DEFAULT_YARN_LOCK_CHECKSUM` in `get-default-application-package-fields.util.ts` so the row-stored checksum matches the value recomputed from file content in `application.service.ts` (the deps-layer cache key; `logicFunctionCreateHash` = SHA-512, first 32 hex). `package.json` is unchanged, so `DEFAULT_PACKAGE_JSON_CHECKSUM` is unaffected. ### Verification - No `tar <= 7.5.15` remains in the seed lockfile. - Both checksum constants verified to match the canonical recompute of the current seed files. - Lint + format pass on the changed `.ts` file. |
||
|
|
adf6eb572b |
feat(billing): embed Stripe Payment Element in onboarding (#21759)
## What & why Replaces the hosted Stripe Checkout redirect on the onboarding "Choose your plan" step (credit-card trial) with an inline Stripe **Payment Element**, so users never leave the app to enter card details. ## How it works - **Frontend:** a deferred `<Elements mode="setup">` renders the Payment Element, themed via the Appearance API. On Continue: `elements.submit()` → `checkoutSession` mutation creates the trialing subscription server-side and returns its pending SetupIntent `clientSecret` → `stripe.confirmSetup()` confirms the card (handling 3DS) → redirect to the existing `/plan-required/payment-success`. - **Backend:** new `BILLING_STRIPE_PUBLISHABLE_KEY` config var exposed via `/client-config`; the card path creates the subscription with `payment_behavior: default_incomplete` + a free trial (so Stripe attaches a `pending_setup_intent`) and returns its client secret. The hosted-Checkout code path is removed. - The **no-credit-card** trial path is unchanged. - Billing address collection is **disabled** in the Payment Element to reduce friction; `automatic_tax` is correspondingly disabled (tax needs an address — collect it later, e.g. at conversion / via the billing portal). ## Required before this works 1. Set `BILLING_STRIPE_PUBLISHABLE_KEY` (`pk_…`) on the server (infra change pending). 2. Run `nx run twenty-front:graphql:generate --configuration=metadata` against a server exposing the updated schema (see inline note on the hand-authored document). 3. Verify in Stripe test mode: happy path, 3DS (`4000 0025 0000 3155`), a decline. ## Verified typecheck (front + server), oxlint + oxfmt clean, `client-config.service.spec` passing. Not run here: the app end-to-end / Stripe test mode and `graphql:generate` (no server/DB in the dev container). I've left self-review comments inline flagging cleanup opportunities plus a couple of architectural/tech-debt items. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01TxCfinXq7abSrbF7aTw2cA --- _Generated by [Claude Code](https://claude.ai/code/session_01TxCfinXq7abSrbF7aTw2cA)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21759?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> |
||
|
|
40c9a11f43 |
feat(onboarding): always show the Create Profile step (#21823)
The Create Profile step was skipped whenever the user already had a first or last name (e.g. provided during sign-up or via SSO), because the create-profile-pending flag was only set when both were empty. Always set it so the step is presented during onboarding and the user can review/confirm their profile; submitting it still clears the flag and advances. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21823?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. --> |
||
|
|
3675f264f1 |
Infer record pickers for record-typed logic function workflow inputs (#21494)
## Context Logic functions can declare workflow inputs typed as records or arrays of records (e.g. the People Data Labs enrichment functions), but the workflow builder rendered those as a plain text input with a variable picker, which is not usable. ## What this does - Adds an `objectUniversalIdentifier` link on input schema properties, so a record-typed input is tied to a workspace object. - The SDK build infers it from a `TwentyRecord<'objectUniversalIdentifier'>` marker type in the handler signature, reading the object's universal identifier straight from the source; explicit input schemas can still set the field directly. - The workflow builder renders these inputs as a single record picker or a record multi-select with the variable picker on the right. Selected records are stored as record ids; `TwentyRecord<UID>` is a branded `string`, so the handler signature reflects that it receives ids (a bound variable resolves to whatever the referenced step produced). - The multi-select collapses overflowing chips into a `+N` badge (reusing `ExpandableList`) and its variable picker offers both record objects and fields. - Updates the People Data Labs enrichment inputs as the reference implementation. <img width="802" height="824" alt="CleanShot 2026-06-12 at 16 54 10@2x" src="https://github.com/user-attachments/assets/a0896d74-0aab-49bd-a173-14c578a2e533" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21494?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. --> |
||
|
|
26b4d6caed |
fix(security): bump form-data to 4.0.6 (CRLF injection) (#21808)
Resolves [Dependabot Alert #1473](https://github.com/twentyhq/twenty/security/dependabot/1473), [#1475](https://github.com/twentyhq/twenty/security/dependabot/1475), [#1477](https://github.com/twentyhq/twenty/security/dependabot/1477), [#1478](https://github.com/twentyhq/twenty/security/dependabot/1478), [#1480](https://github.com/twentyhq/twenty/security/dependabot/1480), [#1482](https://github.com/twentyhq/twenty/security/dependabot/1482), [#1484](https://github.com/twentyhq/twenty/security/dependabot/1484), [#1486](https://github.com/twentyhq/twenty/security/dependabot/1486), [#1488](https://github.com/twentyhq/twenty/security/dependabot/1488), [#1490](https://github.com/twentyhq/twenty/security/dependabot/1490), [#1492](https://github.com/twentyhq/twenty/security/dependabot/1492), [#1494](https://github.com/twentyhq/twenty/security/dependabot/1494), [#1495](https://github.com/twentyhq/twenty/security/dependabot/1495), [#1497](https://github.com/twentyhq/twenty/security/dependabot/1497), [#1499](https://github.com/twentyhq/twenty/security/dependabot/1499), [#1501](https://github.com/twentyhq/twenty/security/dependabot/1501) and [#1506](https://github.com/twentyhq/twenty/security/dependabot/1506). |
||
|
|
814b43ca41 |
feat(server): derive email/calendar timelines from object relations (#21684)
Simplifies our existing implementation that uses three different GraphQL
endpoints to just one `getTimelineEventsFrom{Person, Company,
Opportunity}Id` to `getTimelineCalendarEventsFromObjectRecord`
/closes https://github.com/twentyhq/twenty/issues/19676
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21684?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>
|
||
|
|
7b5ee8a7bc |
feat(server): report enterprise instance metadata on license validation (#21793)
## What Enriches the **enterprise-only** license-validation channel (`/validate`, `/seats`) with best-effort instance metadata so the licensing backend can later reconcile seats and surface signs of license abuse (e.g. one subscription on many `serverId`s, a `serverId` on many URLs, dev-mode-in-prod). Reported alongside the existing `enterpriseKey` (and `seatCount` on `/seats`), under a new `instanceMetadata` object: | Field | Purpose | |---|---| | `serverId`, `serverUrl` | instance identity — sharing / clone signals | | `workspaceCount`, `activeUserWorkspaceCount`, `distinctUserCount` | seat reconciliation / overage | | `appVersion`, `nodeEnv`, `telemetryEnabled` | fleet/support; dev-mode-in-prod signal | | `adminContactEmail` | **single** administrative contact (oldest active user) for license administration — explicitly *not* an abuse signal | | `sentAt` | timestamp | No CRM data, record contents, or member PII beyond the one admin contact are sent. ## Why it's safe for existing instances - **Enterprise-only.** Gathering runs only after the `ENTERPRISE_KEY` checks, so free/community instances make no extra queries and send nothing — unchanged behavior. - **Never blocks a refresh.** Each lookup is isolated (`safeCount` / try-catch); any failure degrades to `null` and the license refresh / seat report proceeds. - **Purely additive.** `enterpriseKey` and `seatCount` are preserved; the `/validate` and `/seats` handlers ignore unknown fields, so this can ship ahead of any backend consumer. - **No schema or token-verification changes** → no migration, existing validity tokens keep validating. ## Verification - `nx test twenty-server` — full unit suite green (5829 passed), including the updated `enterprise-plan.service.spec` with a new metadata-payload test - `nx typecheck twenty-server` — pass - `oxlint` + `oxfmt --check` on changed files — clean ## Deliberately out of scope (follow-ups) - **Server-side correlation/detection** and **short-TTL + instance-bound validity tokens** live on the signing/billing side (`twenty-website`) and need a coordinated rollout (enforcing token binding now would break already-issued tokens). - **`adminContactEmail`** is PII on a contractual enterprise channel — the enterprise terms should disclose it before rollout. - The dev-key / build-provenance hardening discussed separately is **not** part of this PR. Opening as **draft** for review of the field set and the cross-repo rollout plan before wiring a consumer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0114DV9tctTjVo8eggBGgtKc --- _Generated by [Claude Code](https://claude.ai/code/session_0114DV9tctTjVo8eggBGgtKc)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21793?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> |
||
|
|
6a1b28bc12 |
feat(auth): collect the workspace logo on the sign-up creation step (#21723)
## What & why A single, consistent **workspace-creation step** for both multi-workspace and single-workspace self-host — collecting **name + logo** (and the **subdomain** in multi-workspace) — which **removes the duplicate name/logo prompt** that previously reappeared on the workspace subdomain (reported after #21641). ## Changes **One creation form for both modes** - With 0 workspaces, both multi-workspace and single-workspace route to the shared `SignInUpWorkspaceCreationForm`; `SignInUp` renders it for the `WorkspaceCreation` step regardless of domain/scope. - The subdomain field shows only in multi-workspace; single-workspace keeps its fixed address. **Logo on the creation step** - New scoped `uploadNewWorkspaceLogo(workspaceId, file)` mutation: the creator sets a logo on their just-created `PENDING_CREATION` workspace via the workspace-agnostic token (membership enforced — only the creator is a member at that point), reusing `uploadWorkspacePicture`. Upload size is capped via `settings.storage.maxFileSize` (also applied to the existing logo / profile-picture uploads). - The picked file is held locally (object-URL preview, revoked on unmount) and uploaded right after creation (non-fatal on failure). **Onboarding step → pure activation loader** - The old "Create your workspace" form (name + logo) is removed. The onboarding step now activates the pending workspace on mount and shows the loader, with a **Retry** action on failure. ## Testing - typecheck (front + server) ✅; oxlint + oxfmt clean on changed files ✅ - Unit tests: `auth.resolver.spec`, `useWorkspaceSubdomainField`, `SignInUpWorkspaceCreationForm` (multi + single-workspace), `useAuth` ✅ - Metadata GraphQL + `twenty-client-sdk` schema regenerated. Follow-up to #21641. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Xw37hR5seiCyWnppG9z4op --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7a1cfc17cc |
fix(logic-function): treat invoke timeout as a user-level error, not a platform error (#21779)
Fixes https://twenty-v7.sentry.io/issues/7527156270?project=4507072499810304 ## Problem Sentry was flooded with high-severity alerts for logic functions that simply ran too long: > Lambda timed out for function '…' during invoke (functionState=Active, phase=invoke …) A function exceeding its configured `timeoutSeconds` is a **user-level outcome** (their code is too slow), not a platform failure — but it was being reported as one. ## Root cause The two timeout mechanisms were classified inconsistently: - **Lambda's own timeout** → returns `{ status: ERROR, … }` → handled as a user error (route returns 500 with `shouldBeCapturedBySentry: false`, queue job records it without failing). Not in Sentry. ✓ - **Client-side `AbortSignal` timeout** → **threw** `LOGIC_FUNCTION_EXECUTION_TIMEOUT`, which isn't mapped in `mapErrorToRouteTriggerCode`, so it fell through to `ROUTE_TRIGGER_PLATFORM_ERROR` (Sentry) and failed the BullMQ job (Sentry). ✗ Since the executor Lambda is fixed at 900s, the client abort is the *sole* timeout enforcement for every function with `timeoutSeconds < 900` — so essentially every slow function paged the team. The `local` driver already returns an ERROR result here; only the Lambda driver threw. ## Fix On an **invoke-phase** `TimeoutError`, return a structured ERROR result instead of throwing — mirroring the Lambda's own timeout and the local driver. The timeout now flows through the normal result path: surfaced to the caller as `status: ERROR`, recorded via `handleExecutionResult` (which the throw path skipped), and kept out of Sentry. **Build- and fetch-phase timeouts still throw** and stay in Sentry — those are platform-side (executor build / code fetch too slow, even for short user code), which is exactly what the phase instrumentation exists to catch. ## Tests - Unit test on the new `buildLogicFunctionTimeoutResult` util - `npx jest logic-function-drivers/drivers/lambda` green |
||
|
|
cc4659ce11 |
fix(server): type WasRemovedInUpgrade columns with WasRemovedInUpgrade<> (#21785)
## Context WasRemovedInUpgrade type brand was introduced in https://github.com/twentyhq/twenty/pull/21228/changes#diff-1b6d688610669a46b3ee8e3a41b1c7eb0ee03e19146d0d249f95df6e56164a92R15 for the `isCustom` property deprecation. The @WasRemovedInUpgrade decorator and the WasRemovedInUpgrade<T> type are meant to go together: the type brand makes the property optional in every derived flat-entity type, so the column only needs to be declared on the entity itself. RolePermissionFlagEntity.flag had the decorator but was typed as a plain PermissionFlagType, forcing the property to be supplied everywhere. This PR: - Types flag as WasRemovedInUpgrade<PermissionFlagType> (matching the isCustom reference impl on object/field metadata). - Removes the now-redundant flag from the flat-entity construction sites, the create input, and the service call site — leaving it only on the entity. The GraphQL RolePermissionFlagDTO.flag is kept (it's an API field derived from permissionFlag.key, not the removed column). - Fixes a latent brand-leak in the flat-entity config type: toStringify is computed via object-detection, and a branded type reads as an object. This was harmless for boolean but wrongly forced toStringify: true for enum/string columns. Added UnwrapWasRemovedInUpgrade<T> and applied it so the brand is transparent making the pattern work for any type, not just booleans. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21785?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. --> |
||
|
|
e7e99247e8 |
Centralize and standardize impersonation validation rules (#21717)
# Introduction Followup https://github.com/twentyhq/twenty/pull/21707 ## Behavioral change worth calling out Server-level impersonation now requires verified 2FA outside development at every checkpoint (generation, exchange, and per-request). In main the 2FA gate only existed in ImpersonationService. This is the right tightening, but it means existing server-admin impersonation sessions in production for admins without verified 2FA will now be rejected on the next request, not just at token creation. cc @s0yd4RK <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21717?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: s0yd4RK <285671363+s0yd4RK@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
ccc77932a0 |
Tool execution metrics (#21587)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21587?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. --> |
||
|
|
640a8e6ca6 |
Add post-call recording ingestion and billing (#21758)
## Summary - Add post-call Recall recording ingestion for transcripts, audio, and video - Request/retrieve async transcripts and reconcile stale pending transcript markers - Complete call recordings atomically once all artifacts and billable timestamps are available - Charge `CALL_RECORDING` usage once per completed recording based on recording duration - Add Recall recording/media API helpers, transcript marker utilities, and audio/video field identifiers - Update generated metadata/SDK files and billing usage operation support - Add unit coverage for ingestion, completion, charging, Recall API behavior, and reconciliation flows <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21758?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. --> |
||
|
|
01bb2f4ab2 |
fix: enforce strict rules for currency value handling by ai chatbot (#21470)
opportunity: <img width="382" height="75" alt="image" src="https://github.com/user-attachments/assets/be443c29-0bca-4537-a775-01cdbf704cdb" /> fix: <img width="382" height="289" alt="image" src="https://github.com/user-attachments/assets/11aa9552-f3ac-4d25-b5aa-efbacfba3a13" /> closes #21419 --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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. --> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
8205c97b5b |
fix(route-trigger): return 422 instead of 500 for logic function execution errors (#21656)
## What Return HTTP 422 instead of 500 for `LOGIC_FUNCTION_EXECUTION_ERROR` in the route trigger exception filter. ## Why When a logic function's user code fails (e.g. an HTTP call inside the function returns a 502 from an upstream service), the exception was mapped to HTTP 500. This caused two problems: - **Sentry noise**: `shouldCaptureException` captures all 5xx responses, so every user-code failure was reported as a platform error. This generated ~56k Sentry events over 2 months for a single workspace's logic function hitting a transient upstream 502. - **Webhook retry loops**: Webhook senders like GitHub auto-retry on 5xx responses, amplifying the event count. `LOGIC_FUNCTION_EXECUTION_ERROR` is a user-code error, not a platform error. A 422 (Unprocessable Entity) correctly signals that the request could not be processed due to the logic function's own failure, without triggering Sentry capture or webhook retries. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21656?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
2de60d7ea1 |
chore(server): temporary diagnostic logging for empty verification email body (#21628)
## What Adds **temporary** diagnostic logging to `EmailVerificationService.sendVerificationEmail` so we can capture the real error behind the empty verification email body in deployed environments. ## Why Verification emails are delivered with an **empty body** (subject is fine). The body is `<!DOCTYPE html …><!--$!--><template></template><!--/$-->` — an **errored React Suspense boundary**. `@react-email/render`'s `render()` wraps the email in `<Suspense>` and streams via `renderToReadableStream` **without an `onError` handler**, so any throw during SSR is swallowed into the errored boundary and the body ships empty. In production React also strips the error text from the markup, so the cause is invisible. This could **not** be reproduced locally on `main` (renders fine in dev, in the production-focused `yarn workspaces focus --production` install layout, and on the React 18 + react-email 6 dep set), so we need the error from a deployed environment. ## What it logs When the rendered html is empty or contains `<!--$!-->`, it logs (prefix `EMAIL_VERIFICATION_RENDER_DEBUG`): - locale, trigger, html length, and the first 400 chars of the html; - the **real error + stack**, obtained by re-rendering synchronously with `renderToStaticMarkup` (which re-throws instead of swallowing). No behavior change on the happy path — the block only runs when rendering already failed. ## How to use Deploy, trigger a verification email (sign up / resend), then: ``` grep -i "EMAIL_VERIFICATION_RENDER_DEBUG" <twenty-server logs> ``` ## Revert Remove this block once the root cause is identified. |
||
|
|
206120677b |
chore: bump version to 2.15.0 (#21624)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21624?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
fdab89ae02 |
Move twenty-client-sdk to dev dep (#21611)
# Introduction The `twenty-client-sdk` is always provided and injected at runtime by the twenty-server instance Which mean that even if in your app locally you're using twenty-client-sdk `1.0` installing this app on twenty instance `2.0` will result in injecting another `twenty-client-sdk` That's the expected behavior and tradeof The twenty-app devdep should only be used to guide local devxp following typesafety and so on A user can still locally generated its own twenty-client-sdk and publish it if necessary <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21611?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
97871131a1 |
fix(server): mitigate integration-test OOM flakiness (#21588)
## Problem `server-integration-test` shards have been failing intermittently across unrelated PRs with a distinctive signature: the shard exits code 1 with **no jest assertion failure, no `Test Suites:` summary, and no V8 `JavaScript heap out of memory` error** — the process just dies mid-run. Failures hit random shards and clear on re-run (e.g. an unrelated branch failed shard 6 once, then passed 3× on identical code), while the `merge_group` gate stays green. ### Root cause Each shard runs a **single in-band jest process** that boots one shared NestJS app (`globalSetup` → `app.listen`) and holds it for the entire shard, driving heavy metadata migrations + cache rebuilds in that one process. `NODE_OPTIONS=--max-old-space-size=12288` let V8 grow to 12 GB — *above* the `ubuntu-latest` runner's available RAM (16 GB, shared with Postgres/Redis/ClickHouse). V8 therefore deferred aggressive GC and grew past physical memory, so the **OS OOM-killer killed the process before V8 hit its own ceiling** — which is why there's no heap error and no jest summary, just a silent exit. ## Changes (CI/test-only — prod runtime untouched) - **Lower the integration jest heap cap `12288` → `6144`** so V8 self-limits below physical headroom instead of being OS-killed. Counterintuitively safer: a real leak now surfaces as a *visible* heap error naming the test, rather than a silent death. (`database:reset` keeps 12288 — it runs alone, before jest.) - **Add `--logHeapUsage`** to the integration jest runs to expose the per-file heap trend for confirming/pinpointing the growth. - **Split integration tests across 16 shards (was 10)** to lower the peak working set per shard. - **Make perf logging a first-class `LoggerService` tool** (per @prastoin's review): add `LoggerService.perf()` and unify the existing `time()`/`timeEnd()` helpers into `perfTime()`/`perfTimeEnd()` (now routed through the driver), all gated by a new `PERF_LOG_ENABLED` config var. It **defaults on** so real environments keep emitting the install-perf logs, and `.env.test` sets it `false` to mute the per-action flood in integration tests. The `application-manifest` and `validate-build` services were moved from the built-in `Logger` to `LoggerService` to use it. ## Notes - `--max-old-space-size` lives only in the `test:integration` nx target; it is **not** the prod server heap setting, so prod is unaffected. - This is mitigation. If `--logHeapUsage` shows monotonic growth across files, there's a real accumulation in the long-lived app (retained flat-maps / metadata cache) worth a follow-up heap-snapshot fix. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21588?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
4be76e3fd1 |
Support morph relations in workflow record nodes (#21403)
## Support morph (polymorphic) relations in workflow record nodes
Morph relations (e.g. a polymorphic `Owner` on `Pet` targeting `Person`
or `Company`) were not selectable in the workflow **Create / Update /
Upsert Record** nodes. This PR adds full support for setting them.
### What changed
**Frontend**
- `shouldDisplayFormField`: allow `MORPH_RELATION` (many-to-one) so
morph fields appear in record forms.
- New `FormMorphRelationToOneFieldInput`: a polymorphic record picker
across the morph's target objects, storing a self-describing value `{
targetObjectMetadataId, id }`.
- Wired the morph branch into `FormFieldInput`.
**Backend**
- New `formatWorkflowRecordMorphRelationFields` util: resolves the form
value (stored under the base field name, e.g. `owner`) into the correct
per-target join column (`ownerCompanyId`), nulling siblings to keep
exactly one target referenced.
- Wired into the create / update / upsert workflow actions (update also
expands `fieldsToUpdate` to the concrete join columns).
### Permissions handling
- The picker's search is scoped to only the morph targets the user can
read (`canReadObjectRecords`), so it no longer breaks when a target
object is inaccessible.
- If an existing value points to an object the user can't read, the
field shows the reused **"Not shared"** lock display instead of an empty
field, while remaining editable when other targets are readable.
### Notes
- No data schema / migration changes — reuses the existing per-target
morph columns and stores the selection in the existing workflow step
JSON settings.
<img width="607" height="717" alt="Screenshot 2026-06-10 at 14 57 40"
src="https://github.com/user-attachments/assets/496442a1-04a5-40f8-8b56-b28e38b00d5a"
/>
Also handles the case where the selected record is not readable
<img width="596" height="737" alt="image"
src="https://github.com/user-attachments/assets/c5ffb94e-3838-4db5-853e-f8e490331f23"
/>
|
||
|
|
5f59ae20bf |
chore: bump version to 2.14.0 (#21593)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21593?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
fb4608e437 |
chore(deps): upgrade Tier-1 deps (googleapis 173, gaxios 7, express 5, jsdom 29, date-fns 4, stripe 20) (#21570)
## What Security-driven upgrade of the biggest-drift Tier-1 dependencies (staying on latest = staying patched). Bundled because they share the lockfile and the googleapis/gaxios pair must move together. | Package | From | To | Gap | |---|---|---|---| | googleapis | 105.0.0 | **173.0.0** | 68 majors | | gaxios | 5.1.3 | **7.1.5** | 2 majors | | express | 4.22.2 | **5.2.1** | 1 major | | jsdom | 26.1.0 | **29.1.1** | 3 majors | | date-fns | 2.30.0 | **4.4.0** | 2 majors | | date-fns-tz | 2.0.0 | **3.2.0** | 1 major | | stripe | 19.3.1 | **20.4.1** | 1 major | `yarn npm audit` reports **0 high/critical** advisories before and after. ## Code changes - **gaxios v7** — `GaxiosError.code` is now `string | number` (guard the calendar network-error check by `typeof`); `GaxiosError` config/response use `URL` + `Headers`; and crucially the v7 constructor drops `response.data` unless `bodyUsed` is set — updated the synthetic gmail error mocks accordingly (production gaxios sets it, so real error parsing is unaffected). - **google-auth-library / gaxios dedup** — `googleapis-common@8.0.2` exact-pins `google-auth-library@10.5.0` + `gaxios@7.1.3` while `googleapis` pulls `^10.2.0`; the two copies made `OAuth2Client`/`GaxiosError` type-identities diverge across every gmail/calendar service. Added two singleton `resolutions` (documented inline in root `package.json`). - **express 5** — no source changes. `@nestjs/platform-express@11.1.24` already resolves `express@5.2.1` internally; the old `4.22.2` pin was the override. - **jsdom 29** — no source changes, but it now pulls ESM-only transitive deps (`@csstools/*` `.mjs`, `parse5`, `entities`, `tough-cookie`, `@exodus/bytes`). Extended the server jest `transformIgnorePatterns` allowlist and added `.mjs` to the transform/extensions so jest can load jsdom. - **stripe 20** — `Subscription` gained a required `customer_account` field; added to mocks. No runtime changes. - **date-fns v4** — `Locale` is no longer ambient (import explicitly in 5 files); per-locale entrypoints dropped the typed `default` export (the locale loader now reads the single named export); fixed the default locale import in `formatTimeZoneLabel`. ## Tests - Full suites green locally: **twenty-server 5709 passed**, **twenty-front 4937 passed**, twenty-ui / twenty-ui-deprecated green; typecheck + builds (swc + vite) + lint all pass. - Added regression tests for the two runtime behaviors these upgrades touch and that had no coverage: - `getDateFnsLocale` — named-export locale resolution (date-fns v4). - `sanitizeFile` — jsdom 29 + DOMPurify still strips `<script>`/event handlers from uploaded SVGs (security guard). ## Deliberately deferred (not in this PR) - **stripe → 21/22**: stripe **21** bundles a runtime `Decimal` type for money fields **and** jumps the pinned API version to `2026-03-25.dahlia` (changes webhook/billing payload behavior) — too risky to fold into a deps bump on billing code. stripe **22** additionally drops the node10-resolvable `types` entry, which would force a repo-wide `moduleResolution` change. Capped at the latest clean **20.x**. - **openid-client → 6**: v6 is a full functional rewrite and its passport strategy manages the OAuth `state` internally, but our SSO flow uses `state` to carry `identityProviderId` across the shared `/auth/oidc/callback`. That needs an auth-flow redesign (session-carried provider id) on Enterprise SSO code with no integration harness — it deserves its own focused PR rather than riding along here. ## Tier-1 source Originated from a dependency-drift audit; remaining Tier-1 items (date-fns done here) plus Tier-2/3 follow-ups tracked separately. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21570?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
7c0136b97b |
feat(deps): migrate frontend to React 19 (#21531)
## What Migrates the frontend stack from **React 18.3 → 19.2**. The website, sdk, companion and emails packages were already on React 19; this brings the remaining holdouts (`twenty-front`, `twenty-ui`, `twenty-ui-deprecated`, `twenty-front-component-renderer`) and `twenty-server`'s email rendering onto 19, and pins a single React version repo-wide. ## Why React 18.x is now the legacy line. Staying current keeps us on the patched/maintained branch and unblocks downstream library majors (react-router 7, mantine 9, etc.) that require React 19 peers. ## Dependency bumps (required by React 19 peers / removed APIs) | Package | From | To | Reason | |---|---|---|---| | react / react-dom | 18.3.1 | 19.2.3 | core | | @hello-pangea/dnd | 16 | 18 | peer `^18 \|\| ^19` | | react-datepicker | 6 | 9 | v<7 used removed `findDOMNode`; drops `@types/react-datepicker` | | react-data-grid | beta.13 | beta.59 | peer `^19.2`; new render API | | graphiql (+ @graphiql/react, plugin-explorer) | 3 / 0.23 / 1 | 5 / 0.37 / 5.1 | peer `^18 \|\| ^19` | | react-helmet-async | 1.3 | **@dr.pogodin/react-helmet** 3.2 | upstream caps peer at `^18`; drop-in React 19 fork | A `resolutions` pin enforces a single React (19.2.3) + `@types/react` (19.2.14) across the monorepo to avoid duplicate copies / type-identity splits. Versions are the aged lockfile patches (clears the `npmMinimalAgeGate`). ## Code changes - **Global `JSX` shim** (`react-jsx-global.d.ts` per package): React 19 moved the `JSX` namespace under `React.JSX`; several deps' published types (notably `@linaria/react`'s `styled.d.ts`, which types every `styled.x` via `keyof JSX.IntrinsicElements`) still reference the global namespace. Without the shim, every styled component degrades to `any` props. - **Ref nullability**: `useRef<T>(null)` now returns `RefObject<T | null>`; widened consumer prop/hook ref types accordingly (incl. the shared `useListenClickOutside`). - **react-datepicker v9**: `onChange`/`onSelect` accept `Date | null`, `calendarStartDay` typing, `ReactDatePickerProps`→`DatePickerProps`, relaxed the dynamic `selectsMultiple` discriminated union. - **react-data-grid beta.59**: `formatter`→`renderCell`, `editor`→`renderEditCell`, `headerRenderer`→`renderHeaderCell`, `components`→`renderers`, `onRowClick`→`onCellClick`, object-shaped `useRowSelection`, Set-based selection. - **dnd style cast**: `@radix-ui/react-popper` augments `CSSProperties` with a `--radix-*` index signature that dnd's closed `DraggingStyle` doesn't satisfy → cast at the spread. ## Status / testing - ✅ `typecheck` green: twenty-front, twenty-ui, twenty-ui-deprecated, twenty-front-component-renderer, twenty-server - ⏳ build / lint / unit tests / storybook+argos / runtime smoke-test in progress Draft until local + CI verification completes. Notable behavior to QA manually: spreadsheet import (data-grid), date pickers, drag-and-drop boards/lists, GraphQL playground, page titles/favicon. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21531?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
5d892bdfd0 |
[WIP] Feat/marketing emails (#21173)
Marketing/campaign emails on top of the emailing-domain (SES) feature:
send a broadcast to a hand-picked list, with per-customer-domain
unsubscribe links and opt-out-only **unsubscribe topics**.
## Model
Standard objects (workspace schema, flat-metadata):
- `messageCampaign` — a campaign send (subject, body template, from
address, status, list, optional unsubscribe topic).
- `messageList` + `messageListMember` — the hand-picked audience (person
↔ list join). A campaign's recipients are its list's members; everyone
is sendable unless suppressed.
Core entities (`core` schema, workspace-scoped — readable by the public
unsubscribe flow without a workspace context):
- `unsubscribeTopic` — an opt-out-only category (name, description,
visibility). There is no opt-in subscription state.
- `messageSuppression` — the single consent store: a row with
`unsubscribeTopicId` NULL is a global block; a row with an
`unsubscribeTopicId` and reason `UNSUBSCRIBE` is a per-topic opt-out.
Two partial unique indexes dedupe global vs per-topic rows (Postgres
treats NULLs as distinct).
- `emailingDomain` — the workspace's SES sending domain,
auto-provisioned when an email channel is added (and cleaned up when its
last channel is removed), with verification status + DNS records.
Campaign messages reuse the existing `message` / `messageThread` /
`messageParticipant` model — one outbound `message` per recipient with a
`deliveryStatus` state machine.
## Sending
- `sendMessageCampaign` resolves the audience **under the caller's
permissions**, creates the campaign, and enqueues a single fan-out job
(the request never materializes per-recipient rows or jobs).
- The fan-out job materializes one QUEUED message per recipient
(deterministic ids → idempotent re-runs, reconciles crash-orphaned rows)
and fans out per-recipient send jobs carrying **only ids**.
- Each send job renders per-recipient `{{variable}}` merge fields and
sends via `EmailingDomainSenderService`, which applies suppression
(global + per-topic) and the unsubscribe footer/headers. Suppressed
recipients are recorded `SKIPPED`.
- The campaign finalizes `SENT`, or `SENT_WITH_ERRORS` if any recipient
terminally failed.
- `previewMessageCampaignAudience` returns a pre-send breakdown (total /
without-email / duplicate / globally-unsubscribed / topic-unsubscribed /
sendable), shown as a hint under the composer pickers.
## Unsubscribe
- Encrypted (AES-256-GCM) token carrying workspaceId, address, optional
`unsubscribeTopicId`, `issuedAt`, and a `preview` flag.
- One-click POST (RFC 8058) + `mailto:` — topic-scoped when the token
carries a topic, global otherwise.
- Preferences page: a checkbox per visible topic (checked = still
receiving); submitting creates per-topic opt-outs for unchecked topics
and lifts re-checked ones (UNSUBSCRIBE only — never
`BOUNCE`/`COMPLAINT`, never a global block).
- A **Preview** action in settings opens the live page via a
preview-claim token; opt-out POSTs are no-ops for preview tokens, so
previewing never mutates state.
- SES webhooks: inbound unsubscribe + outbound bounce/complaint →
suppression (race-safe against at-least-once delivery, with reason
escalation that never downgrades).
- Per-customer unsubscribe hostname (Cloudflare DNS); sends are gated on
it being active, except in LOG/demo mode.
## Architecture
Campaign orchestration, suppression, the sender, the unsubscribe
controller, and the SES webhook handlers live in `src/modules/emailing`
+ `src/modules/messaging-webhooks` (the workspace-feature layer).
`core-modules/emailing-domain` keeps the SES driver, domain
provisioning, the `unsubscribeTopic` / `messageSuppression` core
entities, and the unsubscribe token/hostname plumbing. Domain creation
is validated (`CreateEmailingDomainInput` — domain-format regex,
lowercased) before any value reaches SES or the unsubscribe hostname.
## Frontend
- Campaign composer side panel (from / list / unsubscribe topic /
subject / body) with a live audience-preview hint.
- Email settings: email channels each showing their auto-provisioned
sending domain in a single section (status + DNS records + a "Check
verification" action), plus an **Unsubscribe Topics** section to
create/manage topics and preview the recipient page. A demo-mode banner
is shown when the LOG driver is active.
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
|
||
|
|
76f69efb43 |
Keep synced messages and events when removing a workspace member (#21443)
## Context Removing a workspace member deletes their connected accounts, which cascades into deleting every message and calendar event those accounts synced. For a CRM, losing the email history of departed teammates is a big deal. ## What this does Connected accounts are now kept and reassigned instead of deleted when a member is removed: - Ownership moves to the acting user (whoever removed the member). When members remove themselves (leave workspace, account deletion), it falls back to the oldest admin. - OAuth tokens are revoked, credentials wiped, message/calendar channels get `isSyncEnabled = false`, and the account is stamped with a new `archivedAt` column (fast instance command included). - Synced messages, threads and calendar events stay in the workspace. Channel visibility settings keep applying as before, since channels and associations survive. - The reassigned account appears in the new owner's Settings → Accounts, where it can still be deleted (with its data) like any other account. The transfer happens synchronously during removal, while the member's userWorkspace row still exists. This also removes `DeleteWorkspaceMemberConnectedAccountsCleanupJob` and its listener: the async job had to reconstruct the account-owner link from rows the removal flow had just deleted, which was race-prone (see 2181fb541e). Archived accounts are excluded from the workflow send-email default account resolution, and both removal confirmation modals now mention what happens to synced data. --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
f63f053444 |
CommandMenuItem overridable entity (#21486)
## Context Second PR of the overridable-entities track (after #21436 for views): command menu items become overridable so that edits on non-owned items are stored as overrides instead of mutating the row, and deletion/deactivation becomes reversible. ## What this does - `CommandMenuItemEntity` now extends `OverridableEntity<CommandMenuItemOverrides>` (adds `isActive` + `overrides`). All editable properties are overridable for now (to discuss). - **Update**: mutations on a command item not owned by the caller (standard items) are written into `overrides`; reads merge them in the DTO. The command palette edit mode (pin, reorder, shortLabel) now preserves standard values, "Reset label to default" gains true post-save semantics. - **Delete**: protected items are deactivated (`isActive = false`) instead of deleted; custom items still hard-delete. - **Object deactivate/enable toggle**: now flips `isActive` on the command item (merged into the main migration call) instead of delete/recreate; a create-if-missing fallback covers legacy deactivated objects. - **Front**: inactive command items are filtered out of the palette selector. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21486?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> |
||
|
|
869680a5a1 |
fix(deps): esbuild ^0.28.1 floors + vite 7→8 (rolldown) upgrade (#21517)
## What this does Resolves the remaining esbuild security alerts on packages we own, and upgrades the repo to **Vite 8** (which drops esbuild entirely in favour of rolldown/oxc). ### 1. esbuild → `^0.28.1` (security) - Raised the declared `esbuild` floor in `twenty-sdk` and the logic-function common-layer (both were `^0.25.0`, which can only resolve to a vulnerable version). These are our packages, so this is just declaring the patched version — clears Dependabot **#1467** and **#1468**. ### 2. Vite 7 → 8 - Bumped `vite` to `^8` in the 5 packages that declare it, and `@vitejs/plugin-react-swc` to `^4.3.1` (the only plugin that needed a bump for Vite 8; everything else already supports it). - `twenty-front` keeps esbuild minification, so esbuild is now an explicit (patched) devDependency there — Vite 8 no longer ships it. ### Two Vite-8 fallout fixes (bundler internals changed) - **Storybook tests:** added React to `optimizeDeps.include` so Vite's dep optimizer doesn't re-bundle React mid-run and break in-flight imports in browser-mode tests. - **`hex-rgb`:** it's ESM-only and broke rolldown's CJS interop (a default import resolved to the wrong thing under jest). Replaced its one use with a tiny inline hex→rgb parse and dropped the dependency. ## Verified Vite resolves to a single `8.0.16` with no esbuild in its tree. Builds pass on Vite 8/rolldown: `twenty-front` production build, the SDKs, and Storybook; the previously-failing front and storybook test jobs now pass; `yarn install --immutable` is clean. ## Note This doesn't close root alert **#1469** — esbuild is still pulled by other third-party tools (storybook, tsx, lingui, zapier, etc.) that haven't shipped a patched release. The vulnerable code path (esbuild's dev server) isn't used here, so that one is best dismissed as not-affected. |
||
|
|
1efa3567ef |
Rename isUIReadOnly to isUIEditable, add isUICreatable, expose both to app developers (#21504)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
# UI capability flags: `isUIEditable` + `isUICreatable`
## Per-verb capability model
This PR replaces the negative `isUIReadOnly` metadata flag with
positive, per-verb capability flags (à la Salesforce
`createable`/`updateable`):
- **`isUIEditable: boolean`, default `true`** — rename of `isUIReadOnly`
with inverted polarity, on **both** `objectMetadata` and
`fieldMetadata`. It is one concept ("can the user edit this through the
generic UI?") at two altitudes, so it carries one name at both levels.
- **`isUICreatable: boolean`, default `true`** — new, **object-level
only** (fields have no create verb). When `false`, no generic UI
affordance to create a record of this object appears anywhere (table "+"
buttons, board column add, calendar add, relation-section "Add new",
record picker "Add new", command-menu create action and its keyboard
shortcut).
Both flags are **UI-affordance flags only**: the server does not block
create/edit mutations based on them, so the system, API, and workflows
continue to mutate these records freely. They are orthogonal statements
about the object's nature with no implication rule in the data model.
Because today's inline creation UX creates a blank record the user must
then edit, the frontend create predicate currently requires both
`isUICreatable` and effective editability.
There is no CREATE permission in `ObjectPermissions`; the frontend keeps
gating creation on `canUpdateObjectRecords` as a proxy, ANDed with the
new flags.
## Unified create predicate
All generic creation entry points now flow through one predicate,
`canCreateRecordsForObjectMetadataItem` (`isUICreatable` && not
`isSystem` && not effectively read-only, where effective read-only
covers `isUIEditable`, `isRemote`, and the `canUpdateObjectRecords`
proxy via `isObjectMetadataReadOnly`). This deletes the previously
hardcoded suppression lists:
- `isRecordTableCreateDisabled.ts` and its hardcoded
`WorkflowRun`/`WorkflowVersion` list — deleted; those objects (plus
`workspaceMember`) now declare `isUICreatable: false` in the standard
application instead.
- The hardcoded `workspaceMember` guard inside
`useAddNewRecordAndOpenSidePanel.ts` — deleted.
- The `CREATE_NEW_RECORD` command menu item's availability expression
now checks `objectMetadataItem.isUICreatable`, `isUIEditable`,
`isSystem`, and `isRemote`; a workspace upgrade command re-syncs the
expression in existing workspaces.
Component-local conditions (soft-delete filter active, layout
customization mode) stay in their components.
## GraphQL compatibility and removal plan
The schema delta versus main is **purely additive plus deprecations —
zero breaking changes**:
- `isUIReadOnly` remains on both the ObjectMetadata and FieldMetadata
GraphQL output types for **one release** as a deprecated field computed
as `!isUIEditable` (`deprecationReason: 'Use isUIEditable'`). The Twenty
frontend no longer queries it.
- `isUIReadOnly` also remains on the **input side** for one release
(`CreateFieldInput`, `UpdateFieldInput`, `FieldFilter`, `ObjectFilter`),
keeping the schema shape identical to main for those members. On create
it acts as a legacy alias mapped to `!isUIReadOnly` (`isUIEditable` wins
when both are provided); on update it is ignored, exactly as on main (it
was never an editable property). Filtering on the deprecated member
keeps working until the column is dropped at upgrade time; after that it
is a deprecated no-op surface kept only for schema compatibility.
**Removal plan for next release: drop `isUIReadOnly` from the output
DTOs (and resolvers' `@ResolveField`s), from the input/filter types,
from the create-input mapping, and the `@WasRemovedInUpgrade`-retained
entity columns and decorators.**
## ⚠️ Webhook / database-event payload shape change
The `database-event-payload` type in `twenty-shared` got a clean rename
(no alias): metadata snapshots in webhook and database-event payloads
now carry `isUIEditable` (and `isUICreatable` at object level) **instead
of** `isUIReadOnly`, with inverted polarity. Consumers of these payloads
that read `isUIReadOnly` must switch to `isUIEditable`.
## New manifest properties (app-developer DX)
Application developers can now set these flags in their app manifests
(purely additive — existing manifests and older `twenty-sdk` versions
are unaffected, defaults apply when omitted):
- `objects[].isUICreatable?: boolean` (default `true`)
- `objects[].isUIEditable?: boolean` (default `true`)
- `fields[].isUIEditable?: boolean` (default `true`)
The manifest converters previously hardcoded `isUIReadOnly: false`; they
now read the manifest values with `?? true` defaults. The types are
re-exported through `twenty-sdk` from `twenty-shared`.
## Migration & backfill
- One fast instance command: adds `isUIEditable` (NOT NULL default
`true`) on `core."objectMetadata"` and `core."fieldMetadata"`, backfills
`isUIEditable = false` exactly where `isUIReadOnly = true`, drops
`isUIReadOnly`, and adds `isUICreatable` (default `true`) on
`objectMetadata`. The `down` is the exact inverse. Uses `ADD/DROP COLUMN
IF (NOT) EXISTS`, matching the 2-12 drop-`isCustom` precedent. Verified
up and down in separate transactions against a dev database with exact
backfill counts.
- **Cross-version upgrade safety (multi-version self-hosted jumps):**
the upgrade sequence interleaves per version (instance → workspace
commands), so pre-2.13 workspace commands run **before** the 2.13 rename
when an old instance jumps several versions. Following the `isCustom`
precedent: `isUIEditable`/`isUICreatable` are marked
`@WasIntroducedInUpgrade` and `isUIReadOnly` stays on both entities as
`@WasRemovedInUpgrade`, so the upgrade-aware entity metadata adapter
hides the not-yet-existing columns (and keeps the legacy column live) at
pre-2.13 cursors. **No committed upgrade command outside the 2-13
directory is modified**: the old 1-21/2-8/2-9 commands keep their
original `isUIReadOnly: true` inputs, which still compile (entity
property retained, deprecated create-input alias mapped) and still
produce the correct legacy column writes pre-rename.
- A 2-13 workspace command (`sync-standard-ui-capability-flags`)
re-syncs `isUICreatable` **and** `isUIEditable` on standard objects and
`isUIEditable` on standard fields from the standard-application
definitions. This backfills `isUICreatable: false` on
`workflowRun`/`workflowVersion`/`workspaceMember` and heals fields
created mid-cross-upgrade by pre-2.13 commands (whose hidden
`isUIEditable` value cannot reach the insert). Both 2-13 sync commands
pass `isSystemBuild: true` — the flat metadata validator otherwise
rejects direct updates to system objects (verified against a
deliberately drifted dev database; the run is idempotent).
- A second 2-13 workspace command re-syncs the create-record command
availability expression.
## Testing
- Unit tests for `canCreateRecordsForObjectMetadataItem`
(flag/permission/system combinations) and for the manifest converters
(flags set / omitted → defaults).
- Full `upgrade --dry-run` boots the sequence (107 steps) and validates
the upgrade-aware decorator references; both 2-13 sync commands verified
end to end against real drift and re-run idempotently.
- Schema verified by live introspection after the input-alias restore:
all four input/filter members match main, output deprecations intact;
frontend metadata types and `twenty-client-sdk` schema regenerated from
the running server.
- Read-only-related and touched jest suites pass on both packages;
typecheck and lint pass on `twenty-server` and `twenty-front`.
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a
href="https://cursor.com/agents/bc-0f3e04cb-b04a-40be-8330-5609c4538e8a"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a
href="https://cursor.com/background-agent?bcId=bc-0f3e04cb-b04a-40be-8330-5609c4538e8a"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21504?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: Cursor Agent <cursoragent@cursor.com>
|
||
|
|
9bb98fa5b5 |
fix(billing): don't crash when workspace has no active subscription (#21510)
## Problem
Sentry (high severity, SLA-breaching): `Billing Subscription Not Found:
No active subscription found for workspace …`
The `billingSubscription` workspace-cache provider
(`WorkspaceBillingSubscriptionCacheService.computeForCache`) called
`getCurrentBillingSubscriptionOrThrow`. For a workspace whose
subscription is fully canceled, `getCurrentBillingSubscription` filters
out `Canceled` and returns `undefined`, so the provider **threw**
`BILLING_SUBSCRIPTION_NOT_FOUND`.
That cache key is read on every usage-recording path:
- workflow execution
(`WorkflowExecutorWorkspaceService.sendWorkflowNodeRunEvent`)
- AI usage (`AiBillingService`)
- logic-function execution (`LogicFunctionExecutorService`)
- app charges (`AppBillingService`)
- the gate `BillingUsageService.canFeatureBeUsed` /
`hasAvailableCredits` / `decrementAvailableCreditsInCache`
- the cancellation webhook
(`invalidateAndRecompute('billingSubscription')`)
So any of these throws an unhandled exception for a
no-active-subscription workspace. The intent was clearly to tolerate
this state — `canFeatureBeUsed` already guards with
`isDefined(billingSubscription)` and the workflow runner logs *"there is
no subscription for this workspace"* — but the throwing provider made
those guards unreachable.
## Fix
- `computeForCache` now returns `FlatBillingSubscription | null` via the
non-throwing `getCurrentBillingSubscription`, and the cache type allows
`null`.
- Every consumer guards the absent case (`isDefined` / optional
chaining) and no-ops: usage events still emit with an undefined
`periodStart`, credits aren't decremented, `hasAvailableCredits` returns
`false`.
- `getCurrentBillingSubscriptionOrThrow` is **left untouched** for the
many callers (resolver, subscription-update, etc.) that genuinely
require a subscription.
## Test
Adds `workspace-billing-subscription-cache.service.spec.ts`: the
provider returns `null` when there's no active subscription (regression)
and the flattened subscription when one exists.
All 142 tests across the billing / ai-billing / workflow-executor suites
pass; `oxlint --type-aware` and `oxfmt` are clean.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21510?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. -->
|
||
|
|
ba94c3b857 |
feat(workflow): idempotent stop + retry failed runs from failing step (#21458)
https://github.com/user-attachments/assets/5a25396f-8959-4bd8-93cb-1187559ffe5f ## Summary Two workflow-run improvements, with all non-trivial logic isolated in pure, unit-tested utils. ### 1. Idempotent stop `stopWorkflowRun` no longer throws when a run is already in a terminal status (`COMPLETED` / `FAILED` / `STOPPED`) or already `STOPPING`; it returns the run unchanged. This fixes: - bulk stop aborting on the first non-stoppable run in a mixed/select-all selection, - the click-vs-processing race on a single run (run finishes between click and mutation). It also releases the cached not-started throttle slot when stopping a `NOT_STARTED` run (prevents counter drift), and ends runs with no `state` directly. ### 2. Retry a failed run from the failing step New `retryWorkflowRun` mutation (same guards/passthrough as `stopWorkflowRun`). It resets the failed step(s) to `NOT_STARTED`, flips the run to `RUNNING`, and enqueues a `RunWorkflowJob` with the steps to re-execute; downstream execution and status computation are unchanged. Logic lives in pure utils: - `build-retry-step-infos.util.ts` - decides per failed step what to reset; delegates iterator-specific logic to `build-retry-iterator-step-infos.util.ts` (an iterator that failed mid-loop is restored to `RUNNING` with cursor preserved, an iterator that failed itself restarts its whole loop). - `get-runnable-step-ids.util.ts` - reuses the executor's `shouldExecuteStep` to also resume branches that never started (avoids hangs), excluding loop-interior steps. The service method only orchestrates; the job's status check is a race guard (retriability is enforced in the service before enqueue). A "Retry" command menu item surfaces only for `FAILED` runs (`someEquals(selectedRecords, "status", "FAILED")`). ### 3. Keep the run diagram visible across regenerations The run diagram is regenerated on every run state change, producing fresh nodes without the dimensions Reactflow had measured. Reactflow hides unmeasured nodes until it re-measures them, so the diagram could flicker and disappear when the last regeneration before going idle left nodes unmeasured (reproducible after retrying a failed run). The regenerated nodes now carry over the previously measured dimensions (by id) so they stay rendered. ## Test plan - [x] Unit tests for both retry utils (9 cases: plain failed step, non-failed untouched, iterator mid-loop restore, iterator self-failure, frontier parent gating, entry steps, loop-interior exclusion, parallel branches) - [x] `twenty-server` + `twenty-front` typecheck - [x] `lint:diff-with-main` clean for both packages - [x] Manual: retry a failed run repeatedly and confirm the diagram stays visible - [ ] Manual: stop a COMPLETED/mixed selection (no error), retry a failed run and confirm it resumes from the failing step |