Commit Graph

13730 Commits

Author SHA1 Message Date
github-actions[bot] ea5f908cb2 i18n - translations (#23085)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23085?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-07-20 19:08:21 +02:00
github-actions[bot] 92d6bcd8ac i18n - docs translations (#23083)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23083?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-07-20 19:00:53 +02:00
github-actions[bot] c32bc5e8ac i18n - translations (#23082)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-20 19:00:28 +02:00
Etienne 9abf76f5f2 fix(ai): show answered ask_questions as a card in chat history (#23075)
## Context

Feedback on the `ask_questions` (Ask AI) tool:
- When answering a select prompt with a free-form message, the answer
wasn't surfaced as expected in the conversation history.
- The selected value also looked dropped once picked.

Root cause: answered `ask_questions` parts were caught by the
thinking-steps grouping and rendered as a generic collapsible "Ran
ask_questions" tool step (JSON output), so the dedicated renderer was
never reached.

## Changes

- **Render answered questions as a card**
(`AiChatQuestionStatusRenderer`): an "Answers" card that shows each full
question with the chosen option label(s) or the free-text answer beneath
it, instead of the faint inline `header: value` line.
- **Free-text keyboard navigation** (`AiChatQuestionCard`): pressing
Enter in the free-text area now advances to the next question, or
submits when on the last question (mirroring the option-select flow).
Shift+Enter still inserts a newline.

## Notes

- No schema/GraphQL changes; display + interaction only.
- Existing `thinkingStepsDisplayState` grouping test is unaffected (only
`web_search`/`create_task`/`code_interpreter` are used there).


<img width="421" height="301" alt="Screenshot 2026-07-20 at 17 48 04"
src="https://github.com/user-attachments/assets/3845055d-7061-40c0-b263-aa1c670329cd"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23075?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. -->


fixes
https://discord.com/channels/1130383047699738754/1526871110300209282
2026-07-20 18:53:51 +02:00
Thomas Trompette 05132d262b fix(workflow): show account select in Send Email node when no account is connected (#23066)
# Why

In the workflow **Send Email** (and Draft Email) node, when the
workspace has no eligible connected account, the **Account** field
disappears entirely — only the variable picker icon remains. The "Add
account" call-to-action is unreachable, so there is no way to connect an
account from the node.

## Root cause

Regression from #21075. `FormSelectFieldInput` used to always pass a
default empty option ("No Account") to `<Select>`; since #21075 it only
prepends it when `isNullable` is set, and the email account field
doesn't set it. With zero connected accounts, `<Select>` then has no
option to resolve a selected option from and bails out rendering an
empty fragment — even though a `callToActionButton` is configured:

```tsx
// Select.tsx
if (!isDefined(controlSelectedOption)) {
  return <></>;
}
```

# What changed

`FormSelectFieldInput` now prepends the empty option whenever the field
is nullable **or there are no options at all**. A populated non-nullable
select still offers no clearing choice (the #21075 behavior is
preserved); an empty one renders its "No X" state so the control — and
its call-to-action — stay visible and clickable.

# Test plan

- New story `FormSelectFieldInput > NoOptionsWithCallToAction`: zero
options + CTA renders the "No Work Policy" control, the dropdown opens,
and the CTA is clickable.
- New story `WorkflowEditActionEmailBase > NoConnectedAccounts`: with
`MyConnectedAccounts` mocked to `[]`, the Account field renders "No
Account" instead of vanishing. The meta's msw handlers move to the
keyed-object form so the story can override a single query (story-level
handler arrays get concatenated after the meta's, and msw's first match
wins), and the story evicts the module-singleton Apollo client's cached
accounts so it actually hits the empty mock.
- `npx nx typecheck twenty-front` and `npx nx lint:diff-with-main
twenty-front` pass; both story files pass under the storybook vitest
project (13 stories total).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23066?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-07-20 16:53:29 +00:00
Paul Rastoin 1be5a0e54a System side effect relations (#22882)
Closes twentyhq/core-team-issues#2667

## What

Default relations to the standard relation objects
(`timelineActivities`, `attachments`, `noteTargets`, `taskTargets`) are
now fully owned by the **metadata side-effect engine**. Neither the API
transpilers nor the SDK manifest builder provision them anymore: any
object creation, rename or deletion — regardless of the caller — goes
through the same engine handlers.

## Why

- Provisioning was duplicated across the API path and the SDK manifest
builder, with diverging behavior.
- Universal identifiers of relation fields were derived from object
**names**, so renaming an object mutated them and forced lossy
delete+create cycles on manifest sync.

## How

### Engine-owned lifecycle (side-effect handlers)

- `objectSystemRelationsOnCreate`: provisions the 8 forward/reverse
relation fields (+ join column indexes) when an object is created.
- `objectSystemRelationsOnUpdate`: renames the reverse morph fields
(`target<ObjectName>`) when their host object is renamed — a lossless
`fieldMetadata.update`.
- `objectSystemSideEffectsOnDelete`: cascades deletion of engine-owned
fields/indexes when the object is deleted.
- The API transpilers and the SDK `buildManifest` no longer inject these
fields; `isSystemSideEffect: true` marks engine-owned entities, guarded
by a granular property allowlist (only `isActive` is user-editable) and
excluded from manifest deletion inference.

### Name-free deterministic universal identifiers

New `getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier, objectUniversalIdentifier,
relationTargetObjectUniversalIdentifier })` in `twenty-shared`, exported
from `twenty-sdk/define`. The identifier is keyed on the two **object**
identifiers instead of field names (direction encoded by argument
order), so object renames never mutate relation field identifiers. It
cannot collide with the name-based `getFieldUniversalIdentifier`
derivation (field names cannot contain `:`).

### twenty-standard re-owned

All 48 forward/reverse system relation field declarations in
`STANDARD_OBJECTS` now pin the derived name-free identifiers (computed
inline via the shared util) and carry `isSystemSideEffect: true`, with
labels/icons declared explicitly (translated via `msg`).
`twenty-standard` is projected as if the engine had generated these
fields itself.

### 2.23 upgrade commands

- `reconcile-system-relation-field-universal-identifier`: structurally
matches existing default relation fields per workspace and backfills the
derived universal identifiers, `isSystemSideEffect` flags, and standard
labels/icons.
- `upgrade-people-data-labs-application`: upgrades installed PDL apps to
`1.0.7` right after the backfill to close the desync window (its views
reference the re-derived identifiers).

### Misc

- `people-data-labs` `1.0.7`: views temporarily pin the new derived
identifiers (TODO: import from the next released `twenty-sdk`).
- `UpgradeStatusModule` split out of `UpgradeModule` so the application
module cluster can consume upgrade status/migration services without
importing the versioned command bundles (fixes a require cycle that
crashed boot).
- Docs: `system-fields.mdx` documents the system relation fields and
their resolver; `sync-and-recovery.mdx` plan example no longer shows
auto-injected relations.

## Known red CI

`people-data-labs (dockerhub-latest)` fails by design until the 2.23
server image is published: the app pins the new identifiers which only
exist on a 2.23 server. The `local` leg (server built from this branch)
is green.

## System fields are no longer manifest-authorable (accepted regression)

The manifest converter no longer derives `isSystem` /
`isSystemSideEffect` from field names. Reserved-system-named manifest
fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`,
`updatedBy`, `position`, `searchVector`) are now skipped at conversion
time when they carry the exact derived universal identifier (keeps
manifests built with older SDKs installable), and rejected with
`INVALID_INPUT` when they pin any other identifier. System fields are
therefore fully engine-canonical: nothing a manifest carries can produce
a system-flagged entity anymore.

**Accepted regression**: a manifest can no longer influence system field
properties at all. Previously a (legacy) re-declaration could shape them
at creation — which actually produced broken system fields, e.g. a
nullable, non-unique `id` — and could still toggle the allowlisted
`isActive` / `universalSettings` afterwards. We consider this acceptable
for now: per-app granularity over system fields will be reintroduced
later through the **override framework**, which will also settle update
semantics by forbidding direct updates over `isSystemSideEffect: true`
entities and expressing divergence as overrides.

`isSystemSideEffect`-only entities (the default relation fields
provisioned by this PR) still have no engine-level update guard (see
Follow-up below); that part is unchanged and also lands with the
overrides refactor.

## Follow-up

`isSystemSideEffect` field update/delete guards intentionally live at
the API layer (`sanitize-raw-update-field-input.ts`,
`from-delete-field-input-...util.ts`) rather than in the engine-level
`FlatFieldMetadataValidatorService`. Moving them into the validator
requires threading operation-origin (direct field mutation vs engine
cascade) through the migration matrix, otherwise legitimate object
rename/delete cascades (which carry `isSystemBuild=false`) would be
rejected. Tracked in twentyhq/core-team-issues#2671.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22882?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-07-20 18:53:24 +02:00
Raphaël Bosi eb651180aa Widen the front component event allow-list (#22616)
Front components are third-party UI that runs in a sandboxed worker, so
every DOM event reaching them has to be on an explicit allow-list. That
list was small: mostly click, focus and pointer events.

This adds touch, drag and drop, focusin/focusout,
animationend/transitionend and scrollend, plus load/error on `<img>` and
toggle on `<details>`/`<dialog>`.

Two of them need the host to do more than forward the event:

- react-dom has no `onFocusIn`/`onFocusOut` props, so the host attaches
those two with `addEventListener` instead.
- a browser only fires `drop` on an element whose `dragover` default was
prevented, and the component's own `preventDefault` arrives too late
across the async worker boundary. The host prevents it synchronously as
soon as the component declares either handler.

Touch events carry their coordinates on `changedTouches`, so the first
touch fills the existing coordinate fields.

Still not crossing, since each would need a new serialized field: touch
lists, `animationName`/`propertyName`/`elapsedTime`, toggle `newState`
and `dataTransfer`.

The diff also renames a few things it touches (`filterProps` and
`EventToReact` in particular) so the host-side event path reads in
order.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22616?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-07-20 18:24:21 +02:00
github-actions[bot] 4ebdecfdf0 i18n - translations (#23077)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-20 18:08:51 +02:00
Gautam pandit 3e14dcbb05 Fix record board column header action accessibility (#22496)
## Summary
- Keep Record Board column header actions mounted instead of rendering
them only on mouse hover
- Show actions on hover and focus-within so keyboard users can reach
them
- Avoid header layout shifts when actions appear

## Context
This is a small follow-up found while reviewing #22323. It does not
duplicate the Kanban column drag-and-drop implementation.

## Testing
- git diff --check
- Not run: package lint/typecheck because this checkout still has no
node_modules and Yarn is not available on PATH

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22496?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: bosiraphael <raphael.bosi@gmail.com>
2026-07-20 16:00:31 +00:00
github-actions[bot] 8a35f78d70 i18n - translations (#23076)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-20 17:51:00 +02:00
martmull 8d84a0b9f3 feat(app): allow non-admin developers to claim and list marketplace apps (#22621)
## Context

Follow-up to #22609. Lets a non-admin developer claim ownership of a
public Twenty app they published to npm, then request a marketplace
listing that a server admin reviews. Marketplace state is per-instance
for now.

## Claiming

- Developer tab gets a **Claim an application** section: look up an
unclaimed npm app by package name or universal identifier.
- Ownership is proven with GitHub OAuth against the package's npm
provenance (trusted publishing): the connected account must own the
GitHub account or organization the package was published from.
- Errors from the GitHub callback come back as a code and are shown
inline with a link to the relevant documentation.
- The old one-click claim stays admin-only.
- A **Sync catalog** button triggers a catalog refresh instead of
waiting for the hourly cron.
- Gated behind the `IS_APP_CLAIMING_ENABLED` feature flag.

## Listing requests

- Catalog-synced apps are created **unlisted**; a data migration unlists
previously auto-listed unclaimed npm apps (owned or vetted rows are left
untouched).
- Owners request a listing from the Distribution tab (logo + description
required); a server admin approves or rejects it from a **Listing
requests** section in the Admin Panel.

## Screenshots

<img width="1512" height="829" alt="image"
src="https://github.com/user-attachments/assets/788d4362-97c4-4e42-810c-ef1f11517bec"/>
<img width="1512" height="829" alt="image"
src="https://github.com/user-attachments/assets/d6246190-c82a-4f64-87be-3bb668527645"/>
<img width="1512" height="828" alt="image"
src="https://github.com/user-attachments/assets/21a8dad4-610b-4d1f-8948-b9acab40d373"/>
<img width="1512" height="829" alt="image"
src="https://github.com/user-attachments/assets/58246130-41f7-451e-ae7f-57bd21d04bb6"/>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-07-20 15:43:40 +00:00
martmull 1b5e974629 feat(call-recorder): add copy-to-clipboard buttons for transcript, summary, and video link (#23052)
## Context

Closes twentyhq/core-team-issues#2692.

Adds copy-to-clipboard actions to the call recorder app so users can
quickly share a call's transcript, summary, and video.

## What changed

- **Copy transcript** button in the *Recording and Transcript* widget
header. Copies the transcript as plain text with resolved speaker
display names and timestamps (mirroring what is shown on screen).
- **Copy video download link** button in the same header. Copies the
signed video file URL.
- **Copy summary** button in the *Summary* widget header. Copies the
summary markdown.

Each button is powered by a new reusable `CopyToClipboardButton`
component that writes to the clipboard, briefly swaps to a check icon
for feedback, and surfaces a success/error snackbar. Buttons are
disabled when there is nothing to copy (no transcript / video / summary,
or while loading).

A `buildTranscriptPlainText` utility turns parsed transcript entries
into shareable text, with participant display names preferred over raw
diarized speaker labels.

## Screenshots

The *Recording and Transcript* header now shows a copy-transcript and a
copy-video-link button, and the *Summary* header shows a copy-summary
button.

| Light | Dark |
| --- | --- |
| <img width="426"
src="https://raw.githubusercontent.com/twentyhq/twenty/claude/issue-2692-screenshots/.github/pr-screenshots/2692/call-recorder-copy-buttons-light.png"
/> | <img width="426"
src="https://raw.githubusercontent.com/twentyhq/twenty/claude/issue-2692-screenshots/.github/pr-screenshots/2692/call-recorder-copy-buttons-dark.png"
/> |

## Tests

- New unit tests for `buildTranscriptPlainText` (speaker/timestamp
formatting, missing timestamps, participant name resolution).
- Full app unit suite passes (491 tests), plus typecheck and lint.
2026-07-20 15:40:17 +00:00
github-actions[bot] f86820552c i18n - docs translations (#23074)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23074?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-07-20 17:21:35 +02:00
Raphaël Bosi 240e185323 Show connect emails step to invited users without granting credits (#23058)
Invited users never saw the connect-emails onboarding step, so they
could not connect their inbox while onboarding. Now they do, but only
the first user (the workspace creator) earns the import-contacts reward
for it.

The credit is gated on the workspace having a single member, reusing the
same "first user" signal the frontend already uses to gate the
invite-team step. The connect-account step is still claimed for everyone
so invited users' onboarding advances normally.

The frontend hides the "free credits" tag and the header counter bump
for invited users, so we do not promise credits they will not receive.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23058?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-07-20 17:00:39 +02:00
martmull 45b319ef2d Add autofocus to 2FA OTP inputs (#23067)
as title
<img width="854" height="533" alt="image"
src="https://github.com/user-attachments/assets/777c2d83-8318-4337-865d-67aebc9186c2"
/>
2026-07-20 14:29:19 +00:00
github-actions[bot] fd5afd00de i18n - translations (#23068)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-20 16:21:07 +02:00
martmull baa84bb2e0 Add auto-upgrade-in-apps (#23001)
We need to auto upgrade application, lets add this column in application
entity, and add an admin button to autoupgrade all applications to
latest app registrration version manually

<img width="1131" height="372" alt="image"
src="https://github.com/user-attachments/assets/4e755abc-38ad-4895-a2e8-d55ee1948ac2"
/>

<img width="906" height="533" alt="image"
src="https://github.com/user-attachments/assets/ce002057-0341-4581-bf9a-66ac2bd84a9b"
/>
2026-07-20 16:12:44 +02:00
Etienne 5bf3472eb9 chore(twenty-exa): bump to 0.2.0, add marketplace metadata and Twenty version floor (#23063)
## What

Prepares the Exa app (`@twentyhq/twenty-exa`) for a fresh npm release.

- Bump `version` `0.1.0` → `0.2.0`
- Add `engines.twenty: ">=2.19.0"` so older servers don't install an
incompatible build
- Add marketplace metadata in `defineApplication()`: `category:
'Search'`, `websiteUrl`, `termsUrl`, `emailSupport`, `issueReportUrl`
(matching the values used by the other `@twentyhq/*` apps)

## Why

The version currently published on npm is the **unscoped**
`twenty-exa@0.1.0`, which predates several SDK breaking changes. The
in-repo source has since migrated to `twenty-sdk@~2.16` and `exa-js` v2:

- `chargeCredits` now imported from `twenty-sdk/billing` (was a local
util)
- logic function uses `toolTriggerSettings.inputSchema` (was `isTool` +
`toolInputSchema`)
- schema type imported from `twenty-sdk/logic-function` (was
`twenty-shared/logic-function`)
- `category` enum updated to the exa-js v2 union (removed
`github`/`tweet`/`linkedin profile`, added `people`)

So the published build is effectively broken on current servers. This PR
readies a `0.2.0` release under the standard scoped name
`@twentyhq/twenty-exa`.

The app's `universalIdentifier` is unchanged
(`2b7f4a2e-9c4b-4a11-b63c-2e5e7d3f5a9a`), so Twenty treats this as the
**same app** and upgrades existing installs in place — the name change
(unscoped → scoped) is only an npm-registry concern.

## Changes

- `packages/twenty-apps/public/twenty-exa/package.json`
- `packages/twenty-apps/public/twenty-exa/src/application.config.ts`

## Testing

- `yarn typecheck` — pass
- `yarn lint` — pass (0 errors)
- `yarn twenty dev:build` — builds a valid `@twentyhq/twenty-exa@0.2.0`
tarball

## Follow-up (not in this PR — npm/ops, needs auth)

- Publish `@twentyhq/twenty-exa@0.2.0` to npm (`yarn twenty
app:publish`)
- Deprecate + de-keyword the old unscoped `twenty-exa` so only one
package feeds the shared `universalIdentifier` on catalog sync

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23063?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-07-20 13:49:39 +00:00
Raphaël Bosi dd57842187 Show the onboarding welcome animation on sign-up only (#23057)
The welcome overlay replayed for existing users signing in, because it
had no signal for "this user just signed up" and inferred it from a
coincidence: a COMPLETED user standing on an onboarding URL while being
redirected away.

This drops that inference and triggers explicitly at the only two places
onboarding reaches COMPLETED: `useSetNextOnboardingStatus` and the
Stripe return.
2026-07-20 13:42:48 +00:00
Félix Malfait d4ac6e752b fix(server): stop cross-pod recompute cascade on localDataOnly workspace cache keys (#22980)
## Context

Prod investigation (Sentry, last 7 days) traced the current slowness to
the per-pod workspace cache. Every recompute of a `localDataOnly` key
(`ORMEntityMetadatas`, `flatWorkspaceMemberMaps`) published a fresh
`crypto.randomUUID()` as the shared Redis validation hash. Because these
keys recover from a hash mismatch by recomputing (their data never
enters Redis), one miss on one pod invalidated the local copy on every
other pod; each of their recomputes minted yet another hash,
re-invalidating everyone else. The fleet never converges.

Measured impact in prod:
- The `ORMEntityMetadatas` rebuild (full `objectMetadata` +
`fieldMetadata` + `application` queries, ~220ms combined, plus
`EntityMetadataBuilder.build`) ran **~963k times in 24h** (~11/s),
roughly 58h of cumulative Postgres time per day.
- The hottest single workspace recomputed its schema metadata 51k
times/day (once per 1.7s).
- Second-order effects: `POST /metadata` averaged 26.6s (p95 2.3s, so a
tail hangs for minutes on pool/event-loop starvation), GraphQL p95 went
846ms (v2.20.0) to 1744ms (v2.21.0), `Query read timeout` on trivial
cron queries at 18x baseline.

The random hash was correct in the original design (#15962): it is a
generation token, and Redis-backed keys recover absorptively by adopting
hash+data from Redis. #16287 added `localOnly` keys (EntityMetadata[] is
not serializable) whose recovery is generative, which silently broke the
invariant later documented in #18649 ("hashes change only on
invalidateAndRecompute").

## What this does

- Recovery recomputes now **adopt** the hash already present in Redis
instead of minting a new one, and write nothing back. A miss costs one
recompute on one pod instead of an unbounded fleet-wide loop.
- Minting is reserved for `invalidateAndRecompute` (real metadata
changes, propagation semantics unchanged, including the frontend
collectionHashes contract) and the bootstrap case where Redis has no
hash.
- The bootstrap write uses **SET NX** (new
`CacheStorageService.setIfAbsent`) instead of a plain overwrite: a slow
bootstrap recompute could otherwise land after a concurrent
`invalidateAndRecompute` mint and clobber it with a hash of
pre-migration data. Under the old code that clobber self-healed via the
cascade; with adopt semantics it would pin stale data, so the bootstrap
write must lose that race. A losing pod keeps its result locally as
provisional and converges on the winning hash at its next revalidation
(covered by a dedicated race test).

Redis-backed keys are untouched: same fetch-on-mismatch recovery, same
mint-and-write on `missingInRedis`.

## Expected effect and how to verify

`FieldMetadataEntity`/`ObjectMetadataEntity`/`ApplicationEntity`
full-workspace query counts in Sentry should collapse from ~1M/day to
the true metadata-change rate, and with them the DB pool pressure behind
the `/metadata` latency tail. This also makes local-cache eviction
(`MAX_LOCAL_CACHE_ENTRIES`, #22946) cheap: the cap can be tuned purely
for RAM.

Complementary to, not competing with, the planned Redis pub/sub
invalidation: a version token in Redis is still needed for restart
catch-up, and this PR gives it sound semantics.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22980?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-07-20 15:26:49 +02:00
Rashad Karanouh e6c6cccafa v1.3.0 — Partner workspace self-service (glowup app) (#22929)
## Glowup — app · v1.3.0 (Release ② of the brief + glowup rollout)

Partner **workspace self-service**: partners manage their own profile,
links, services, and case studies from inside the CRM (new objects +
record-page views + a "My Profile" self-service front-component).
Evolved superset of the closed #22470 (v1.3.0). App-only — **0 website
files**.

Version **1.3.0** (prod is currently 1.2.10). SDK **2.19.0**.

Supersedes **#22470** (closed).

### Verified locally
Provisioned a throwaway workspace, synced the schema, seeded, and
exercised the full surface end-to-end: marketplace + public profiles
render live; **partner self-service pages** (My Profile / My Case
Studies / links / services) load and save when acting as a partner user;
both intake forms (partner application + client brief) submit
successfully. `oxlint` 0/0, typecheck clean.

### Notes
- Committed `APPLICATION_UNIVERSAL_IDENTIFIER` is the **canonical** prod
id `e662fc1f-02c1-41ff-b8ba-c95a447b3965` (local bundle rewrites it to a
throwaway that stays uncommitted).
- New views reference app-owned fields only — no hardcoded system-field
ids.

### Remaining before merge
- CI lint / typecheck / tests (green locally).
- Refresh the partners-doc (new objects/views change the app surface).

---

## 🚦 Release order — do not break

```
① BRIEF WEB — #22291   MERGED (website deploy pending prod CLIENT_BRIEF_* env vars)
        │
        ▼
② GLOWUP APP — THIS PR  (rk-partner-profile-page v1.3.0 → main)   ⟵ replaces #22470
   merge → DEPLOY TO PROD  (verify canonical id first,
                            yarn twenty deploy && install -r partner-twenty-com)
         → set new app variables on prod → refresh partners-doc
        │   ⟵⟵ GATE for ③ ⟵⟵
        ▼
③ GLOWUP WEB — rk-glowup-web-stacked  (reopen ONE PR, base main; was #22471 / #22402)
   ONLY after ② is LIVE on prod (the site reads the new links / services / case-study objects)
```

- ② gates only ③. After ② deploys, reconcile **#22637**
(partners-traffic-web) with ③ — both touch `partners-marketplace/*`.
2026-07-20 15:25:07 +02:00
Paul Rastoin 6b55a6b51c Fix duplicate searchFieldMetadata inserts in the 2.16 backfill upgrade command (#23060)
## Context

A self-hosted instance upgrading from 2.0.3 to v2.22.0 got stuck with
one workspace failing at `2.16.0_BackfillSearchFieldMetadataCommand`:

```
[QueryFailedError] duplicate key value violates unique constraint "IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE"
Detail: Key ("objectMetadataId", "fieldMetadataId")=(...) already exists.
```

The failure happened on a retry after a previous partial run, and
reproduced even though the command already recomputes
`flatSearchFieldMetadataMaps` before deriving the create-set (#22884).

## Root cause

The idempotency dedupe compares `(objectMetadataId, fieldMetadataId)`
pairs across two differently-fresh caches:

- The **existing rows** side comes from `flatSearchFieldMetadataMaps`,
which is recomputed from the database (real current ids).
- The **candidate** side resolves ids through `flatObjectMetadataMaps` /
`flatFieldMetadataMaps`, which are **not** invalidated. During a
cross-version upgrade these can be stale, since the migration runner
only invalidates the cache keys a migration touched.

When a stale map resolves a candidate to an outdated id, the dedupe key
doesn't match the existing row and the row is re-emitted. The migration
runner then re-resolves the universal identifiers against fresh maps at
execution time and inserts with the real current ids — exactly the pair
already committed by the earlier partial run (each per-application
migration commits independently) — tripping the unique constraint and
failing the upgrade.

## Fix

Two independent layers, either of which would have prevented the
failure:

1. **Consistent snapshot for the build phase**: the command now
invalidates and recomputes all three maps the dedupe depends on
(`flatObjectMetadataMaps`, `flatFieldMetadataMaps`,
`flatSearchFieldMetadataMaps`), so candidate resolution, existing-row
keys, and the runner all see the same database state.
2. **Id-churn-proof dedupe**: every row this command creates carries a
deterministic universal identifier (`getSearchFieldUniversalIdentifier`,
derived from application + field universal identifiers, no database ids
involved) and `(workspaceId, universalIdentifier)` is unique. The build
util now also skips any candidate whose deterministic universal
identifier already exists, catching leftovers from a previous partial
run even if objects/fields were recreated under new ids in between.

Deliberately **not** done: `ON CONFLICT DO NOTHING` in the create action
handler — it is shared by all runtime `searchFieldMetadata` creation,
and swallowing a conflict would leave the flat-entity cache holding an
entity id that differs from the row actually in the database.

## Test

Added a regression test reproducing the failure shape: an existing row
with the same deterministic universal identifier but stale metadata ids
must not be re-emitted by the backfill.

Note: `ReconcileSearchFieldMetadataCommand` (2.20) has the same
stale-cache exposure; hardening it is left to a follow-up.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23060?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-07-20 13:23:26 +00:00
Thomas Trompette 98c71d7b3d fix(server): recover workflow runs whose queue job was lost - monitoring only (#22995)
## Context

A workflow run can get permanently stuck in RUNNING when the queue job
executing a step dies without failing (worker crash/restart, lost BullMQ
job). The step stays `RUNNING` in the persisted state, nothing ever
recomputes the run status, and the run never terminates. If a user
clicks Stop, it wedges in STOPPING instead (the stuck-STOPPING sweeper
from #22900 then catches it after 1h, but only because of the manual
stop). Self-hosters also have no way to tell that a job was lost, or
when.

## What this PR does

### Detect runs stuck in RUNNING (monitoring only, no finalization yet)

New `handleStuckRunningRunsForWorkspace` in the staled-runs sweeper
(same cron/job/CLI wiring as the stuck-STOPPING recovery):

- Targets RUNNING runs with `updatedAt` older than 1h (`updatedAt`
refreshes on every step-info write, so staleness means zero progress).
- Skips any run that still has a job in the queue: new `getInFlightJobs`
on the message queue driver
(active/waiting/waiting-children/paused/prioritized/delayed), matched by
run-id-prefixed job id with a `job.data.workflowRunId` fallback for jobs
enqueued before this deploys.
- A truly orphaned run (orphaned RUNNING step, lost between two steps,
failed branch, or finished-but-never-finalized) is **flagged, not
finalized**: warn log + `WorkflowRunStuckRunningDetected` metric + entry
in a per-workspace cache.
- On every subsequent sweep, flagged runs are re-checked. One that ended
or got a new queue job on its own is recorded as
`WorkflowRunStuckRunningFalsePositive` (warn log with the status it
reached) and unflagged. The cron keeps sweeping a workspace as long as
it has flagged runs.

This validates the detection before it is allowed to act: if flagged
runs never resolve on their own (no false positives) while `Detected`
counts real incidents, a follow-up PR can turn the flag into an actual
finalization (fail with a clear "job lost" error so Retry works). Runs
waiting on PENDING steps (delay, form) are never flagged.

### Make queue jobs traceable to their run

All RunWorkflowJob dispatches now set the job id prefix to the workflow
run id, so BullMQ job ids become `<workflowRunId>-<uuid>`. Worker logs
(`Processing job <id>` / `processed`) and Redis job keys are now
greppable by run id. A new opt-in `allowDuplicatedPrefixes` queue option
bypasses the one-waiting-job-per-id dedup (which would otherwise drop
parallel-branch continuations); existing `id` users keep dedup by
default.

### Observability

- `stalled` worker event listener: warn log + new `JobStalled` metric —
emitted when BullMQ detects a job whose worker stopped renewing its lock
(i.e. died mid-job).
- `WorkflowRunStuckRunningDetected` /
`WorkflowRunStuckRunningFalsePositive` metrics as described above.

## Out of scope (follow-ups)

- Actually finalizing flagged runs once monitoring shows no false
positives.
- Recovering lost *delayed* resume jobs (PENDING delay step whose
scheduled job vanished).
- Persisting job ids on the run entity — unnecessary given derived ids.

## Testing

- 11 unit tests for the monitoring sweeper (flagging, id-prefix + data
fallback in-flight guards, pending skip, failed-branch precedence,
false-positive tracking, still-stuck retention, error isolation,
never-finalizes) plus find-options specs; 613 tests pass across workflow
and message-queue modules.
- Not covered: end-to-end kill-the-worker scenario against a real queue.
2026-07-20 14:21:33 +02:00
Thomas Trompette 87729a2822 feat(workflow): backfill + dual-write for core workflow entity (#22776)
Workflow-side soft-ref sync — the `coreWorkflowId` mirror of the merged
version side (#22821 / #22940 / #22944 / #22961). Rebased onto current
main; supersedes the original shared-UUID version of this PR.

## What
Gives `core.workflow` a per-workspace copy of each workflow (`name`,
`lastPublishedVersionId`), soft-reffed from the workspace record via
`coreWorkflowId`, so app-shipped workflows have a core home. Does not
touch reads/dispatch (that's Phase B).

- **Sync service** (`WorkflowCoreSyncService`): core rows get their own
id (`uuidv5(workspaceId:recordId)`), the workspace record links via
`coreWorkflowId` (written back after the upsert), and the write-back is
**guarded** on the `coreWorkflowId` field being present (skips with a
warning otherwise — mirrors #22940). Injected repo renamed
`coreWorkflowRepository`.
- **Dual-write listener** on the `workflow` object:
CREATED/UPDATED/RESTORED upsert, DELETED/DESTROYED delete by
`coreWorkflowId`. Always-on; failures routed to Sentry so they never
break the user write.
- **2-20 backfill** (`backfill-workflow-to-core`): reads via the
provided `RunOnWorkspaceArgs.dataSource`, upserts each workspace
workflow into core.
- **2-22 provisioning** (mirrors #22944/#22961):
- `add-workflow-core-soft-ref-field`: adds the `coreWorkflowId` system
field on existing workspaces (flat-entity legacy migration).
- `backfill-workflow-core-links`: full rebuild — per workspace, in one
raw-SQL transaction, wipes all `core.workflow` rows, inserts a fresh
own-id row per workflow, and re-links every record. (No trigger-map
cache to invalidate on `core.workflow`.)

Simpler than the version side: `core.workflow` has no
one-active-per-workflow index and no trigger-map cache, and it was never
backfilled in prod, so there are no legacy shared-id rows.

## Test
Fresh `database:reset` + full sequence (2-20 backfill → 2-22 add-field →
2-22 rebuild link): 4/4 workspace records linked via `coreWorkflowId`
(id != coreWorkflowId), links resolve, **0 dangling**, no duplicate core
rows, names populated. Typecheck + lint + oxfmt clean.
2026-07-20 14:04:32 +02:00
neo773 c04714b9e0 fix(messaging): register and harden webhook subscription renewal cron (#23006)
The renewal cron was never wired into cron:register:all, so
Gmail/Calendar/Graph watches were never renewed and went dark ~7 days
after connect (the max watch lifetime all three providers allow).
Register it, fan renewals out as per-channel queue jobs scoped to active
workspaces, retry FAILED channels, and recreate Google Calendar watches
before stopping the old one.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23006?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-07-20 17:26:47 +05:30
martmull fa720358d9 Ignore call-recorder bots for unsupported meeting platforms (#23050)
## What

The call-recorder scheduled a Recall bot for any calendar event that had
a conference link, even when the link pointed to a platform Recall
cannot join (e.g. ro.am, Daily, Whereby, or a plain dial-in). Those
requests could never produce a recording.

This adds a supported-platform check to the recording policy so
unsupported links are ignored, with a dedicated reason, and documents
the supported platforms in the app README.

## Changes

- Add `SUPPORTED_MEETING_PLATFORM_URL_PATTERNS` constant (Zoom, Google
Meet, Microsoft Teams, Webex, GoTo Meeting), extracted from the existing
link-extraction patterns so extraction and validation share one source
of truth.
- Add `isSupportedMeetingPlatformUrl` util.
- `resolveCallRecorderPolicyResult` now returns
`UNSUPPORTED_MEETING_PLATFORM` (bot not required) when the resolved
conference link is not a supported platform.
- Document supported platforms and the ignore behavior in the
call-recorder README.

## Tests

- New unit tests for `isSupportedMeetingPlatformUrl`.
- New policy test for the unsupported-platform case; updated existing
policy tests to use real supported URLs.
- All call-recorder unit tests pass; typecheck and lint clean.

Closes twentyhq/core-team-issues#2705


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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23050?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-07-20 13:49:21 +02:00
Raphaël Bosi 70e4d8d36e Skip install-apps onboarding step for invited users (#22823)
## What

The onboarding "install apps" step was proposed to every new user,
including those joining an existing workspace through an invitation
link. Now it is only proposed to the user who creates the workspace.

## Why

App installation only makes sense for the workspace creator. Invited
members should go straight to profile creation.

## How

`activateOnboardingForUser` set the `ONBOARDING_INSTALL_APPS_PENDING`
flag unconditionally, and that flag is the sole driver of the
`APPS_INSTALLATION` status (the frontend just follows the backend
status). Gated the setter behind a new `shouldShowInstallAppsStep` flag,
mirroring the existing `shouldShowConnectAccountStep` flag: creator path
passes `true`, join path (personal invitation, public invite link, SSO
into an existing workspace) passes `false`.

Backend-only change, no migration needed. Covered by unit tests for both
branches.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22823?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-07-20 09:25:52 +00:00
martmull c90057178c Bump app version (#23049)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23049?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-07-20 10:52:27 +02:00
martmull 89609c520c Reduce call-recorder Recall API load and harden bot scheduling recovery (#23014)
## Context

We receive Recall rate limit alerts on `/api/v1/bot`. Recall's List Bots
endpoint allows only 60 requests/min per Recall workspace (vs 300/min
for Retrieve and 120/min for Create), and that budget is shared by every
Twenty workspace on the instance since `RECALL_API_KEY` is a
server-level variable. The call-recorder recovery crons fanned out one
list call per stuck recording, fired at the same wall-clock minute for
every workspace, and never resolved dead rows, so the pending set only
grew.

This PR reworks the bot-scheduling recovery mechanism so that
crash-recovery work is rare, cheap, and mostly event-driven. One commit
per change:

## Changes

1. **Fail never-scheduled recordings once their meeting ends**
(`bot_never_scheduled` failure reason). Previously these rows stayed
`REQUESTED+SCHEDULED` forever and were re-fetched by every recovery run.
Rows with an unresolved creation attempt keep their recovery chance
until the 7-day convergence lookback passes (a bot may have recorded
before the id write-back was lost), then fail as
`bot_schedule_outcome_unknown`.

2. **Batch bot lookups into one list call per run.** The pending-bot
sweep and the failed-cancellation retry each issue at most one
workspace-wide `GET /api/v1/bot/` (filtered by `twentyWorkspaceId` +
active statuses) and match bots to recordings in memory via
`twentyCallRecordingId` metadata, instead of one list call per stuck
row. Truncated lists count as failed lookups so an incomplete map never
authorizes a duplicate creation.

3. **Record a `botScheduleAttemptedAt` marker before POSTing a bot.**
Recovery can now distinguish rows that never reached Recall (re-schedule
directly, zero Recall reads) from rows whose creation outcome is unknown
(only these join the lookup).

4. **Store the bot-creation `Idempotency-Key` on the row and recover by
re-sending.** When a stuck row's stored key still hashes from the
current scheduling inputs, recovery re-sends the creation: Recall either
returns the existing bot or creates the intended one, all on the Create
budget (120/min) without touching the List budget (60/min). Drifted
inputs still fall back to the lookup. Re-sends preserve the first
attempt's timestamp and are only trusted within a 12-hour window, so
repeated unknown outcomes age into the lookup path rather than risking a
twin bot after Recall's key retention expires.

5. **Resume pending rows on `callRecording.updated` events and slow the
cron.** A new database-event trigger resumes scheduling within seconds
when a row transitions back to pending (bot vanished at Recall, canceled
request re-requested, failed row reset by reconciliation), with queue
retries. It skips creations (the inserting run schedules inline), skips
its own progress writes, uses slim-payload diffs to skip cheaply, and
defers ambiguous rows to the cron so event bursts cannot fan out list
calls. The pending-requests cron becomes a backstop and drops from every
5 minutes to every 15.

Follow-up commits harden edge cases raised in review (status
revalidation before POST, per-row cancellation recovery window,
future-timestamp guard, attempt-state cleanup when a bot is confirmed
gone at Recall) and add a lifecycle integration test.

## Notes

- Two new app fields on `callRecording`: `botScheduleAttemptedAt`
(DATE_TIME) and `botScheduleIdempotencyKey` (TEXT), both nullable and
not UI-editable.
- A tight race between the event trigger and the cron converges on one
bot via the deterministic idempotency key.
- Not addressed here (needs a server-side change): per-workspace jitter
when dispatching logic-function cron triggers, so identical patterns
don't fire for every workspace on the same minute.

## Test

- New `call-recorder-lifecycle.integration-test.ts` on the app's
integration harness: the global setup installs the app on a live test
server, all reads and writes go through the real API into the test
database, and only externals are mocked — the Recall API (a fetch
interceptor that replays the same bot for a repeated `Idempotency-Key`,
like the real API) and the trigger transports (webhook payloads invoke
the webhook logic function handler; cron and database-event triggers run
their flows). Thirteen scenarios assert the resulting CallRecording rows
in the DB: scheduling from calendar reconciliation (events attached to a
seeded `SHARE_EVERYTHING` calendar channel, since unassociated events
are invisible), webhook status progression with artifact-import route
calls, transcript completion, out-of-order delivery protection, fatal
failure, unknown bots, cancellation with retried Recall delete, and
every crash recovery path. Verified locally against a live server: 15
integration tests pass (including the existing schema contract test).
- `yarn test:unit`: 488 tests pass. `yarn typecheck` and `yarn lint`
clean.

---------

Co-authored-by: martmull <martin@twenty.com>
2026-07-20 10:37:04 +02:00
Félix Malfait 6e1e98f4ab fix(server): shard the server-test unit job to stop the intermittent crash (#23009)
## Problem

`server-test` fails intermittently: exit 1 with **no `FAIL` line and no
`Test Suites:/Tests:` summary** — the jest run is aborted mid-way,
before the reporter's `onRunComplete`.

## Root cause

The `test` target runs the entire unit suite (~6,600 tests) in **one
in-band jest process on a single runner VM** (`nx.json` sets
`maxWorkers: 1` for the `ci` configuration). A few minutes in, that
process is killed by an **external `SIGKILL`** — confirmed *not* OOM
(~15 GB free at kill time, no cgroup `oom_kill`) and *not* an in-process
crash (a Node diagnostic report armed with `--report-on-fatalerror` +
`--report-uncaught-exception` writes nothing). The whole-run kill is why
it fails intermittently with no summary. `maxWorkers=2` on one VM still
dies, so the threshold is **per-VM**, not per-process.

## Fix

Shard the unit suite across VMs, the same way `server-integration-test`
already does:

- A `twenty-server` `test:ci` target runs jest directly (so `--shard`
forwards) with `dependsOn: ["^build"]` so the workspace deps are built.
- `server-test` becomes a 4-way matrix; each shard runs a quarter of the
suite, well under the kill threshold.
- `ci-server-status-check` already aggregates `server-test`, so required
checks are unchanged.

Also provides two mocks a completed run needs but the SIGKILL had been
masking in `ApplicationRegistrationService.upsertFromCatalog` unit
tests: the `MetricsService` provider and
`applicationRegistrationRepository.createQueryBuilder`.
2026-07-20 06:39:13 +02:00
github-actions[bot] cdb7e56720 chore: sync AI model catalog from models.dev (#23015)
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/23015?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-07-19 08:48:44 +02:00
github-actions[bot] dcd6683cac i18n - translations (#23007)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23007?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-07-17 22:48:12 +02:00
neo773 5bedd5b8cc feat(email-group): communications UX, per-record DNS status (#23002)
- Rename Communications label to singular, remove docs-home banner
- Provision unsubscribe Cloudflare records at domain creation and
surface per-record status badges; skip Cloudflare when not configured
- Move sending-domain status into the section header and only show the
records table when a record is unverified
- Reply to the original recipients when replying to your own message
- Fix DNS records table column/badge alignment; emit synthetic records
in the log driver for local testing

<img width="1496" height="849" alt="Screenshot 2026-07-17 at 7 47 24 PM"
src="https://github.com/user-attachments/assets/a5a59adb-2df4-4154-98b2-acf87a8008da"
/>
2026-07-17 22:41:25 +02:00
github-actions[bot] 80ff1716e5 i18n - website translations (#23005)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23005?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-07-17 21:20:33 +02:00
Félix Malfait 7e133a4930 Converge email recipient fields on existing patterns: shared parser/formatter, search-index members, one display-name rule (#22997)
# Why

Follow-up to #22668, addressing @charlesBochet's five post-merge review
comments. They all point the same direction: the recipient fields
rebuilt things the codebase already had. This PR converges on the
existing patterns where that holds up, and answers on the threads where
it deliberately does not.

# What changed, per comment

**Parser duplication
([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064242))**:
`parseEmailAddressList` now lives in twenty-shared (addressparser, group
flattening, try/catch). The server's `safeParseEmailAddresses` delegates
to it, the front wrapper keeps only paste normalization (newlines to
commas) and invalid-token preservation for red chips. The
`addressparser` dependency moves from twenty-front to twenty-shared.
Side effect worth knowing: RFC 5322 group members in inbound To/Cc
headers were previously dropped entirely (group entries have no
top-level address, so the filter removed them); flattening now imports
those participants. Covered by a new regression test.

**Formatter duplication
([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064243))**:
`formatEmailAddress` (quote only when specials require it) lives in
twenty-shared. The composer chips and the server's
`formatMessageFromHeader` both delegate to it. The Gmail From header
output is byte-identical: the name is mime-encoded first and encoded
words never contain characters that trigger quoting. CodeQL then caught
that the quoting (ported from the original front util) escaped quotes
but not backslashes, letting a crafted name close the quoted string
early; escaping now covers both as RFC 5322 quoted-pairs, with a
containment test proving a hostile name cannot split into extra
recipients on reparse.

**Member search divergence
([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064231))**:
suggestions now search WorkspaceMember through the search index in the
same `useObjectRecordSearchRecords` call as Person (one ranked query),
and enrich hits from `currentWorkspaceMembersState`, exactly like
`SettingsRoleAssignmentWorkspaceMemberPickerDropdown`. The client-side
`filterBySearchQuery` pass is gone. The hook is now what the comment
described: the merge of context people, searched people, and members
into one ranked list, rendered with the same
`SelectableList`/`MenuItemAvatar` primitives the pickers use. Also fixed
while in there: searched person ids are sliced to the suggestion limit
before hydration, so top-ranked people can no longer be crowded out of
the hydration page.

**Chip resolution duplication
([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064236))**:
the display-name preference is now one rule,
`getEmailIdentityDisplayName`, used by both
`getDisplayNameFromParticipant` (threads) and the composer chip/menu, so
the same address renders identically everywhere. The order is workspace
member, then person, then display name, then handle: when an address
belongs to both a teammate and a Person record, the internal identity
wins (product call from Felix). `BaseChip.maxLabelWidth` is renamed
`maxWidth` to match the twenty-ui `Chip` API. `ParticipantChip` itself
is not used inside the field: it renders a navigating `RecordChip` when
a person is linked, and navigation from the composer destroys the draft
(no draft persistence yet), plus the field chips need
remove/selected/danger/edit affordances it does not have.

**Rebuilding on MultiItemFieldInput
([comment](https://github.com/twentyhq/twenty/pull/22668#discussion_r3553064221))**:
answered on the thread rather than in code, deliberately.
`MultiItemFieldInput` is a dropdown-panel list editor (vertical rows,
one input at a time, bound to record-field contexts and
`FieldMetadataType`), and its own TODO says the API should be refactored
into a hook before growing. The inline wrapping chip row commits batches
(paste), dedupes with a flash, and keeps a persistent inline input with
suggestions; layering that through `renderItem`/`renderInput` would
strain both components. On the menu overlap: after comparing side by
side, the shared surface between `MultiItemFieldMenuItem`'s dropdown and
the chip menu is three `MenuItem` rows with different copy, order, and
neighbors; `MenuItem` is already the shared primitive, and a
config-driven fragment would be indirection without deduplication. If
deeper convergence is wanted, the honest path is the existing TODO
(extract the multi-item state machine into a hook, rebase both editors
on it); that touches the Links/Phones/Emails/Array/Files cell editors
and deserves its own PR.

# Verification

- New twenty-shared suites for the parser and formatter (16 tests),
including parse/format round-trips, the encoded-word case, and the
backslash-escaping containment case.
- Server messaging util specs all pass (70 tests), including new
group-flattening regression tests; From-header spec output unchanged.
- Front email module suites all pass (59 tests) with the slimmed
wrappers.
- Typecheck and lint green on twenty-shared, twenty-front,
twenty-server; oxfmt clean on all three.
- Playwright smoke against the seeded dev stack passes end to end:
context suggestions on the Google company, typed search showing people
and the workspace member row (now served by the search index), Enter
picking the top suggestion, duplicate merge, keyboard delete, chip menu
with clipboard copy, Ctrl+Enter committing the buffer then triggering
send.
2026-07-17 21:20:14 +02:00
Rashad Karanouh ccbd3b6c46 Client brief wizard — /partners/brief (#22291)
## Brief — website (Release ① of the brief + glowup rollout)

Public client-brief wizard at `/partners/brief`, plus the marketplace
entry points (match-me card, brief prompt/link, brief CTAs across
partner surfaces).

**Backend already shipped:** the `submit-client-brief` logic function
merged in #22290 (v1.2.0) and is live in prod, so this PR is
**website-only** and needs no app deploy.

Rebased onto current `main` (was ~341 commits behind); lint + format
pass locally, typecheck/tests via CI.

### Release sequence (do not break)
1. **① Brief website — THIS PR.** Independent; backend already live in
prod. → merge → website deploy.
2. **② Glowup app → prod** (#22470). Rebase onto `main` (SDK 2.21),
apply deterministic-id handling, bump 1.2.10 → 1.3.0, `deploy` +
`install` on `partner-twenty-com`, set new app variables, refresh
partners-doc. **This is the gate for ③.**
3. **③ Glowup website** (#22471) — only **after ② is LIVE on prod** (it
reads the new partner links / services / case-study objects). → merge →
website deploy.
4. Reconcile #22637 (partners-traffic-web) with ③ — both touch
`partners-marketplace/*`.

Draft — do not merge until vetted.
2026-07-17 17:13:16 +00:00
nicoko93 0d13db1d9c fix(twenty-server): re-list marketplace registration when catalog serves an app first installed locally (#22877)
## Problem

Fixes #22872.

An app first installed from a **local/CLI source** (`yarn twenty dev` /
tarball upload) gets its `applicationRegistration` created with
`isListed: false` — sensible for a dev app. But when that same app (same
`universalIdentifier`) is later **published to the configured app
registry**, the marketplace catalog sync's update branch in
`upsertFromCatalog` spreads the existing entity and updates
name/sourceType/sourcePackage/version/manifest **without ever setting
`isListed` back to `true`** (only the create branch does).

Result: the app is permanently invisible in the Marketplace tab
(`findManyMarketplaceApps` → `findManyListed()`), while
`installApplication(universalIdentifier)` still works — a confusing
split-brain state with no error anywhere. Reproduced on a self-hosted
v2.18.5 with a private Verdaccio registry (details and repro steps in
the issue).

## Fix

In the `upsertFromCatalog` update branch, re-list the registration
**only when its previous source was local** (`TARBALL`/`LOCAL`):

```ts
const isRelistedFromLocalSource =
  existing.sourceType === ApplicationRegistrationSourceType.TARBALL ||
  existing.sourceType === ApplicationRegistrationSourceType.LOCAL;
...
isListed: existing.isListed || isRelistedFromLocalSource,
```

This deliberately does **not** blanket-set `isListed: true` on every
sync: an operator who delisted a registry-sourced app (via
`updateApplicationRegistration`) keeps their decision — the hourly sync
won't override it.

## Tests

New unit spec `application-registration-upsert-from-catalog.spec.ts`:

- re-lists a registration first created by a local install once the
catalog serves it;
- preserves an operator delisting of a registry-sourced registration;
- keeps an already-listed registration listed;
- still creates new catalog registrations as listed.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22877?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: Nicolas Chanal <nicolaschanal@MacBook-Pro-de-Nicolas.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:51:33 +02:00
Weiko 716e67a276 Copy application source files during build and install (#22991)
## Summary
This is a requirement for the 2-way sync feature

- Copy logic function and front component source files into the build
output during SDK app builds.
- Share application file list construction between install and build
paths so source and built artifacts stay aligned.
- Allow app installs to skip missing optional source files for backward
compatibility while still requiring built artifacts.
- Extend watcher handling to track source-file uploads and restart when
the source set changes.
- Update integration coverage to assert built outputs and copied source
files for minimal apps.

<img width="940" height="165" alt="Screenshot 2026-07-17 at 14 53 40"
src="https://github.com/user-attachments/assets/b9306990-5fc0-4866-a390-3bb8c21a88ad"
/>
<img width="579" height="181" alt="Screenshot 2026-07-17 at 14 53 57"
src="https://github.com/user-attachments/assets/af282318-76c3-49a9-b62a-833a0aadc688"
/>
2026-07-17 17:41:39 +02:00
admin-laicadev 62aa4f6dac docs(skills): use 'twenty apply' in codex-plugin skills (dev --once deprecated) (#22880)
The `twenty` CLI deprecated `yarn twenty dev --once` in favour of `yarn
twenty apply` (added in #22372). The developer docs were updated in
#22688, but the codex-plugin skills still tell agents to run the
deprecated command. This swaps `yarn twenty dev --once` -> `yarn twenty
apply` (including the `--verbose` variants) in the create-app,
develop-app and manage-app skills so agent guidance matches the current
CLI.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22880?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-07-17 17:16:32 +02:00
martmull 4e2f9e3416 Fix join at in the past (#23000)
as title, we floor the bot join at date 1second in the future

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23000?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-07-17 14:59:09 +00:00
Félix Malfait aafee63706 Count self-hosted billable seats per distinct user (#22986)
## Context

Self-hosted enterprise pricing is documented as per user ("Pricing is
per user and each user needs a licence"), but seat counts reported to
the enterprise API counted active `userWorkspace` rows. A user belonging
to two workspaces on the same instance was billed as two seats.

## What this does

- Adds `EnterprisePlanService.getBillableSeatCount()`, which counts
`DISTINCT userId` over non-deleted userWorkspaces, keeping the existing
floor of 1.
- Uses it at all four seat-reporting sites: the checkout session
quantity, the seat reports on key activation (`setEnterpriseKey`) and
server-binding release, and the recurring validation cron report.
- Removes the two duplicated private `getActiveUserWorkspaceCount()`
counters and their `UserWorkspaceEntity` repository injections from the
resolver and cron job.
- Adds unit tests for the new method (dedup across workspaces, floor of
1, missing row).

## Intentionally unchanged

- The website `/api/enterprise/seats` and `/checkout` routes apply
whatever quantity the instance reports, so no Stripe-side change is
needed.
- Instance telemetry still reports both `activeUserWorkspaceCount` and
`distinctUserCount`, so the delta stays observable.
- Existing subscriptions need no migration: the next cron seat report
prorates affected instances down automatically.

## Test

- `enterprise-plan.service.spec.ts`: 58 passed (3 new)
- `lint:diff-with-main` and `typecheck` for twenty-server pass

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22986?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-07-17 16:16:12 +02:00
nitin 19f0e3cad5 Reduce Recall bot lifecycle reconciliation traffic (#22908)
## Summary

- split pending Call Recording request maintenance from stale recording
convergence
- recover Recall bots by workspace and Call Recording metadata before
creating a replacement
- retry failed cancellations, including the canceled-plus-botless
write-back race
- move the broad orphaned-bot list sweep from every five minutes to a
dedicated daily job
- filter Recall bot lists by workspace metadata at the provider boundary

## Why

This is stack 1/3 extracted from #22739. Healthy installed workspaces
currently list Recall bots every five minutes even when no local state
has diverged. This layer removes that unconditional list sweep while
keeping pending request recovery at five-minute latency.

The cancellation recovery also closes a crash window where Recall
accepted a bot creation but the local bot ID write-back failed before
the user canceled the request. The maintenance job now rediscovers and
cancels that bot before it can join.

## Stack

1. **Recall bot lifecycle reconciliation** — this PR
2. Divergence-scoped recording synchronization — #22909
3. Artifact import offloading — #22910

## Validation

- `npm run typecheck`
- `npm run lint`
- `npm run test:unit` — 71 files, 454 tests

---------

Co-authored-by: Claude <martmull@hotmail.fr>
2026-07-17 15:49:28 +02:00
Félix Malfait dc0bb7760f fix(front): only sign out when token renewal is rejected by the server (#22983)
## Context

Users are frequently signed out when coming back to Twenty. The console
shows `Failed to renew token after retries, triggering unauthenticated
error`: the access token has expired and the `renewToken` call fails.
Today any renewal failure wipes the stored token pair and redirects to
sign-in, even when the refresh token is still valid, for example when
the renewal request hits a transient network failure (laptop waking up,
VPN reconnecting) or a server restart during a deploy. Since the token
pair state is synced across tabs, one failing tab signs out every tab.

## What this does

- Only triggers the unauthenticated flow when the server definitively
rejects the refresh token. The `renewToken` mutation maps those cases to
`UNAUTHENTICATED` (expired or invalid JWT), `FORBIDDEN` (revoked) and
`BAD_USER_INPUT` (unknown or malformed token).
- Keeps the session on any other renewal failure (network errors after
retries, server errors): the token pair stays in place and the next
request triggers a fresh renewal attempt, so the session recovers once
the server is reachable again.
- Signs out immediately when the stored pair has no refresh token
instead of attempting a renewal that cannot succeed.
- Logs the renewal error, which was previously swallowed and made this
class of logouts hard to diagnose.

## Tests

- renews and replays the operation after an access token rejection
- signs out when the server rejects the refresh token
- keeps the session on a network error (asserts all retry attempts ran)
and on a server error
- signs out without attempting renewal when the stored pair has no
refresh token

Test mocks now reset between tests so per-test overrides cannot leak
into other tests.

[[Review in
cubic](https://www.cubic.dev/buttons/review-in-cubic-dark.svg)](https://cubic.dev/pr/twentyhq/twenty/pull/22983?utm_source=github)
2026-07-17 15:45:54 +02:00
neo773 f6612e5a85 fix(server): stop treating Microsoft Graph 401 as a permanent failure (#22989)
In production we are seeing some accounts being occasionally marked as
permanent failure even though when you check with their refresh token it
never actually failed this PR stops treating 401 as permanent failure
and treats them as Transient error.

We already have token refresh stage that runs before the actual import
stage, so if it's an actual revoke token error, it should catch it.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22989?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-07-17 19:13:50 +05:30
Charles Bochet 39082cf787 Only show the install-apps onboarding step to workspace creators (#22990)
New users joining an existing workspace through an invite link were
routed through the "Install your first apps" onboarding step, which is
meant for workspace creation only.

#22347 set `ONBOARDING_INSTALL_APPS_PENDING` inside
`activateOnboardingForUser`, which is shared by both sign-up paths. Gate
it per path like the connect-account step: `true` on
`signUpOnNewWorkspace`, `false` on `signInUpOnExistingWorkspace`.

Verified locally: invited users now go straight from create-profile into
the workspace, and workspace creators still get the install-apps step.
Covered by a unit test on the invite path and integration tests
asserting the onboarding status per sign-up path (invite →
`PROFILE_CREATION`; workspace creation → `SYNC_EMAIL` then
`APPS_INSTALLATION`).
2026-07-17 15:33:42 +02:00
Charles Bochet 3303bdd258 fix(docker): bump curl pin to 8.20.0-r0 (2 high + 6 medium CVEs) (#22994)
## Context

AWS Inspector flags `curl 8.19.0` in every `prod-twenty` image,
including current builds, with 2 high and 6 medium findings:

- **High**: CVE-2026-6276, CVE-2026-5773
- **Medium**: CVE-2026-4873, CVE-2026-5545, CVE-2026-6253,
CVE-2026-6429, CVE-2026-7009, CVE-2026-7168

These drive the Oneleet monitor "AWS ECR repository image
vulnerabilities are remediated" (high severity SLA window currently
open).

## Fix

All 8 CVEs are fixed in Alpine 3.23's `curl 8.20.0-r0`, now available in
the v3.23 main repository:

- `twenty-server` runtime stage: raise the pin from `curl>=8.19.0-r0` to
`curl>=8.20.0-r0`
- `twenty-app-dev` stage: pin the previously unpinned `curl` to the same
floor

## Verification

Built the runtime apk layer locally against the pinned
`node:24.18.0-alpine3.23` base:

```
curl-8.20.0-r0
libcurl-8.20.0-r0
curl 8.20.0 (aarch64-alpine-linux-musl) libcurl/8.20.0 OpenSSL/3.5.7 ...
```

Next deployed image will carry the patched curl, and the remaining
findings on old images age out via the 14-day ECR lifecycle.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22994?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-07-17 15:23:56 +02:00
github-actions[bot] c8f0b86316 i18n - translations (#22988)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22988?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-07-17 14:11:31 +02:00
Félix Malfait 5e27e04c0a Add mostly-empty field hints to data model settings (#22962)
## What

Fields that are empty in almost all records now show a subtle `Mostly
empty` hint next to their name in the object's Fields settings table
(same visual treatment as `Deactivated`), with a tooltip explaining the
signal and a matching **Mostly empty** toggle in the search filter
dropdown. The goal is to nudge admins to clean up and deactivate fields
nobody uses, while keeping the page untouched when the data model is
healthy.

## How

**No table scans.** Emptiness is read from Postgres planner statistics,
so the cost is a catalog lookup regardless of table size:

- `pg_class.reltuples` gates the feature on an approximate row count (≥
100 records; never-analyzed tables mean no hints). Reuses the shared
helper extracted from `ObjectRecordCountService`.
- `pg_stats.null_frac` plus the sampled frequency of the column type's
empty sentinel (`''` for text columns, `'{}'` for arrays, `'{}'`/`'[]'`
for json — matched per physical column type) gives a per-column empty
fraction. A value dominating ≥ 95% of a column is guaranteed to appear
in the most-common-values list, so the approximation is reliable exactly
at the threshold we care about.

**Decision rules** (pure util, unit-tested):

- Flag when every relevant column is ≥ 95% empty and the object has ≥
100 records.
- Skip system fields, the label identifier, relations, booleans, and
actor fields (exhaustive switch — a new `FieldMetadataType` fails to
compile until classified).
- Composite fields must have all their columns empty, with column sets
derived from `compositeTypeDefinitions`; only default-bearing code
columns (`currencyCode`, phone country/calling codes) are excluded so
stamped defaults don't mask emptiness.
- Anything unknown (missing stats, new column since last ANALYZE)
degrades to silence — no hint is ever shown on missing data.

**API:** one `mostlyEmptyFieldMetadataIds(objectMetadataId)` query on
the metadata schema, guarded by the `DATA_MODEL` settings permission,
fetched lazily when the fields page opens.

**UI:** exception-based — no new columns, no persistent controls. The
badge and the filter toggle only materialize when at least one field
qualifies, and disappear once things are cleaned up.

## Test

- Unit tests for the decision util (threshold,
system/label-identifier/inactive exclusion, missing statistics,
composite all-columns rule, links label/secondary data, currency
narrowing, excluded types).
- Catalog SQL validated against Postgres 16 with a table mimicking
Twenty's column shapes (text `''` defaults, enums, arrays, jsonb,
currency pairs), including the type-aware sentinel matching (a text
column full of literal `"{}"` strings does not count as empty).
- End-to-end on a seeded dev instance: 899 companies with a mix of
filled/empty fields — the API returned exactly the five fields predicted
by the raw statistics (`annualRevenue`, `employees`, `introVideo`,
`tagline`, `workPolicy`) and correctly excluded `address` (city 33%
filled), actor/system fields, and the label identifier.
- UI driven with Playwright: badge, tooltip copy, filter toggle, and
filtered table all verified visually.
- `lint:diff-with-main`, `typecheck` (server + front), and all three
`graphql:generate` configurations + SDK metadata client regenerated and
committed.
2026-07-17 12:03:11 +00:00
Weiko 20e74d0553 Revert "Remove calendar week view feature flag from public flags" (#22987)
Reverts twentyhq/twenty#22950

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22987?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-07-17 13:41:05 +02:00
Charles Bochet 2e671342f5 [2/2] Write CREATED at activation, clean stale onboarding workspaces (#22915)
## Context

Follow-up to #22904 (merged), which introduced the `CREATED` activation
status (schema provisioned, onboarding incomplete — no billing
subscription), its enum migration, and the read path. This PR turns the
status on and closes the zombie-workspace leak (~60–110
subscription-less ACTIVE workspaces per day since v2 onboarding, 935+
total).

 **Deploy gating satisfied**: #22904's slow enum migration shipped with
the release deployed to prod on 2026-07-17, so writing `CREATED` is now
safe. Rebased on main (clean, no conflicts) — main's #22943/#22955
guarded-transition rework already handles `CREATED` correctly: the
webhook suspend switch only suspends `ACTIVE` workspaces, and
`reactivateWorkspace` promotes `CREATED`→`ACTIVE`.

## What this PR does

1. **Write path** — `activateWorkspace` sets `CREATED` instead of
`ACTIVE` when the workspace has no billing subscription.
`hasWorkspaceAnySubscription` returns true when billing is disabled, so
self-hosted workspaces keep going straight to `ACTIVE` — no behavior
change outside cloud.
2. **Cleanup** — `CREATED` joins `PENDING_CREATION`/`ONGOING_CREATION`
in the existing onboarding cleaning flow (cron +
`workspace:clean:onboarding` with `--dry-run`): workspaces older than
the same seven-day threshold are soft-deleted, then hard-deleted on a
later run. **No suspension step and no emails** — an abandoned
onboarding is treated as never having completed, exactly like a
workspace stuck in creation. A workspace that subscribes before cleanup
exits the flow (`CREATED`→`ACTIVE` synchronously via checkout).
3. **Backfill** — slow instance command moving `ACTIVE` workspaces with
no `billingSubscription` row, created since v2 onboarding shipped
(2026-07-01), to `CREATED`. Gated on `IS_BILLING_ENABLED` so self-hosted
instances are untouched.
4. **Resolves #22904's text-cast TODO on the billing activation update**
— this PR only deploys after the enum migration, so the
`CREATED`→`ACTIVE` promotion is a plain status-scoped update again. The
upgrade-path filters (`activationStatusIn`) keep the `::text` cast:
upgrade tooling has to run against databases coming from pre-2.22
versions, so its TODO now points at the real removal trigger (dropping
pre-2.22 upgrade support).

## Ops note before deploying

The backfilled zombies are all older than seven days, so the first cron
run after the backfill **soft-deletes them and the next run destroys
them (schema and data), with no user-facing communication**. The
backfill also catches any post-July-1 cloud workspace that is ACTIVE
without a subscription — including intentionally comped/demo/internal
ones if any were created since then (verified locally: the seeded demo
workspaces matched). **Run the backfill's SELECT as a dry-run against
prod and review the list before deploying.**

## CI note

~~`cross-version-upgrade` (and its `ci-server-status-check` aggregate)
is red due to a pre-existing regression on main — `Field metadata
"coreWorkflowVersionId" is missing in object metadata workflowVersion`
on the seed workspaces.~~ Resolved: the rebase picks up main's
#22944/#22961 which fixed that regression.

## Verification

Server-side (billing-enabled local instance, Stripe test mode) and
through the full onboarding UI in both billing modes:

- **Billing enabled, UI**: signup → workspace creation →
**`activationStatus: CREATED`** in DB mid-onboarding → profile/invite
steps work on the CREATED workspace → plan-required page → no-card trial
→ app loads, workspace **`ACTIVE`** with a `trialing` subscription
(exercises #22904's synchronous promotion).
- **Billing disabled, UI**: signup → workspace creation → **`ACTIVE`
directly**, no plan step anywhere, app loads — self-hosted behavior
unchanged.
- **Cleanup**: a `CREATED` workspace backdated 8 days is listed by
`workspace:clean:onboarding --dry-run`; the real run soft-deletes it
silently (no suspension, no email) and the next run hard-deletes it
(workspace row and schema gone).
- **Backfill**: synthetic `ACTIVE` no-sub workspaces — created
2026-07-05 flips to `CREATED`, created 2026-06-15 stays `ACTIVE`,
subscribed workspaces stay `ACTIVE`; billing-disabled short-circuit
returns without touching anything.
- Lint + typecheck green.
2026-07-17 11:26:46 +00:00