Commit Graph

14156 Commits

Author SHA1 Message Date
Raphaël Bosi 453f3479ab Accept a singleton or filter in the GraphQL filter walker (#23738)
`RecordGqlOperationFilter` types `or` as `RecordGqlOperationFilter[] |
RecordGqlOperationFilter`, so both `{ or: [{ name: { ilike: '%acme%' }
}] }` and `{ or: { name: { ilike: '%acme%' } } }` are valid.
`applyLogicalGroup` went straight to `filters.forEach(...)`, so the
non-array form threw `TypeError: filters.forEach is not a function`
instead of returning records. Only `or` is affected: `and` is always an
array and `not` is always a single object.

This is not a new bug. The same assumption existed before the walker was
extracted, when `parseKeyFilter` did the `value.forEach` inline. Sentry
surfaced it on #23369, and it was left out of that PR to keep it scoped.

The fix mirrors `renderLogicalGroup` in the RLS SQL renderer, which
already normalizes a singleton to an array on its first line, so the two
walkers over this filter format now accept the same shapes.
2026-08-04 11:06:59 +00:00
github-actions[bot] 0408816781 i18n - translations (#23746)
Created by Github action

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

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-08-04 12:32:22 +02:00
neo773 8e5bdcc781 webhook subscriptions error handling (#23707)
A `subscriptionRemoved` lifecycle notification was routed to
`renewSubscription`, which PATCHes a subscription Microsoft has already
deleted and always 404s
([TWENTY-SERVER-J1N](https://twenty-v7.sentry.io/issues/7604034376/),
884 events). Every sampled webhook event on that issue was
`subscriptionRemoved`.

Each lifecycle event now gets its own path: `subscriptionRemoved`
recreates and resyncs the gap, `reauthorizationRequired` renews in
place, `missed` resyncs, unrecognised events are logged and ignored.
Provider errors are parsed into driver exception codes following the
message-import drivers.

Max retry for the renewal cron is deliberately left out and will follow
separately.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23707?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 <huzef@twenty.com>
2026-08-04 10:23:52 +00:00
Paul Rastoin 997b2c38de Add cookie-session integration test suite (#23715)
Stacked on #23642. Integration suite for the cookie-session surface,
organized as one successful/failing spec pair per stage of the session
lifecycle. 14 spec files, ~36 tests, all over real HTTP against the
booted app.

## Coverage by stage

**1. Session creation on auth exchanges**
(`successful-`/`failing-session-creation`)
Flag gating (default off: tokens, no cookie, no row); httpOnly cookie
snapshot with 180d expiry window; SHA-256 hash-at-rest with the row
bound to the apple seed workspace; scripted sign-ins without an Origin
header still get the cookie; login-CSRF refuses the cookie for
disallowed origins while returning the token pair; sign-in over an
existing session revokes it as `SUPERSEDED`; a failed credentials
exchange mints nothing.

**2. Cookie delivery** (`successful-session-cookie-delivery`,
`secure-deployment-session-cookie`)
The runtime side door (`AUTH_COOKIE_SAME_SITE=none` forces the secure
path) pins the `__Host-`/`Secure`/`SameSite=None` variant in the default
CI run. The exact production combination (`__Host-`, `Secure`,
`SameSite=Lax`) is covered by a dedicated spec that requires the app to
boot with an https `SERVER_URL`: the secure branch is decided by config,
never the transport, so no TLS is needed. It skips itself on plain-http
boots; CI runs it as an extra step on one shard with
`SERVER_URL=https://localhost:3000`, including the `__Host-` round-trip
and the plain-cookie-name downgrade refusal.

**3. Per-request authentication and the CSRF read gate**
(`successful-`/`failing-session-cookie-authentication`)
A cookie-only request resolves the seeded user; a `sess_` token
presented as Bearer is rejected; cookie-authenticated unsafe requests
with a disallowed or missing Origin get 403 `CSRF_ORIGIN_MISMATCH`; an
unknown session token is unauthenticated and its dead cookie is cleared.

**3b. Workspace binding** (`successful-session-workspace-binding`)
Tim signs into both seeded workspaces (apple and yc); each session row
is bound to the workspace its exchange selected (`workspaceId` and
`userWorkspaceId` pinned to the seed ids), and each cookie resolves to
its own workspace context, with no request-side input able to pivot a
session across workspaces.

**3c. Credentialed CORS** (`cors-credentialed-origins`)
Allowlisted origins get the reflected `Access-Control-Allow-Origin` plus
`Access-Control-Allow-Credentials: true` and `Vary: Origin`, preflight
included; other origins keep the public wildcard. See tooling notes:
this surface was previously untestable.

**4. Sessions API** (`successful-`/`failing-user-sessions-api`)
`currentUserSessions` marks exactly the presented session as current;
`revokeUserSession` revokes by id (`USER_REVOKED`) and drops it from the
listing; `revokeAllOtherUserSessions` spares the presented session;
cross-user revocation and unauthenticated listing are refused.

**5. Exits** (`successful-sign-out`, `failing-session-expiration`)
`signOut` revokes with `USER_SIGN_OUT`, clears the cookie, and reuse
fails immediately (cache invalidated, not TTL-bound); a cookie-less
sign-out clears nothing, so a cross-site POST cannot log a visitor out;
absolute-lifetime and idle-timeout expiry both reject and clear the
cookie.

**7. Cleanup cron** (`user-session-cleanup-cron`)
Both halves run in-process against fixtures spanning the 30d retention
boundary. Sessions: expired/revoked-beyond-retention deleted; active,
recently-expired, and idle-expired rows survive (the idle case pins the
known predicate gap). Refresh tokens: old-expired and old-revoked
deleted, fresh kept, and a long-expired token of another type survives,
pinning the `type` filter that keeps the shared `appToken` table safe
from the hard-delete.

Not covered here by design: the impersonation park/restore sub-funnel
(stage 6, follow-up) and the client-side funnel (stage 8, front-end
scope). Password-change revocation and the renewal bridge are also left
to follow-ups.

## How the flag is flipped

`AUTH_COOKIE_SESSIONS_ENABLED` (and `AUTH_COOKIE_SAME_SITE` for the
secure side door) are toggled at runtime through the admin panel config
API, reusing the `twenty-config` test utils: `DatabaseConfigDriver.set`
updates its cache synchronously and `TwentyConfigService` consults the
DB driver before the env driver. No `.env.test` change, no app reboot,
runs in the default CI environment without the `ci:auth-cookie-sessions`
label. `SERVER_URL` is env-only, hence the dedicated CI step for the
production secure-deployment spec.

## Shared tooling changes

- **`applyCredentialedCors` extraction (src change)**: the integration
harness booted with Nest's wildcard `cors: true`, not the
credentialed-allowlist setup living in `main.ts`, so the CORS surface
was untestable by construction. The setup moved into
`applyCredentialedCors`, now called by both the production bootstrap and
`createApp`, making the harness's CORS behavior the deployed one.
Behavior-neutral for production.
- `makeMetadataAPIRequest` accepts an explicit `null` token for
unauthenticated requests. Passing `undefined` silently fell back to the
default admin token (parameter defaults apply to `undefined`), which
made supposedly public requests Bearer-authenticated, bypassing both the
cookie auth path and the CSRF middleware. Existing call sites are
unaffected.
- The `GetLoginTokenFromCredentials` / `GetAuthTokensFromLoginToken`
documents moved into shared query factories; the workspace-origin
builder is extracted and generalized to any seeded subdomain
(`buildWorkspaceOriginForSubdomain`, reused by
`getAccessTokenForCredentials`).
- Suite-local helpers: `signInWithCookieCapture` (full credentials
exchange returning the raw supertest response, with a
`workspaceSubdomain` option), `postMetadataOperationWithHeaders`
(Origin/Cookie header control), cookie extraction for both cookie names,
clearing-cookie detection, snapshot normalization (token and expiry
redacted), and shared `ALLOWED_ORIGIN`/`DISALLOWED_ORIGIN` constants
derived from `FRONTEND_URL`.

Verified locally: full suite green in CI mode on both plain-http and
https-`SERVER_URL` boots; oxlint and tsc clean.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-08-04 10:05:12 +00:00
github-actions[bot] 2db730b65f i18n - translations (#23740)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-08-04 11:52:40 +02:00
twenty-pr[bot] 3ab6bb7915 chore: bump version to 2.28.0 (#23730)
## 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/23730?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-08-04 09:47:09 +00:00
Raphaël Bosi a26b507361 Enforce row-level permissions on joined relations (#23369)
Row-level permission predicates were only ever applied to a query's main
alias, so any SQL join leaked rows the caller is not allowed to see. The
visible symptom: dashboard charts grouped by a relation field (e.g.
Opportunities by Company) read the group dimension off an unfiltered
joined table, surfacing hidden companies as chart labels.

`WorkspaceSelectQueryBuilder` now applies the joined object's predicate
to every relation join's `ON` condition. Using `ON` rather than `WHERE`
keeps left-join semantics correct: a visible record linked to a hidden
related row is still counted, it just falls into the null group instead
of being attributed to the hidden row.

This closes the same class of leak in relation filters and
order-by-on-relation, plus the three paths that serialize a builder via
`.getQuery()` and never reach the execution overrides (group-by with
records, per-parent relation limiting, mutation id subqueries). The
per-parent fix also stops hidden rows from consuming `LIMIT` slots
before being filtered out.

```mermaid
flowchart TD
    A["WorkspaceSelectQueryBuilder<br/>SELECT FROM person LEFT JOIN company"]
    A --> B["getMany / getOne / getCount / execute<br/>(execution overrides)"]
    A --> C["getQuery() serialization:<br/>group-by with records,<br/>per-parent relation limit,<br/>mutation id subquery"]
    B --> D["validatePermissions()"]
    D --> E["applyRowLevelPermissionPredicates<br/>ToMainAliasAndJoinedRelations()"]
    C --> E
    E --> F["main alias:<br/>WHERE person predicate"]
    E --> G["every relation join:<br/>ON person.companyId = company.id<br/>AND company predicate"]
    F --> H["hidden companies never surface as group dimensions,<br/>relation-filter matches or sort keys;<br/>a hidden link sorts as NULL and the row is still counted"]
    G --> H
```

The last two commits remove the duplication this fix would otherwise
have introduced: one shared `and`/`or`/`not` filter walker (the GraphQL
filter parser and the RLS util were verbatim forks), one RLS
record-filter resolver used by all three call sites, and one shared set
of RLS integration-test fixtures. Behaviour-preserving, with new
characterization tests pinning the emitted condition tree.

Reviewer notes:
- Results change where a join is involved: relation filters no longer
match hidden related records, and order-by-on-relation sorts
hidden-linked rows as null, which can shift pagination.
- Joins on subqueries/custom tables are skipped, and objects with no
predicates for the role are a no-op, so admins and system contexts are
unaffected.
- Timeline messaging inner joins are filtered too, so thread counts can
change for restricted roles.
- Predicates that need the current workspace member (Me) are still
skipped for API key and application contexts, on joins as on the main
alias.
- The join renderer skips the field-level read-permission check the
main-alias parser performs: predicates on read-restricted fields still
filter joins, and the field values are never selected.
- One user-facing change beyond the leak fix: the empty-array filter
error no longer echoes the submitted value back (`Invalid filter value:
"<value>"` -> `Invalid filter value`), on every filter path rather than
just RLS. Catalogs are not regenerated here, so it falls back to English
until the next i18n sync.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23369?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-04 09:43:45 +00:00
Thomas des Francs 91b6cbd320 Update website release notes through 2.26 (#23736)
## Summary

- Add weekly, user-facing release notes from 2.1 through 2.26.
- Highlight completed, generally available product features and exclude
Labs or rollout-gated work.
- Update the Releases menu preview to the latest 2.26 entry.

## Before/After

Before: production ends at 2.0. After: the changelog includes weekly
releases through 2.26.

<img width="2268" height="720" alt="before-after"
src="https://github.com/user-attachments/assets/7ebf0154-596a-4846-b148-3d9aeff7cf0e"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23736?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-04 11:16:15 +02:00
Raphaël Bosi 5bafaa0994 Hide onboarding credits when billing is disabled (#23717)
Onboarding advertises free credits (the header pill and the green "Earn
+N free credits" tags) even when `IS_BILLING_ENABLED` is false,
promising a reward that can never be granted: `creditWorkspaceBalance`
already no-ops when billing is off.

The server now omits the `onboarding` credit-rewards block from the
client config when billing is disabled, which hides every reward tag on
its own since they all render behind a defined-config guard. The header
pill gets an explicit gate.

Also stops treating onboarding invites as reward-eligible when billing
is off, so they are minted as plain invitation tokens and the 10-invite
`ONBOARDING_INVITE_TEAM_MAX_INVITES` cap no longer applies to
self-hosted instances.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23717?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-04 08:31:24 +00:00
Raphaël Bosi 6f6da14cc8 Stop showing raw chunk load errors (#23571)
On iOS Safari, a failed chunk load showed a snackbar containing the raw
browser string `Importing a module script failed.`

`PromiseRejectionEffect` snackbars the raw `error.message` of any
unhandled rejection, so a floating dynamic import leaked browser
internals to the user. It now skips the snackbar for stale chunk errors,
which are still captured by Sentry.

This only changes what the user sees, not the underlying fetch failure.
2026-08-04 08:24:59 +00:00
Paul Rastoin 850d3d70fc chore(codeowners): guard .claude, .mcp.json and CLAUDE.md (#23734)
## What

Adds `.claude/`, `.mcp.json` and `CLAUDE.md` to CODEOWNERS.

## Why

These files configure the coding agent that maintainers attach to PRs:
`SessionStart` hooks, MCP servers, and agent instructions. They were
previously outside CODEOWNERS coverage (which only spanned `.github/`
and `.yarnrc.yml`), so a change to any of them could land on `main`
without core-team review.

CODEOWNERS gates merge approval only. It does not affect an unmerged
branch that a session merely checks out, so this closes the "malicious
agent config quietly lands on main" path, not fork-branch execution. It
is one layer, not the whole answer.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23734?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-04 08:11:38 +00:00
Shinu Cherian 4e3747f3a9 fix(twenty-front): expand HTML preview viewport in DocumentViewer (#23668) (#23676)
## Description
Fixes #23668.

When previewing `.html` or `.htm` files in the file preview modal,
`@cyntler/react-doc-viewer` mounts an `iframe` inside `#html-renderer`.

Previously, `StyledDocumentViewerContainer` applied `height: 100%;
width: 100%;` to `#react-doc-viewer`, `#proxy-renderer`, and
`#msdoc-renderer`, but omitted `#html-renderer` and its inner `iframe`.
As a result, the preview iframe defaulted to inline iframe bounds
instead of expanding to fill the modal container.

This PR adds `#html-renderer`, `#html-renderer iframe`, and `iframe`
selectors to `StyledDocumentViewerContainer`, ensuring HTML previews
expand fully within the modal viewport.

## Testing
- Verified StyledDocumentViewerContainer CSS rules target
`#html-renderer` and `iframe` elements.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23676?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-04 06:46:04 +00:00
Félix Malfait 66e0f620bc Default workspaceMember openRecordIn when the workspace is not upgraded yet (#23723)
`WorkspaceMemberDTO.openRecordIn` is `@Field(() => OpenRecordIn, {
nullable: false })`, and the transpiler passed the entity value straight
through. The field is created per workspace by the 2-27 workspace
command `upgrade:2-27:add-workspace-member-open-record-in`, which runs
*after* the code is already serving traffic — the deploy job only runs
instance commands. Until a workspace's turn comes, `openRecordIn` is
`undefined`, GraphQL raises `Cannot return null for non-nullable field
WorkspaceMember.openRecordIn`, `GetCurrentUser` fails outright, and
nobody in that workspace can load the app.

This is not theoretical. On main it broke all 68 live workspaces and
stayed broken for three days: the instance command ran on Jul 31 with
#23614, and `core."upgradeMigration"` had no row for any 2-27 workspace
command until the sequence was run manually today. On prod the window is
however long `upgrade` takes to walk every workspace sequentially.

`SIDE_PANEL` is already the declared `defaultValue` of the standard
field, so behaviour is unchanged once a workspace is upgraded. The same
function already guards `userEmail` this way.

The other write path, `user-workspace.service.ts` inserting
`openRecordIn` on workspace member creation, does not need a guard: the
workspace entity metadata is built per workspace from its own field
metadata, so TypeORM's insert builder omits a property that has no
column rather than failing.

`OpenRecordIn` moves from a type-only to a value import since it is now
referenced at runtime.

## Test

Two cases in a new spec: the value is preserved when present, and falls
back to `SIDE_PANEL` when the workspace has not been upgraded. The
second fails on `main`.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23723?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-04 06:51:53 +02:00
github-actions[bot] e623d6fd88 i18n - docs translations (#23729)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-08-04 05:39:19 +02:00
github-actions[bot] a042b350f7 i18n - translations (#23728)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-08-04 04:55:37 +02:00
Abdul Rahman 3a646ffcb0 feat(connections): add an onDisconnect lifecycle hook to connection providers (#23538)
Platform half of the follow-up to
https://github.com/twentyhq/twenty/pull/22984#discussion_r3673946334.
The Slack app claims a `team_id` on connect and had no way to release
it, because connection providers only had an on-connect hook. Nothing
here is Slack-specific, so it targets `main`. The app side is #23540, on
top of `feat/slack-bot`, and waits on this plus an SDK release.

## What changes

`defineConnectionProvider` accepts `onDisconnectLogicFunction` alongside
`onConnectLogicFunction`. It is stored on
`connectionProvider.onDisconnectLogicFunctionUniversalIdentifier` (fast
instance command `2.26.0_...1785350000000`) and enqueued right after the
`ConnectedAccount` row is deleted, in the disconnecting workspace, with
the same payload as on-connect:

```ts
type OnDisconnectPayload = {
  connectionProviderId: string;
  connectionProviderName: string;
  connectedAccountId: string;
};
```

The `ConnectedAccount` is gone by the time the hook runs, so
`getConnection` no longer resolves. Anything the cleanup needs has to be
in the key-value store, written at connect time and keyed by
`connectedAccountId`. The docs section spells that out, along with the
fact that uninstalling an app drops its connections through a cascade
that never reaches this hook, where `uninstallLogicFunction` is the
right tool instead.

Both dispatches moved into a new
`ConnectionProviderLifecycleHookService`, so
`ConnectionProviderOAuthFlowService` no longer owns hook plumbing and
`ConnectedAccountMetadataService.delete` can reuse it. On-connect
behaviour is unchanged: best effort, never blocks the caller, failures
go to Sentry.

## Tests

- `connection-provider-lifecycle-hook.service.spec.ts`: the on-connect
cases moved over, plus on-disconnect dispatch, no-hook, and
missing-provider cases
- `connection-provider-oauth-flow.service.spec.ts`: now asserts
delegation to the lifecycle hook service
- SDK validation, manifest duplicate-identifier, and manifest to flat
converter specs extended

Server unit tests and typecheck for shared, sdk and server pass locally.
2026-08-04 02:47:23 +00:00
Abdul Rahman 5ffa121e59 feat(slack): implement channel welcome message functionality (#23699)
https://github.com/user-attachments/assets/a77aa941-da48-4c30-8e14-587516c19ac4


Added a new feature that allows the bot to introduce itself when added
to a Slack channel. This includes a welcome message and a detailed
thread reply outlining its capabilities. The implementation includes new
utility functions for handling the welcome event, managing welcome
state, and posting messages. Updated relevant logic functions to support
this feature, ensuring the bot can provide a seamless introduction to
users in new channels.

- Introduced `slack-channel-welcome` logic function.
- Added constants for welcome message text.
- Implemented event parsing and handling for `member_joined_channel`.
- Updated `slack-events-resolver` to route welcome events appropriately.
2026-08-03 19:08:40 +00:00
github-actions[bot] 1d367bbc57 i18n - docs translations (#23721)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-08-03 21:07:03 +02:00
Félix Malfait 116c04d8b2 Record and enforce per-user OAuth application authorizations (#23678)
Sits on `main` now that #23642 has merged. 18 files changed.

## Why

Application tokens are stateless JWTs. When a user completes an OAuth
`authorization_code` exchange, the server issues an access/refresh pair
carrying `userId` as a claim and stores nothing. So today:

- there is no record that a person ever authorized an app, hence nothing
to list on a settings screen
- there is no way for that person to take an app&#39;s access away. The
only revocation that exists is uninstalling the app, which is
workspace-wide and admin-only
- `/oauth/revoke` accepted a refresh token, logged it and did nothing,
because there was no state to change

`client_credentials` is unaffected: no user is involved and it returns
an access token with no refresh token.

## What

**`core."applicationAuthorization"`**, one row per (user, application),
unique on that pair so re-authorizing updates in place. Written at the
`authorization_code` exchange, before the token pair is issued, so a
refresh token is never handed out without the grant that makes it
redeemable.

A dedicated table rather than a new `AppTokenType`: this is a grant
keyed on identity, not a token keyed on a secret, and `appToken` is
already overloaded.

FKs to user, workspace, application and userWorkspace all cascade, which
covers hard deletes. Membership removal soft-deletes the `userWorkspace`
row, so that cascade does not fire and the grant outlives the
membership. The refresh path therefore rechecks membership on every
renewal rather than trusting the row&#39;s existence.

**Enforcement.** `refresh_token` checks the row when the token carries a
user, and returns `invalid_grant` if it is revoked. Revoking does not
kill live access tokens, so access ends within one access-token window
(`APPLICATION_ACCESS_TOKEN_EXPIRES_IN`, 30 minutes) rather than
instantly. The alternative is a DB read on every API request, which is
not worth it for a 30 minute tail; the UI should say so.

**RFC 7009 revocation now revokes.** Revoking a refresh token revokes
the authorization behind it. It also now checks the token was issued to
the client asking, which it never did before. That check did not matter
while revocation was a no-op; it does now.

**Introspection** reports a refresh token inactive once its
authorization is revoked. Access tokens keep reporting active until they
expire, because they genuinely still work.

**API:** `currentUserApplicationAuthorizations` and
`revokeApplicationAuthorization`, both behind `UserAuthGuard`. The
mutation scopes by `userId` inside the `UPDATE` rather than
read-then-write, so one user cannot revoke another&#39;s authorization
by guessing an id.

## Backwards compatibility

Refresh tokens already in the wild have no row. Rejecting them would
sign every live integration out on deploy, so the first refresh
backfills the grant that was always implied. A revoked authorization
keeps its row, so this never resurrects access someone turned off, and
the backfill is insert-only so it cannot overwrite a real consent. If
the user has since left the workspace, the refresh fails instead.

Those tokens carry no scope claim and no record of when consent was
given, so `scopes` and `lastAuthorizedAt` are nullable and left null on
a backfilled row. Null means &#34;the original consent is not on
record&#34; rather than a guess assembled from what the application
declares today; a real re-authorization fills both in. Revoking such a
token lays the row down before marking it, so the revocation sticks
instead of being undone by the next refresh.

## Not in this PR

The settings UI, following how #23643 shipped the sessions API and
#23645 the devices screen.

Introspection still reports a refresh token active once the membership
is gone. That matches access tokens, which genuinely keep working in
that case, so closing it belongs with the wider question of validating
membership on every application-token request.

## Testing

- 29 unit tests across the authorization service and the three OAuth
grant paths
- 9 integration tests on `/oauth/token`, `/oauth/revoke` and the GraphQL
API: scopes as granted are recorded, revoking blocks the next refresh,
re-authorizing reinstates, a pre-record token backfills without
inventing a consent, a revoked pre-record token stays revoked, the
authorization is listed to the user who granted it, revoking from that
list stops the refresh token being redeemed, a repeated revocation
reports no-op, and another user can neither see nor revoke it
- the cross-user isolation and revoke-from-list tests are
mutation-checked: dropping the `userId` scoping from
`revokeAuthorizationById` fails only the isolation test, and disabling
the `revokedAt` check in `oauth.service.ts` fails the revoke-from-list
test plus two pre-existing ones
- full `twenty-server` suite green
- instance command applied against a fresh `database:reset`,
table/index/FK shape verified against `information_schema`

Closes part of https://github.com/twentyhq/core-team-issues/issues/2747

---------

Co-authored-by: prastoin <45004772+prastoin@users.noreply.github.com>
2026-08-03 20:59:28 +02:00
nitin b28fc54b44 Classify Recall no-capture sub codes as NOT_RECORDED in call-recorder app (#23693)
Second part of twentyhq/core-team-issues#2706, following #23478 which
shipped the NOT_RECORDED status and workspace upgrade: the call-recorder
app now classifies benign no-capture outcomes (bot never admitted,
meeting not started, nobody joined) as NOT_RECORDED instead of FAILED.

- Parse status sub codes from Recall webhooks and bot snapshots, and map
no-capture sub codes to NOT_RECORDED with the sub code stored as the
failure reason
- Derive NOT_RECORDED from bot snapshots during sync when the bot
finished without a recording and a no-capture leave is in its history
- Treat NOT_RECORDED as terminal alongside FAILED: no artifact-import
completion, no late-event flips between the two; calendar reconciliation
may reset it to SCHEDULED for upcoming meetings
- Prefer the sub code over the status code in FAILED reasons
- Bump the app to 1.6.0 and require twenty >=2.26.0, where the status
exists

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23693?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-03 18:19:34 +00:00
github-actions[bot] 713fae189d i18n - translations (#23720)
Created by Github action

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

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-08-03 20:02:41 +02:00
Félix Malfait 267ecb12db Migrate web auth from localStorage token pairs to httpOnly cookie sessions (#23642) 2026-08-03 19:54:54 +02:00
Félix Malfait f8e3fd110d Bound an application by its own role as well as the user's (#23680)
## Why

When an application acts on someone's behalf its token carries `userId`
and `userWorkspaceId` alongside `applicationId`, and the application
then received **that person's permissions in full**. The role it
installs with was never consulted, so it was not a bound on what the
application could do for them. It was meant to be an intersection.

`permissions.service.ts` made this visible: the branches are `apiKeyId →
userWorkspaceId → applicationId` and each returns early, so with both
present the user branch won and `application.defaultRoleId` was never
read. The same was true on the object and row-level paths, for different
reasons.

## What had to change

Three independent causes, all of which blocked the intersection from
existing or from being enforced.

**The application was thrown away before anything could use it.**
`workspace-auth-context.middleware.ts` built a `type: 'user'` context
when both principals were present, and `UserWorkspaceAuthContext` had no
slot for an application. It now carries an optional one.

Additive rather than a new union member on purpose. Nothing in the
server exhaustively checks this union (no `assertUnreachable`, one
`switch`, in Sentry tagging), so a sixth member would have compiled fine
and then fallen through actor attribution, that switch, and
`metadata-event-emitter.ts` silently. The additive change leaves all
four type guards returning identical booleans.

**Role resolution returned a single id.** The rule itself now lives in
one place, `resolveRoleIdsForUser`: a user's role, narrowed by the
application's if it declared one, never the same id twice.
`resolveRoleIdsFromAuthContext` and `resolveRolePermissionConfig` build
`{ intersectionOf: [...] }` from it, which `getRepository` already
applied over N roles. A user with no role still resolves to nothing, so
an application can never stand in for a missing user role.

**Row-level security ignored all of it.** RLS was re-derived from a
single role at query time, so it would have been unaffected by any
intersection. Each role is now compiled on its own and the resulting
filters are ANDed.

That last choice matters. Merging the raw predicates and groups first
would have been wrong: `computeRecordGqlOperationFilter` honours only
the first parentless group, so concatenating two roles' groups makes one
role's predicates vanish, **widening** access. Compiling per role and
ANDing needs no synthetic groups, no re-parenting and no `twenty-shared`
type change, and reuses the single-role logic untouched.

Subscriptions go through the same rule. An event stream resolved only
the subscriber's role, so a stream opened by an application acting for
someone was filtered by that person's role alone. The stream now records
the application it was opened by and the publisher intersects both roles
for object permissions, restricted fields and RLS, exactly as a query
does.

Two smaller fixes fall out:

- `getObjectsPermissionsFromRolePermissionConfig` had a `// Multi-role
union/intersection is not ready — use the first assigned role only`
shortcut and now intersects.
- `computePermissionIntersection` hardcoded empty row-level predicate
arrays, which is why RLS-constrained fields were not exempted from the
field-permission check on insert and could fail spuriously. It now
reports the fields **every** role constrains. Reporting fields
constrained by only one role would be worse than the original bug: the
insert guard waives a field-update deny on them, so one role's row-level
rule would cancel another role's deny.

## Behaviour on the edges

**An application that declares no role adds no bound.** `defaultRoleId`
stays null whenever a manifest omits `defaultRoleUniversalIdentifier`,
which is the common case, so denying would have broken a lot of
installed applications. Behaviour changes only for applications that
actually declared a role.

To stop that being permanent, `defaultRoleUniversalIdentifier` should
become required for new applications. Hard-requiring it needs a backfill
for existing installs, so it is not in this PR.

**An application that cannot be found denies.** That is not the same as
one that declared no role, and treating it as such would have let a
token naming a deleted application fall back to the full permissions of
the user it acts for.

**A role that cannot be resolved denies.** `application.defaultRoleId`
is a plain uuid column with no foreign key, and role deletion does not
clear it, so it can dangle. A bound we cannot apply must not let the
remaining roles decide on their own, so the ORM path,
`getObjectsPermissionsFromRolePermissionConfig` and the subscription
publisher all return no permissions in that case rather than falling
back.

## Testing

- Full `twenty-server` unit suite green (896 suites, 7353 tests)
- New spec for `resolveRoleIdsFromAuthContext`: both roles, application
with no declared role, application holding the user's own role, user
with no role, api key, application-only, system
- New spec for multi-role RLS, including the case this fixes (a
restricted role intersected with an unrestricted one keeps the
restriction) and two restricted roles ANDing
- `permissions.service.spec.ts` had **no coverage of the application
branch at all** (`ApplicationEntity` was mocked as `{}`); it now has a
real mock plus user-grants/application-denies, the reverse, both-grant,
the null-role fallback, a shared role, and a missing application
- First coverage of non-empty row-level predicates through
`computePermissionIntersection`, including a field constrained by one
role only
- Subscription publisher: application role denies, both allow,
application role dangling, and both roles reaching the RLS filter
- Updated the two specs that asserted the old behaviour: the middleware
dropping the application, and "use the first role when multiple are
provided"

No schema, cache or GraphQL change: `rolesPermissions` is keyed by role
id alone and the intersection is computed per request from cached
per-role entries.

## Not in this PR

`workflow-execution-context.service.ts` falls back to the **admin** role
when an application has no `defaultRoleId`, and to
`shouldBypassPermissionChecks: true` if admin is not found. That is the
inverse of the rule here and an escalation in its own right, but
workflow execution is sensitive, so it is tracked separately in
twentyhq/core-team-issues#2753.

Three resolvers still carry their own principal precedence and do not
use this seam: `rest-api-base.handler.ts`, `mcp-protocol.service.ts`
(which never builds a user or application context at all), and actor
attribution in `actor-from-auth-context.service.ts`.

Separately, `computeRecordGqlOperationFilter` silently discards
predicates under any parentless group after the first, with no test
coverage. That is a latent bug independent of this work and lives in
`twenty-shared`, shared with the front-end filter system.

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



<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23680?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``&lt;img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;``</a>
2026-08-03 17:01:21 +00:00
Thomas Trompette 9e2c870574 fix(workflow): clear nextStepIds when converting a step to If/Else (#23714)
## Context

Fixes #22947.

A workflow with If/Else branches could fail at runtime with `Step not
found` (and no detail in the Runs panel) because of dangling
`nextStepIds` in the workflow graph — references to steps that no longer
exist.

## Root cause

An If/Else routes only through `settings.input.branches[].nextStepIds`;
its top-level `nextStepIds` is never read by the executor and must stay
empty. But converting an existing step into an If/Else copied the
previous step's `nextStepIds` onto the new If/Else, leaving a stray
top-level reference. That reference is invisible to the executor, and
when steps around it are later deleted it becomes dangling and
propagates into a normal step's `nextStepIds`, which the executor then
tries to follow — `Step not found`.

## Fix

When a step's type is changed to If/Else, don't carry over the previous
step's `nextStepIds`. One change in
`workflow-version-step-update.workspace-service.ts`.

## Verification

Reproduced on a local instance via the editor's GraphQL mutations (build
`trigger → P → X → D`, convert X to If/Else, delete D then X):
- Before: converting X produced a stray `nextStepIds: [D]`, and after
the deletes P was left with a dangling reference.
- After: converting X yields `nextStepIds: []`, and P stays clean — no
dangling reference.
2026-08-03 16:17:02 +00:00
Thomas Trompette 62c1cf78af fix(server): scope personal favorite SSE events to their owner (#23712)
## Problem

Fixes #20483.

In a multi-user workspace, when a user creates/updates/deletes a
**personal favorite** (a `navigationMenuItem` with a non-null
`userWorkspaceId`), the metadata SSE event is broadcast workspace-wide.
Every other connected user receives it and the favorite pops into their
own sidebar in real time. Cross-user data-isolation leak.

## Root cause

The delivery filter in `WorkspaceEventBroadcaster` already supports
per-user scoping via `recipientUserWorkspaceIds`, but treats an
**undefined** list as workspace-wide (delivered to every stream).
`MetadataEventPublisher.publish` never set that field, so every favorite
event fell into the workspace-wide default. The `agentChatThread` path
already sets `recipientUserWorkspaceIds` explicitly and is scoped
correctly; favorites simply never opted in.

## Fix

In `MetadataEventPublisher`, resolve the owning `userWorkspaceId` for
`navigationMenuItem` events (from `properties.after` on create/update,
`properties.before` on delete) and set `recipientUserWorkspaceIds:
[userWorkspaceId]` when present. Workspace-level items (`userWorkspaceId
=== null`) leave it unset and keep broadcasting to everyone. This
mirrors the existing `agentChatThread` precedent and touches only the
producer, not the broadcaster or any consumer.

## Testing

Unit test (`metadata-event-publisher.spec.ts`) covers personal
create/update/delete (scoped to owner), workspace-level (unscoped), and
an unrelated metadata entity carrying a user id (unscoped).

Also verified end to end against a local multi-user workspace (Tim and
Jane, same workspace): each opened a live SSE stream (`/metadata`
`onEventSubscription`) and Tim created favorites.

| Case | Before fix | After fix |
|------|-----------|-----------|
| Personal favorite -> owner (Tim) | receives | receives |
| Personal favorite -> other user (Jane) | **receives (leak)** | not
received |
| Workspace-level favorite -> other user (Jane) | receives | receives |

- `nx typecheck twenty-server`: pass
- oxlint + oxfmt on changed files: clean

## Scope / follow-up

Favorites only. Two related items are intentionally out of scope and
worth tracking separately: scoping other user-owned metadata (`view` via
its visibility rules, `roleTarget`), and making the broadcaster's "no
recipient list = everyone" default explicit rather than fail-open.
2026-08-03 16:07:54 +00:00
nitin d359496b8b Keep chart palette colors stable when series order changes (#23638)
https://discord.com/channels/1130383047699738754/1522812783140540538

Chart palette colors were assigned by array position, so changing the
sort order (or any reordering of the data) reshuffled every series color
on line, bar and pie charts. Grouping by a select field was unaffected
since options carry their own colors — this only hit groupings without
intrinsic colors (relations, text fields, etc).

Colors are now assigned by the alphabetical rank of the series key, so a
key keeps its color no matter what order the data arrives in. Side
effect: existing palette-colored charts get a one-time color
reassignment.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23638?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-03 14:03:50 +00:00
github-actions[bot] b7724bbbee i18n - translations (#23713)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-08-03 16:01:05 +02:00
Raphaël Bosi 2389b4f807 Add captcha and throttling to the password reset link (#23372)
The public `emailPasswordResetLink` mutation was the only email-taking
auth mutation without `CaptchaGuard`, so bots could drive reset email
spam against arbitrary addresses.

- Adds `CaptchaGuard` and a `captchaToken` argument (no-op when no
captcha provider is configured). The frontend sends it like sign-in
does, and `/settings/profile` joins the captcha-protected paths so the
Change Password button keeps working
- Throttles reset emails per address, 3 per 15 minutes, and surfaces a
rate limit error once the bucket is empty
- Acknowledges the request as soon as the throttle passes and generates
the link off the request path, so the response time no longer depends on
whether the address is registered
- Returns a generic success instead of distinguishing found from
not-found, with matching frontend copy
- Rotates the reset token in a single transaction, so a failed write can
no longer revoke a still valid link

This does not close user enumeration on its own: `checkUserExists`
exposes `exists` on the same unauthenticated surface, and sign-in
returns distinguishable errors. Tracked in #23711.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23372?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-03 13:52:05 +00:00
neo773 96ea1e1ffc Track connected account webhook subscription lifecycle metrics (#23710)
Emits created/renewed/deleted counters and their failure counterparts
from the messaging and calendar webhook subscription services.

Each counter carries channel_type and provider attributes so the Grafana
panels can break them down. Infra side: twentyhq/twenty-infra#841

Co-authored-by: neo773 <huzef@twenty.com>
2026-08-03 13:51:17 +00:00
Priyanshu Bartwal 22d83c75e6 fix(twenty-front): Scrolling and Dragging conflict on mobile devices (#23677)
Fixes: #23675 



https://github.com/user-attachments/assets/1f0d4f0f-0dd6-4731-8359-6da13a5e11b3



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23677?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: Félix Malfait <felix.malfait@gmail.com>
2026-08-03 13:43:09 +00:00
Raphaël Bosi 4a5c623ece Improve the workspace setup kickoff prompt (#23594)
Rewrites the workspace setup chat kickoff prompt for conversion: the
goal is a real workspace the team keeps using, with the setup doubling
as a tour of what Twenty can do.

- Replaces the arbitrary bands (2-4 custom objects, 3-6 fields, 250
words) with admission tests, favoring custom fields on standard objects
over custom objects.
- Opens by sharing what we already know about the company and the user's
role, then lets them steer: propose a model right away, or hear their
use case first.
- After the data model there is no fixed script. The agent proposes the
single next capability worth building (workflow, dashboard, role) based
on what the user actually said, and names the ones it did not build
before closing so nothing stays hidden.
- Introduces each capability in one plain sentence where it comes up,
and drops the view-field step that #23585 made redundant.

Also passes the workspace member job title into the AI chat user
context, so the agent can shape the setup around what the user does.
This applies to every chat, not just onboarding.

Example: Creating an Apple workspace

<img width="2584" height="5022" alt="CleanShot 2026-08-03 at 15 33
48@2x"
src="https://github.com/user-attachments/assets/bc12ec95-e29d-42fd-8757-38ab3a8a5705"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23594?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-03 13:40:57 +00:00
Charles Bochet a9ca1eae95 ci(pr-review): dispatch on PR open (standard review only) (#23708)
## Why

A PR opened directly as non-draft (the normal member flow: push branch →
`gh pr create`) fires no dispatcher trigger — the initial commits
arrived before the PR existed, so they're an `opened` event, not
`synchronize`. With no `opened` trigger, such a PR gets **no review at
all** unless it's later pushed to or manually labelled. This is live
today: #23697 and #23707 are core-team PRs sitting with the bot's `-PR:
draft` label but zero "PR Review" status.

## Change

Add `opened` back to the dispatcher, and forward the triggering PR event
to the orchestrator:

```yaml
types: [opened, ready_for_review, synchronize, labeled]
# ...
-f pr_number="$PR_NUMBER" -f event="$EVENT"
```

The orchestrator (twentyhq/ci-privileged#65) maps **`opened` → standard
review only**; `security` + `triage` stay on pushes / ready-for-review.
So opening a PR gives core-team authors the standard (architectural)
review early, without firing the full gate on open, and the "opened and
never pushed again" hole is closed.

No author-role logic lives here — the dispatcher just forwards
`pr_number` + `event`; all who-gets-what policy is resolved in the
orchestrator.

## Merge order

Depends on **twentyhq/ci-privileged#65** (adds the `event` input). Merge
that first — it's backward-compatible (empty `event` = today's auto-gate
behaviour), so nothing breaks in between.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23708?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-03 15:05:02 +02:00
BOHEUS 495bd193a3 Messaging archived account fix (#23553)
Fix case where workspace admin wants to reconnect inherited connected
account

Case:
- workspace admin inherits team accounts from other workspace member who
left the workspace
- admin wants to reconnect inherited channels but it's not possible as
there's no path to make archived connected account active

Expected outcome: admin, who has credentials to archived connected
accounts, can reconnect said accounts

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23553?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 <huzef@twenty.com>
Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
2026-08-03 12:34:31 +00:00
Thomas Trompette e81fdbcc7a feat(workflow): variable pickers for Search Records limit, offset and date filters (#23696)
## Summary

<img width="491" height="390" alt="Capture d’écran 2026-08-03 à 11 30
40"
src="https://github.com/user-attachments/assets/6db59b8a-3e8e-41b8-80b3-c736e56b7f7f"
/>

Adds workflow variable pickers to the **Search Records** action for
fields that previously only accepted static values:

- **Limit** and **Offset** number inputs now expose the
`WorkflowVariablePicker`, so they can be bound to a variable from a
previous step. The stored value can be a standalone variable string; the
backend coerces the resolved value back to a number.
- **Date filters** using the `Is before` (`IS_BEFORE`) and `Is after or
equal` (`IS_AFTER`) operands now expose the variable picker in the
advanced filter side panel (previously disabled for all date filters).

The backend already resolves these inputs via `resolveInput`; the only
backend change is a small numeric coercion of the resolved limit/offset.

## Changes

- `WorkflowEditActionFindRecords.tsx` — pass `WorkflowVariablePicker` to
the Limit/Offset inputs; make `onChange` and form state variable-aware
(`number | string`).
- `AdvancedFilterSidePanelValueFormInput.tsx` — enable the date
`VariablePicker` only for `IS_BEFORE` / `IS_AFTER`.
- `useGetRecordFilterDisplayValue.ts` — return the raw variable for a
standalone `{{variable}}` value so date filters don't crash
`Temporal.*.from`.
- `find-records-action-settings-schema.ts` — allow a string (variable)
for `limit` / `offset`.
- `find-records.workflow-action.ts` — coerce resolved `limit` / `offset`
to numbers before querying.

## Testing

Built a workflow locally (Manual trigger → Code step returning `{ limit:
2, offset: 1, sinceDate }` → Search Records) with all three fields bound
to those variables. The run completed successfully; the Search Records
step returned exactly 2 records (limit applied) filtered by `createdAt
>= sinceDate`, confirming the backend resolves each variable.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23696?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-03 12:32:06 +00:00
Paul Rastoin 9e25121616 Fix ReconcileIndexViewUniversalIdentifier failing on occupied derived identifiers (#23647)
## Context

The 2.26 upgrade failed on 93 workspaces at
`ReconcileIndexViewUniversalIdentifier` with `duplicate key value
violates unique constraint "IDX_552aa6908966e980099b3e5ebf"`. The
colliding identifiers decode to standard objects' INDEX views (task in
91 of 93 cases, plus person, company, opportunity, messageThread).

## Root cause (verified against production data)

A fleet-wide scan of live INDEX views whose `applicationId` differs from
their object's `applicationId` returned one row per failing workspace,
matching the failure list one-to-one including every anomaly. The shape
is always the same: a **legacy caller-created view with `key: INDEX`**
(predating the flat view validator rejecting caller-provided INDEX
keys), **attributed to the workspace-custom application but sitting on a
standard object** (usually task), with a random v4 `universalIdentifier`
and `isSystemSideEffect: false` — coexisting with the object's real
INDEX view, which already holds the derived identifier. These were
minted by the old UI default-view bootstrap: views are attributed to the
caller's application context (workspace-custom) regardless of the object
they sit on. Both producer paths are closed by 2.26 itself (validator
rejects caller INDEX keys, side-effect engine provisions INDEX views at
object creation), so this cleans a closed wound.

The command selected candidates by the view's application (engine-owned)
but derived the identifier from the object's application, so it tried to
UPDATE the legacy view onto the exact identifier the real INDEX view
already holds, violating the unique index on `("workspaceId",
"universalIdentifier")`.

## Fix

The command stays updates-only (no inserts, no deletes), with one
decision per candidate view:

- **View's application differs from its object's application** → demote:
`key: null`, plus `isSystemSideEffect: false` if it was stamped as
system-owned. An INDEX view belongs to the application of its object;
the demoted row becomes a plain caller-owned view (same id, name,
fields, filters). This resolves the 93 failures.
- **View already holds its derived identifier** → at most flip
`isSystemSideEffect` to `true`.
- **Derived identifier free** → claim it. **Held by any other row** —
active or soft-deleted (the unique index is not partial on `deletedAt`,
flat maps are loaded `withDeleted`) — or claimed earlier in the same run
→ skip with a warning instead of crashing the workspace upgrade. Same
resolution for view fields.

## Deliberate non-goals

- **Demotion does not touch `universalIdentifier`**: releasing a held
derived identifier would let the backfill command insert a bare
duplicate INDEX view next to the user's customized one; the identifier
stays put and drifted holders are repaired at the data level instead.
- **A skipped view keeps its view fields untouched**: derived view-field
identifiers encode "field F on view D", so a view that cannot claim D
must not stamp its fields with D-derived identifiers (they belong to the
actual holder's field space). Fields converge only when their parent
view converges.
- **No tombstone deletion**: no production failure traced to a
soft-deleted holder; an occupied identifier is skipped, not
destructively freed.

## Release sequencing

The 2 workspaces failing at `DemoteAndBackfillApplicationIndexView`
(Sales, Synergentic) carry the same cross-attribution on objects of
installed applications, but their July reconcile run committed and
stamped the drifted views with the derived identifiers. **Before
re-running the upgrade**: repair their 5 drifted views by re-pointing
`view."applicationId"` at the object's application (SQL in the internal
runbook), then flush the workspace metadata cache (`cache:flush`) so the
commands don't read the stale attribution. After that, both commands are
no-ops there, and the 93 converge on the re-run.

## Test plan

- 7 new unit tests: cross-application demotion (external-app object,
system-flag reset on a stamped drifted view, and the production shape of
a workspace-custom view next to the standard INDEX view), skip on active
and on soft-deleted holders, first-claim-wins on same-object duplicates.
- All 14 pre-existing tests pass unchanged; lint and typecheck pass.
2026-08-03 12:23:15 +00:00
neo773 00ad1544d8 Classify OAuth refresh errors by reason instead of status code (#23705)
Both provider parsers treated unrecognised failures as permanent, so a
single transient error marked a working account as needing reconnection
and it never recovered on its own. Permanence is now decided by the
provider's OAuth error code, everything else is temporary and retries.

Checked against prod: 15 connected accounts currently flagged
auth-failed still return a valid token when refreshed, and 12 of those
were flagged in bursts across unrelated workspaces (five within 90
seconds on 2026-01-13), which points at a transient blip rather than
users revoking access.

---------

Co-authored-by: neo773 <huzef@twenty.com>
2026-08-03 11:43:00 +00:00
github-actions[bot] 37f1fe17ab i18n - docs translations (#23701)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-08-03 11:49:08 +02:00
github-actions[bot] 5fa54abd79 i18n - translations (#23700)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-08-03 11:47:29 +02:00
neo773 7c797ff0c9 Filter connected account webhook renewal to exclude accounts with failed authentication errors (#23694)
/closes TWENTY-SERVER-J1F

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23694?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 <huzef@twenty.com>
2026-08-03 09:43:54 +00:00
github-actions[bot] e2d71d2635 i18n - translations (#23698)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-08-03 11:40:00 +02:00
Thomas Trompette 9731921983 fix(server): catch application job enqueue throttle instead of failing the batch job (#23684)
## Context

Sentry issue
[TWENTY-SERVER-GXJ](https://twenty-v7.sentry.io/issues/7495161692?project=4507072499810304)
(`Application job enqueue limit reached`, 10k+ events, 33 workspaces) is
the application-job enqueue throttle firing as designed, but being
reported as an error.

## Root cause

`CallDatabaseEventTriggerJobsJob.handle` calls `throttleOrThrow` inside
its per-application loop with no `try/catch`. When an application
exceeds its enqueue budget, the `ThrottlerException` propagates out of
the bullmq job handler, where `shouldCaptureException` captures it (it
has no `statusCode < 500`, so it is not filtered) and the whole batch
job fails.

Two consequences:
- Expected throttling shows up in Sentry as an error (noise). A metric
(`JobEnqueueApplicationRateLimited`) already tracks it.
- The batch job fails and is retried (`retryLimit: 3`), re-running the
loop from the top and re-enqueuing logic-function jobs for applications
that already succeeded before the throttled one (duplicate triggers);
after retries are exhausted the remaining applications' triggers are
dropped.

The workflow hard-throttle uses the same `ThrottlerException` but does
not show up in Sentry because its call site (`checkHardThrottleLimit`)
catches it and turns it into a graceful signal. This PR applies the same
pattern to the application enqueue path.

## Change

- Wrap the `throttleOrThrow` call: on `ThrottlerException`, `continue`
to the next application instead of failing the job; rethrow anything
else.
- Add a unit test covering the skip-throttled-application and
rethrow-other-errors behavior.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23684?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-03 09:38:45 +00:00
Thomas des Francs 536b91c1dd Update record page layout card (#23654)
## Summary

- Redesign the data-model Layout card to match the record-page
customization design.
- Add dedicated light and dark cover assets and preserve the existing
customization workflow.
- Reuse a dedicated discovery-hero footer across the workspace and
record layout cards.
- Match the Figma cover, footer, icon, typography, and spacing metrics.

## Before/After

<img width="1588" height="720" alt="before-after"
src="https://github.com/user-attachments/assets/28be577c-9f1b-40f1-99e2-66eeafbac75b"
/>
2026-08-03 09:31:25 +00:00
Paul Rastoin 6afc3d33a4 fix: provision INDEX view fields for relations created in the same batch as their object (#23665)
## Context

Fixes twentyhq/core-team-issues#2749: when an app manifest creates an
object and its relation fields in a single sync, the engine-owned INDEX
view ended up with no viewField at all for RELATION / MORPH_RELATION
fields, not even a hidden one. Adding the same relation to a
pre-existing object in a second sync produced a visible viewField.

## Root cause

`fieldIndexViewFieldOnCreate` is the sole owner of caller-field view
fields on the INDEX view (`objectSystemFieldsAndIndexViewOnCreate` only
emits view fields for displayable system fields). Its same-batch branch
gated on `isFlatFieldMetadataDisplayableInDefaultView`, which excludes
RELATION / MORPH_RELATION, so relations were dropped and nothing else
picked them up.

The guard's other exclusions (reserved names like `id`/`deletedAt`,
system-only types TS_VECTOR / POSITION) are unreachable there: side
effect handlers only trigger on caller-authored entities (the engine
reads triggers from the pre-expansion matrix), and the flat field
validators reject those names/types for caller fields. The guard could
only ever drop relations.

## Fix

- Remove the displayability guard from
`buildViewFieldForObjectCreatedInSameBatch`.
- Remove the `displayableOnly` filter from
`computeCallerFlatFieldMetadatasForObject`: every caller field now gets
a view field, and both handlers keep deriving positions from the same
list, so the interleaved layout stays consistent by construction (label
identifier, caller fields in input order with relations, then
displayable system fields).
- Build the view field literal through a single
`buildIndexFlatViewFieldToCreate` helper on all handler branches instead
of `computeFlatViewFieldsToCreate`, whose internal displayability filter
would have dropped relations again. That util keeps its semantics for
its remaining callers (system-field view fields, object creation via
API, committed upgrade commands). Both identifier derivations are the
same deterministic uuid (asserted by an existing twenty-shared spec), so
emitted identifiers are unchanged.
- `isFlatFieldMetadataDisplayableInDefaultView` itself is untouched: the
committed 2-26 upgrade command and the system-field filtering still rely
on its current semantics.

## Tests

- New manifest-sync integration test (first commit, TDD red then green):
a single sync creating two objects and a MANY_TO_ONE / ONE_TO_MANY
relation pair asserts each object's INDEX view has a visible view field
for its relation, plus a control case adding the same relations to
pre-existing objects in a second sync.
- Unit spec: the test that locked in the noop now asserts a visible view
field at the expected position for RELATION and MORPH_RELATION.
- Verified locally: all 62 metadata-side-effect unit tests, the new
integration spec, `successful-sync-application-workspace-migration` (4
snapshots), `relabel-onto-new-field-manifest-sync`,
`create-one-field-metadata-relation`, plus twenty-server typecheck and
lint.
2026-08-03 09:23:12 +00:00
BOHEUS c0efc1d897 Docs update - Microsoft integration (#23671)
Based on
https://discord.com/channels/1130383047699738754/1526558939221856276/1532723206194991194

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23671?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-03 08:43:45 +00:00
Thomas Trompette ad8830ecbf fix(server): stop FIND_RECORDS from silently ignoring its filter (#23640)
## Problem

A FIND_RECORDS workflow step with a filter configured could silently
return every record (and thus the first row of the table) instead of
applying the filter. Ways to hit it:

- `recordFilters` set but `recordFilterGroups` omitted (e.g. an
API/agent caller, or any non-UI config).
- A grouped filter (carrying `recordFilterGroupId`) whose
`recordFilterGroups` is missing.
- A filter referencing an unknown `fieldMetadataId` or an unresolvable
relation (`turnRecordFilterIntoRecordGqlOperationFilter` returns
`undefined`, silently dropped).
- `gqlOperationFilter` set, which passed validation but was never read.

In every case the computed filter collapses to `{}`,
`FindRecordsService` returns all records ordered by `id ASC`, and
`records[0]` is the first row. This is fail-open: the step reports
success and returns wrong records rather than erroring.

## Fix

- Compute the filter whenever `recordFilters` is non-empty, defaulting
`recordFilterGroups` to `[]`. `computeRecordGqlOperationFilter` handles
ungrouped filters independently of groups.
- **Fail closed**: if `recordFilters` is non-empty but the computed
`gqlOperationFilter` is empty, throw `INVALID_STEP_INPUT` instead of
running an unfiltered query. This covers grouped-without-groups and
unknown-field/unresolvable-relation cases raised in review. An absent or
empty `recordFilters` still legitimately means "find all".
- Remove the unused `gqlOperationFilter` field from the find-records
input type and settings schema so it is no longer advertised as a filter
option (it has been dead since #16147, when the action moved to
computing the filter at runtime from `recordFilters`).
2026-08-03 08:24:44 +00:00
github-actions[bot] 9df893ead1 chore: sync AI model catalog from models.dev (#23682)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23682?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: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-08-03 09:01:12 +02:00
github-actions[bot] b754e15331 i18n - docs translations (#23670)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-08-01 18:48:55 +02:00
Paul Rastoin ebe816abb5 Remove outdated index view pitfall from app agent docs (#23667)
## Context

Index views are now auto-generated by a metadata side effect when an
object is created, so the "Common Pitfalls" entry warning against
creating an object without an associated index view is obsolete.

## What changed

Removed the line "Creating an object without an index view associated.
Unless this is a technical object, user will need to visualize it."
from:

- the `create-twenty-app` template
(`packages/create-twenty-app/src/constants/template/AGENTS.md`), used
for newly scaffolded apps
- the 14 existing copies across `packages/twenty-apps` (`AGENT.md` /
`AGENTS.md` / `CLAUDE.md` / `LLMS.md` in `examples`, `public`, and
`internal` apps)

The other pitfalls (navigationMenuItem, front-component scroll) are
unchanged since they still apply.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23667?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-01 15:04:30 +00:00
github-actions[bot] d77b1017aa chore: sync AI model catalog from models.dev (#23666)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23666?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: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-08-01 08:47:11 +02:00
github-actions[bot] 0773311fa8 i18n - docs translations (#23662)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-31 19:04:46 +02:00