Commit Graph

10745 Commits

Author SHA1 Message Date
Paul Rastoin b768441c13 Fix 2.16 search field metadata cross version upgrade (#22039)
# Introduction
Allow decorating at class scope the properties introduced in specific
upgrade command
```
@WasIntroducedInUpgrade({
  upgradeCommandName:
    ADD_UNIVERSAL_IDENTIFIER_AND_APPLICATION_ID_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME,
  properties: ['universalIdentifier', 'applicationId', 'position'],
})
``` 

Here the search field metadata has been created as it without extending
the syncableEntity a previous PR I've created now extends it, but
nothing has been protected the fact they're not decorated. Also having
to re-declare the properties would be redundant to me

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22039?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 16:51:07 +00:00
Paul Rastoin b7350eba46 Fix main ci: generate client-sdk (#22042)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22042?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 18:24:53 +02:00
Raphaël Bosi f5f8865c4e Add a real README for the twenty-ui package (#22038)
Replaces the twenty-ui README, which was a long internal
design/migration document, with a real package README aimed at
consumers.

The new README covers installation, peer dependencies, a verified usage
example, the available subpath entry points, theming, and development
commands. It is what will be shown on the npm package page once
twenty-ui is published.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22038?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 18:24:04 +02:00
Raphaël Bosi b8e1004534 Bump twenty-ui to 1.0.0-alpha.0 for first npm pre-release (#22040)
Sets `twenty-ui` to `1.0.0-alpha.0` so it can be published as the first
pre-release on npm.

The package name was previously published once (`0.23.4`) and fully
unpublished in Sept 2024. Starting at `1.0.0-alpha.0` avoids the burned
version, stays above the old number, and publishes to the `alpha`
dist-tag (not `latest`) via the existing twenty-infra publish workflow,
so it can be dogfooded before a stable `1.0.0`.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22040?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 18:23:18 +02:00
Stefan Huber 5a55c2563e Update location of settings (#22033)
In the latest version the «Lab Features» are no longer called like that
and are in a different location.

See
[Discord-Discussion](https://discord.com/channels/1130383047699738754/1508471962308051116)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22033?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 18:03:36 +02:00
Félix Malfait 2e099c91e1 fix(domains): show custom domain DNS records and activation status without a page reload (#22037)
## Problem

Setting up a custom domain had two confusing UX issues, both caused by
local state not being refreshed after the relevant mutation:

1. **DNS records didn't appear after saving.** After hitting save you
got the green "Custom domain updated" snackbar, but the "Domain Setup"
section (the Cloudflare/DNS records to configure) stayed empty. You had
to leave the page and come back for the records to show up.
2. **The "Custom Domain" card stayed "Inactive"** even after the DNS
records validated as "Success". Only a full page reload flipped it to
"Active".

## Root cause

**Issue 1 — stale closure.** In `useSettingsCustomDomain.handleSave`,
the `updateWorkspace` `onCompleted` callback called
`setCurrentWorkspace({ ...currentWorkspace, customDomain })` and then
`checkCustomDomainRecords()`. But `checkCustomDomainRecords` guarded on
the closed-over `currentWorkspace.customDomain`, which was still `null`
at that render. The `setCurrentWorkspace` call doesn't synchronously
update that captured value, so the guard returned early and the records
were never fetched. Remounting the page (navigate away/back) ran the
on-mount effect with a fresh workspace, which is why the trip "fixed"
it.

**Issue 2 — `isCustomDomainEnabled` never refreshed locally.** The
Active/Inactive badge is driven by
`currentWorkspace.isCustomDomainEnabled`. The backend flips this flag
inside `checkCustomDomainValidRecords`
(`custom-domain-manager.service.ts`), but the mutation didn't return it,
so the local `currentWorkspaceState` stayed stale until a full reload
re-ran the bootstrap query. The green "Success" DNS rows read from a
different source (`record.status`), which is why the rows and the badge
disagreed.

## Changes

**Issue 1**
- `checkCustomDomainRecords` now accepts the domain explicitly
(defaulting to the workspace value), so the freshly-saved domain can be
passed straight from `handleSave` instead of relying on the stale
closure. No new `useEffect` introduced.
- Fixed the Reload button so it no longer passes its click event as the
domain argument.

**Issue 2**
- Added a nullable `isCustomDomainEnabled` field to the
`DomainValidRecords` GraphQL type, populated only by the custom-domain
check (the shared public-domain flow leaves it null, so it's backward
compatible).
- The frontend now writes that value back into `currentWorkspaceState`
when the check completes, using a **functional** Jotai update so a
concurrent `customDomain` update is never clobbered. The badge flips to
"Active" as soon as validation passes — on mount, on Reload, and right
after save.

I deliberately kept this targeted rather than introducing real-time
workspace sync: `isCustomDomainEnabled` only changes server-side during
the on-demand DNS check (mount/Reload/cron), so returning it from that
mutation is sufficient and far lower risk.

## Notes
- `packages/twenty-front/src/generated-metadata/graphql.ts` was updated
to match what `graphql:generate` produces for the new schema field
(codegen requires a running backend, which isn't available in this
environment). Worth re-running codegen in CI to confirm it's
byte-identical.
- No existing unit or integration tests reference these paths.

## Test plan
- [ ] Set a custom domain → DNS records appear immediately (no
navigation needed).
- [ ] Once DNS validates, the "Custom Domain" card flips to "Active"
without a reload.
- [ ] Reload button still refreshes records.
- [ ] Public domain validation flow is unaffected.

https://claude.ai/code/session_01BB6C6bpPZMUbMzKSCydEaj

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22037?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 18:02:44 +02:00
Félix Malfait f24be8eacb fix: keep AI chat open when opening a record full-page (e.g. workflows) (#22036)
## Problem

When the AI chat is open in the side panel and you click a workflow from
the Workflows list, the AI chat closes as you navigate to the workflow
page. Navigating between other index/list pages, or to settings, keeps
the chat open — only opening a record full-page closes it.

## Root cause

`useOpenRecordFromIndexView` unconditionally calls
`closeSidePanelMenu()` before navigating to a full-page record:

```ts
} else {
  closeSidePanelMenu();
  navigate(AppPath.RecordShowPage, { ... });
}
```

Workflows (and other objects excluded by `canOpenObjectInSidePanel` —
`workflow`, `workflowVersion`, `dashboard`) can't open in the side
panel, so they *always* take this branch and close the panel, including
the AI chat.

This is inconsistent with `PageChangeEffect`, which already lets the AI
chat survive navigation by exempting `SidePanelPages.AskAI`. That
exemption is why navigating between index pages or to settings doesn't
close the chat.

## Fix

Skip the close when the side panel is showing the AI chat, mirroring the
exemption already used in `PageChangeEffect`. Any other side panel page
still closes as before.

## Testing

- `nx lint:diff-with-main twenty-front` (file lints clean)
- `nx typecheck twenty-front` passes

https://claude.ai/code/session_01LtBBAxjn32FQVduyAi6B37

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22036?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 18:01:51 +02:00
Raphaël Bosi 766d90af7e Remove framer-motion from twenty-ui (#22021)
## What

Removes the `framer-motion` dependency from `twenty-ui` and replaces
every usage with pure CSS animations, reaching for Base UI primitives
where one fits:

- **Collapse/expand** (`AnimatedEaseInOut`,
`AnimatedExpandableContainer`): rebuilt on Base UI `Collapsible`
(CSS-animated `--collapsible-panel-height/width` + transition states).
Public props unchanged, so the ~28 call sites are untouched.
- **ProgressBar**: rebuilt on Base UI `Progress` (proper
`role`/`aria-valuenow`). The snackbar auto-dismiss countdown now uses a
CSS keyframe + `animation-play-state` (pause on hover), removing a
per-frame React re-render; `useProgressAnimation` is deleted.
- The remaining `Animated*` components, the circular spinner, checkmark,
and the placeholder pointer parallax move to plain CSS (SCSS modules +
the `duration()` helper + theme tokens).
- Deletes 3 unused components (`AnimatedTranslation`,
`AnimatedTextWord`, `AnimatedFadeOut`).

## Why

`twenty-ui` is a publicly published library with a size budget, so
dropping framer-motion shrinks what consumers ship. `twenty-front` keeps
its own framer-motion; that is out of scope here.

## Notes for reviewers

- A few `twenty-ui` components received framer props from `twenty-front`
call sites; those were migrated (e.g. `AnimatedLightIconButton` gained a
CSS `rotate` prop, and the `EMPTY_PLACEHOLDER_TRANSITION_PROPS` spreads
were removed).
- Behavior change: Base UI `Collapsible` animates only on open/close
transitions, so the old "animate in on first mount while already open"
case no longer plays (the `initial` prop is kept for API compatibility).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22021?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 17:43:08 +02:00
Johnny Martin d4e4e2612b feat(front): render Instagram URLs as @handles in link fields (#21642)
LinkedIn and X links already show a readable handle in Twenty's link
fields. Instagram doesn't — it just shows `instagram.com`, which isn't
much help when you're scanning a record.

This adds the same handling for Instagram. `instagram.com/ptcrash` now
shows as `@ptcrash`, in tables, on record pages, and in the edit menu.
Post and reel links (`/p/...`, `/reel/...`) have no handle, so they fall
back to `Instagram`.

How it works:
- `Instagram` added to the `LinkType` enum
- `checkUrlType` detects `instagram.com`
- `getDisplayValueByUrlType` pulls the handle and prefixes `@`
- a shared `isSocialLinkType` helper keeps the three display components
in sync

Tested with unit tests for both helpers, the updated story, and manually
against a record whose Instagram field is
`http://instagram.com/ptcrash`.

Closes #21644

Co-authored-by: Johnny Martin <ptcrash@users.noreply.github.com>
2026-06-23 17:25:50 +02:00
Félix Malfait 855664daa2 feat(timeline): activity kind registry (Layer A) (#21950)
## What & why

The timeline-activity system's contract is a magic `name` string
(`"company.updated"`, `"linked-note.created"`, `"message.linked"`)
decoded by `String.split('.')` in **four** different frontend spots and
produced by a hardcoded `if`-ladder + two listeners. It is not
extensible and it already harbored a latent bug.

This PR replaces that stringly-typed protocol with an explicit,
persisted **`kind`** contract consumed through registries on both ends.
Adding a new timeline activity type becomes: add a producer + register a
presenter — no edits to a central switch.

This is **Layer A** of a larger plan (see
`packages/twenty-server/docs/TIMELINE_ACTIVITIES_REFACTOR.md` and
`TIMELINE_ACTIVITIES_PR_A.md`). Layer B (timeline projection /
"inheritance") and Layer C (user-defined aggregation rules) are
intentionally **out of scope** here.

## 🐛 Bug fixed along the way

`calendar-event-participant.listener.ts` was writing calendar-event
timeline rows with `name: 'message.linked'` (copy-paste from the message
listener). It rendered "correctly" only by luck — the frontend routed on
`linkedObjectMetadataId → nameSingular`, never on `name`. This PR fixes
it at the source (`calendarEvent.linked` / `kind:
'linkedCalendarEvent'`), and the shared resolver also corrects
historical rows that carry the wrong `name`.

## Changes

**`twenty-shared`** — new `timeline` module
- `TimelineActivityKind` (`recordChange | linkedNote | linkedTask |
linkedMessage | linkedCalendarEvent | linkedRecord`) +
`resolveTimelineActivityDescriptor`, the **single** place that decodes
an activity into `{ kind, action }`. Reads the persisted `kind` when
present and falls back to legacy `name`/`linkedObjectMetadataId` parsing
(back-compat shim). Unit-tested (20 cases).

**`twenty-server`**
- Persist a nullable `kind` field on the `timelineActivity` standard
object (entity shape + field-metadata builder + universalIdentifier).
- Producers (`timeline-activity.service.ts`, the two participant
listeners) set `kind` explicitly; dev seeder populates it.
- Fix the `calendarEvent.linked` mislabel.

**`twenty-front`**
- Static `TIMELINE_ACTIVITY_PRESENTERS` registry replaces the render
`switch`, the icon `if`-chain, the diff-validation name-parsing, and the
`name.match(/note|task/i)` title-prefetch hack.
- New `EventRowGenericLinked` so an unknown linked object type renders a
real "linked a {object}" row instead of falling through to the wrong
(main-object) renderer.

## Migration / compatibility
- The `kind` column on this **workspace** standard object is created by
the normal workspace metadata sync — no hand-written migration. It is
**nullable**, so pre-upgrade rows degrade gracefully through the
resolver shim (they resolve correctly from `linkedObjectMetadataId` +
`name`). An optional backfill workspace command could populate `kind` on
old rows later; not required for correctness.
- No GraphQL breaking change — `kind` is additive, `name` is retained
for display/search.

## Test plan
- `twenty-shared` unit tests (resolver) 
- `typecheck` + `lint:diff-with-main` green on `twenty-front`,
`twenty-server`, `twenty-shared` 
- Reset + reseed a workspace: `kind` is populated for all seeded rows
(recordChange / linkedMessage / linkedNote / linkedTask /
linkedCalendarEvent) with no nulls 
- Manual end-to-end verification via Playwright on person / company
record timelines — screenshots in a follow-up comment.

Screenshots attesting the rendering (incl. the calendar fix) are posted
as a comment below.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21950?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 16:59:09 +02:00
nitin 5e8932001c Simplify Recall webhook logic function config (#22026)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22026?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 20:22:44 +05:30
Rashad Karanouh ceecf33555 fix(twenty-partners): restore partner side panel field visibility (#22024)
## Summary

- Restores 17 partner profile fields in the `FIELDS_WIDGET` view backing
the Partner record page side panel (My Profile, admin partner views).
- Read-locks `validationStage` and `partnerTier` for the Partner role so
admins still see them on the record page but partners do not on My
Profile.
- Renames legacy `profilePicture` label to "Profile Picture (legacy)" to
distinguish from the new file field.
- Bumps `twenty-partners` to **1.1.3** (already deployed to prod).

## Test plan

- [ ] `yarn lint` in `packages/twenty-apps/internal/twenty-partners` — 0
errors
- [ ] `yarn twenty dev --once` on a local workspace — sync succeeds
- [ ] As admin: open a Partner record → side panel shows all profile
fields including validationStage and partnerTier
- [ ] As Partner role (My Profile): side panel shows profile fields but
**not** validationStage or partnerTier
- [ ] Upgrade path: install v1.1.3 on an existing workspace — view
fields update in place

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22024?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 16:49:38 +02:00
Abdullah. bab71afe54 fix(deps): bump vitest to 4 in twenty-meeting-bot (drops vulnerable esbuild) (#22025)
## Summary

Bumps **vitest** `^3.1.1 → ^4.1.9` in `twenty-meeting-bot`, which lets
**vite** resolve to **8.0.16** — and vite 8 dropped esbuild entirely
(moved to rolldown). That **removes the vulnerable transitive
`esbuild@0.27.7` outright**, resolving [Dependabot alert
#1470](https://github.com/twentyhq/twenty/security/dependabot/1470) —
GHSA-g7r4-m6w7-qqqr (esbuild dev-server arbitrary file read on Windows,
`>=0.27.3 <0.28.1`).

## Why a parent-bump, not a resolution

- The vulnerable esbuild came from `vite@7.3.5` (`esbuild ^0.27.0`, a
`0.x` caret capped at `<0.28` — so `yarn up` couldn't reach the fix).
- vite is gated by vitest's vite range: vitest **3.x** allows only
`^5||^6||^7` (caps vite at 7 → esbuild 0.27); vitest **4.x** allows
`^8`, and **vite 8 has no esbuild dependency at all**.
- So bumping vitest lets vite resolve to 8, which **eliminates the
vulnerable dependency entirely** — no `resolutions` entry to force or
maintain. (Matches the repo's stated preference: fix by upgrading the
parent, not by resolution.)

## Verification

- `yarn install` — vite resolves to `8.0.16`; all `@esbuild/*@0.27.7`
platform packages pruned; the only esbuild left is `0.28.1`
(already-fixed, from another consumer).
- `yarn typecheck` — passes.
- `yarn test:unit` — **202 tests / 30 files pass** under vitest 4.1.9,
no peer warnings; `vite-tsconfig-paths` still compatible with vite 8.
- `yarn install --immutable` — passes.
- (Integration `yarn test` is gated on a live Twenty server, so not run
here — that requirement is independent of this bump.)
- Separate yarn project — changes are confined to
`twenty-meeting-bot/{package.json,yarn.lock}`; no root impact.

## Note

vite 8 supports tsconfig-paths resolution natively
(`resolve.tsconfigPaths: true`), so `vite-tsconfig-paths` could be
dropped in a follow-up — left as-is to keep this change minimal.
2026-06-23 19:40:31 +05:00
neo773 c6aca3f0ea fix(calendar): ID-first chunked import for Google and CalDAV (#22015)
Google and CalDAV returned full events and imported them inline in the
list-fetch job. Large/initial syncs overran BullMQ's lock, the job
stalled, the workspace query runner was released mid-import, and TypeORM
threw 'Query runner already released'.

Mirror the messaging pipeline: every provider now returns event IDs
only, cached in Redis; the import job drains them in
CALENDAR_EVENT_IMPORT_BATCH_SIZE chunks and re-enqueues until empty, so
no single job runs long. Adds Google/CalDAV import-by-id services and a
provider dispatcher; removes the full-events inline path.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22015?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 20:08:38 +05:30
nitin 9b2ca6f3f7 bump sdk and app version for call recording bot app (#22019)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22019?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 16:32:54 +02:00
Paul Rastoin e9d5d71cd3 Wire up search field metadata (#21964)
## Part 1 - Exact scope of the current PR (#21964)

close https://github.com/twentyhq/core-team-issues/issues/2586

This PR introduces `searchFieldMetadata` as a first-class flat metadata
entity and migrates the existing search surface onto it, with **no
change to which records are searchable** (ISO with `main`).

In scope (what the PR does):
- New flat entity `searchFieldMetadata` (universalIdentifier,
applicationId, **`position`**, maps, conversions), registered in the
central flat-entity constants and the migration build orchestrator.
- `searchVector.asExpression` is **derived server-side** from
`searchFieldMetadata` rows (validated by `isSafeTsVectorExpression`);
never trusted from client input.
- **Derivation order is deterministic, driven by each row's `position`**
([compute-search-vector-as-expression-from-search-field-metadatas.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-search-field-metadata/utils/compute-search-vector-as-expression-from-search-field-metadatas.util.ts)),
replacing the previous non-deterministic `(createdAt, id)` sort. That
sort collapsed to random UUIDs for standard fields (same `createdAt`),
so any rename/relabel rewrote the `STORED` generated column to a
logically-identical-but-textually-different expression and produced a
permanent per-workspace diff vs the standard definition. Ordering now
equals provisioning order; ties break on `universalIdentifier`.
- Provisioning at object creation mirrors the existing surface exactly
**and seeds `position`**:
- custom objects -> the `name` field only, at `position: 0`
([build-default-search-field-metadatas-for-custom-object.util.ts](packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-search-field-metadatas-for-custom-object.util.ts))
- standard objects -> their curated `SEARCH_FIELDS_FOR_*` sets,
`position` = the curated index
- Backfill (instance + workspace commands in `2-16`) provisions rows for
existing workspaces with the same surface **and the same positions**
(standard from the curated standard maps, custom `name` = `0`), scoped
to the workspace's own custom application
([build-search-field-metadata-backfill-operations.util.ts](packages/twenty-server/src/database/commands/upgrade-version-command/2-16/utils/build-search-field-metadata-backfill-operations.util.ts)).
The `position` column is added in the same `2-16` fast instance command
as `universalIdentifier`/`applicationId`.
- Field rename of an already-indexed field recomputes `asExpression`
(positions preserved, so order is stable)
([recompute-search-vector-on-field-rename.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-search-vector-on-field-rename.util.ts)).
- Field delete drops the matching row(s) and recomputes; remaining rows
keep their relative order (no renumber)
([from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts)).
- Object relabel is **additive** and ISO/regression-fix only: it indexes
the new label identifier **appended last (`position = max(existing) +
1`)** without dropping `name`
([recompute-search-vector-on-label-identifier-update.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-on-label-identifier-update.util.ts)).
This is a deliberate, temporary bridge.

Explicitly OUT of scope (deferred):
- No API to edit `searchFieldMetadata` (no user-facing search-field
configuration, including `position` — it is internal and only written by
provisioning/backfill/recompute).
- No auto-indexing of arbitrary searchable fields. Creating a custom
TEXT/EMAILS/etc. field does NOT add it to search (the
`computeSearchFieldMetadataCreationForFields` behavior was removed in
`e6820ad`).
- No field-type-transition handling (field type is immutable - not in
`FLAT_FIELD_METADATA_EDITABLE_PROPERTIES`, so that path was dead code).
- No `position` validation (uniqueness/range) and no multi-vector /
per-field `weight` config — deferred to the configurable-search
follow-up (#1428).

Net: `searchFieldMetadata` becomes the source of truth for the *same*
surface as `main`. The only intentional divergences from `main` are
"relabel preserves `name`" (additive) and the deterministic
`position`-ordered `asExpression` (a correctness/perf fix that is
byte-identical to provisioning order, so it does not change the
searchable surface).

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2026-06-23 16:27:13 +02:00
nitin 99e7c2cde0 Improve meeting bot recording tab layout and transcript speakers ui (#22016) 2026-06-23 19:56:01 +05:30
Raphaël Bosi 293ff4c462 Auto-generate app cover images from the app logo (#22011)
<img width="1388" height="858" alt="image"
src="https://github.com/user-attachments/assets/59f16bf7-5908-4624-b3af-51416bbebba3"
/>


## What

When an app is built (`twenty build` / `twenty publish`), the SDK now
generates a marketplace cover image and sets it as the app's screenshot,
but only when the app declares a `logoUrl` and has no `screenshots`. The
cover composites the app's logo and the Twenty logo over the branded
halftone backdrop, matching the design reference.

## Why

Most apps ship a logo but no screenshots, so their marketplace detail
page had no hero visual. This gives them a polished cover for free, with
no per-app design work.

## Notes for reviewers

- Generation lives in the build path (`operations/build.ts`), not
`buildManifest`, so `twenty dev` and the shared manifest builder are
untouched. It is best-effort: on failure it logs a warning and the build
continues.
- The cover is written to `.twenty/output` and registered as a public
asset + screenshot, so the existing copy/checksum/serve pipeline handles
it unchanged. No app source files are modified.
- Adds `sharp` as a runtime dependency of `twenty-sdk` (a build-time
tool, like `esbuild`); it is not bundled into built apps.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22011?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 13:41:19 +00:00
Rashad Karanouh 4f1ffa0a96 fix(twenty-partners): coerce null fields in TFT opportunity import (#22017)
## What

The TFT `HTTP Request` action POSTs `null` for empty fields (e.g.
`amountMicros:null`, `closeDate:null`). The import schema typed those as
`z.number()/z.string().optional()`, which reject `null` (it is not
`undefined`), so the endpoint returned `ok:false / invalid_input` before
any API call.

## Fix

A `dropNulls` preprocessor on the request schema converts `null`
(top-level or nested) to "field absent" before validation. Null optional
fields are simply omitted from the created opportunity; required `name`
still fails correctly if null. No schema-shape or behaviour-contract
change.

## Tests

Added a case feeding the failing payload shape (`amountMicros:null`,
`closeDate:null`) → `created:true` with `amount`/`closeDate` omitted.
42/42 unit pass, lint clean.

Patch bump `1.1.1 → 1.1.2`.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22017?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 17:37:41 +04:00
Raphaël Bosi c52c983b90 Source app About description from README and improve internal app READMEs (#22012)
## What

- The SDK manifest build now sources an app's `aboutDescription` (the
long-form "About" tab content) from its `README.md`. An explicit
`aboutDescription` in the config still wins, matching the existing
marketplace CDN fallback.
- Removed the now-duplicated `aboutDescription` from internal app
configs and deleted the standalone `ABOUT_DESCRIPTION` constant files.
- Rewrote internal app READMEs to read as user-facing About content:
stripped developer/build/source-path noise, and expanded the thin ones.
`call-recording` and `self-hosting` (one-liners over substantial apps)
and `people-data-labs` were rewritten from a close reading of the code;
`twenty-exa` was verified for accuracy.
- Added a unit test (and a fixture README) covering README →
`aboutDescription` in the build.

## Why

The README and the About description were maintained separately and
drifted. Making the README the single source keeps the About tab
accurate and removes duplicated copy.

## Notes for reviewers

- Internal apps depend on the published `twenty-sdk`, so the build
change takes effect for them after an SDK release + dependency bump.
Until then, published apps still get README → `aboutDescription` via the
marketplace CDN sync.
- Standard/Custom app descriptions are unchanged (they are resolved in
the frontend, not via the manifest).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22012?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 15:27:52 +02:00
Abdullah. 6520db22ca fix(deps): bump opentelemetry suite to core 2.8.0 (+ sentry 10.59) (#22010)
## Summary

Bumps the OpenTelemetry suite onto the **`@opentelemetry/core` 2.8.0**
wave (plus Sentry `10.51 → 10.59`, which carries the otel
instrumentation), resolving [Dependabot alert
#1510](https://github.com/twentyhq/twenty/security/dependabot/1510)
(`@opentelemetry/core < 2.8.0`).

## Why a parent-bump, not a `resolutions` entry

The vulnerable `@opentelemetry/core` is transitive, pulled in by the
otel packages we declare (`exporter-metrics-otlp-http`,
`exporter-prometheus`, `sdk-metrics`) **and** by `@sentry/*` (which
bundles `@opentelemetry/instrumentation-*`). The otel **stable**
packages pin `core` to their own exact version and are version-coupled —
forcing `core` ahead of the suite via `resolutions` risks runtime
breakage. So this bumps the declared parents instead.

## Changes

- `twenty-server/package.json`:
  - `@opentelemetry/exporter-metrics-otlp-http` `^0.200.0 → ^0.219.0`
  - `@opentelemetry/exporter-prometheus` `^0.217.0 → ^0.219.0`
  - `@opentelemetry/sdk-metrics` `^2.0.0 → ^2.8.0`
  - `@sentry/{nestjs,node,profiling-node}` `^10.51.0 → ^10.59.0`
- `yarn dedupe` collapses the remaining transitive `core@2.7.1` (caret
consumers) onto `2.8.0` — the whole stable set (`core` / `resources` /
`sdk-trace-base` / `sdk-metrics`) is now `2.8.0`.
- **`@types/pg` added as a direct devDependency.** The newer Sentry
drops the instrumentation that used to *transitively* provide
`@types/pg`; twenty-server imports `pg` directly
(`set-pg-date-type-parser.ts`), so it now declares its own types —
fixing a latent fragility the bump exposed.

## Verification

- `nx typecheck twenty-server` — **0 errors** (validates the otel/sentry
API surface we call is intact).
- `yarn install --immutable` passes.
- No `@opentelemetry/core < 2.8.0` remains.
- Lockfile churn is contained to the observability subtree (otel/sentry
+ their transitive deps; net **−615 lines**).

> Sentry resolved to `10.59.0` rather than the just-published `10.60.0`
due to the repo's `npmMinimalAgeGate`.
> Worth a quick server-boot check during review to confirm Sentry/otel
init at runtime.
2026-06-23 17:32:41 +05:00
Raphaël Bosi 0f451897cf Make twenty-ui theming a consumer-facing API (#22007)
**What**
- Add `useTheme()` and `useThemeColorScheme()` as the public theme
accessors, and migrate the 88 internal `useContext(ThemeContext)` call
sites to them. `ThemeContext` stays exported.
- Make `ThemeProvider` overridable and scopeable: new `applyToRoot`
(default `true`), `overrides` (a `--t-*` map), and `className` props.
When scoping is requested it renders a `display: contents` wrapper that
also serves as the themed portal container, exposed via
`ThemeScopeContext` / `useThemeContainer()`. `AppTooltip` and `Modal`
portal into that container, falling back to `document.body`.
- Document the `--t-*` override contract in the README; barrels
regenerated.

**Why**
Consumers had no stable theme accessor (they reached into the raw
context) and no supported way to re-theme. This adds both without
behavior change.

**Reviewer notes**
- The default path is unchanged: `applyToRoot` defaults to `true`, so
the colorScheme class still lands on `<html>` and portaled overlays
(tooltips, dropdowns, modals) stay themed. The global class is
load-bearing for body portals; scoping is opt-in.
- `twenty-front` is untouched (migrating its consumers is a separate
follow-up).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22007?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 14:16:09 +02:00
Raphaël Bosi 85406a58fb Use minimal babel presets for wyw to fix the website Cloudflare build (#21994)
The website's Cloudflare build (`opennextjs-cloudflare` / Turbopack) was
failing with `_defineProperty is not a function` while linaria/wyw
evaluates `twenty-ui/dist/theme.cjs` at build time. It regressed in
#21946, whose twenty-ui build rework changed the emitted `theme.cjs` so
the theme objects ship as runtime object spreads (`{ ...THEME_COMMON
}`).

**Cause:** wyw evaluates modules in Node through `next/babel`, which
pulls in `preset-env` + `transform-runtime`. Those re-lower the runtime
spreads into `@babel/runtime` helpers imported as ESM; wyw then
`require()`s that ESM module in a CJS context where the export is not
callable, so `_defineProperty` fails.

**Fix:** wyw runs in Node and needs no downleveling, so replace
`next/babel` with minimal presets (`@babel/preset-typescript`,
`@babel/preset-react`, `@wyw-in-js/babel-preset`, plus
`@babel/plugin-transform-export-namespace-from`), matching
twenty-front's wyw config. No `@babel/runtime` helpers get injected.
Kept on the website side so twenty-ui keeps react/react-dom as peer deps
(#21946).

Note: no blocking PR check runs the website production build, so this is
best validated via the website preview build or the twenty-infra deploy.
2026-06-23 13:25:48 +02:00
github-actions[bot] 16c9782c96 i18n - website translations (#22001)
Created by Github action

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

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-23 13:17:15 +02:00
Abdullah. 6a4dc37c6d Organize twenty-website product-feature into folder structure (#21999)
Applies the PipelineVisual folder structure to every other file in
`product-feature`.

- Each feature visual (Import, Tasks, Files, Emails, Dashboard,
Contacts) becomes an `XVisual/` folder: shell + `index.ts` barrel, with
`components/`, `data/`, `types/` (one export per file), and `utils/` as
applicable. `BarChart`/`DonutChart` move into `DashboardVisual`
(exclusive to it); `RecordTabHeader` stays shared.
- The section's non-visual files get the same treatment: `components/`
(Tiles, TileVisual, TileContent, ScrollEntrance, RecordTabHeader),
`data/`, `types/`, `utils/`. `ProductFeature.tsx` stays the section
shell.
- Drop dead code: unused `WindowChrome` and the now-orphaned
`product-feature-scene` token.

No behavior change — `index.ts` barrels keep all import paths stable.
Typecheck, check-conventions, oxlint, and unit tests pass locally.
2026-06-23 13:01:00 +02:00
Etienne 7b45380777 feat(ai): large tool output handling + navigation tools (#21982)
## Summary

Large tool outputs (e.g. a workflow run that serializes to ~70k tokens)
blow the chat context budget and force per-tool "raw" variants. This PR
handles oversized outputs generically in one place:

1. **Producer:** when a tool result exceeds a byte budget, it is spilled
to a `FileFolder.AgentChat` file and replaced with a compact `{ spilled,
outputRef, shape, hint }` envelope.
2. **Consumer:** two bounded, in-server navigation tools —
`extract_json_path` and `search_output` — let the model dig into the
spilled file by `fileId` without spinning up `code_interpreter`.

Together they add a fast, auditable middle tier between "truncated
inline preview" and "full code_interpreter relay," and enable an
enterprise "restricted" mode (spill + navigation, no sandbox).

## Data flow

```mermaid
flowchart TD
  exec["resolveAndExecute / hydrateToolSet closure"] --> compact[compactToolOutput]
  compact --> enabled{"spillLargeOutput enabled? (chat only)"}
  enabled -->|no| inlineRaw["inline raw (MCP, workflow, sandbox bridge)"]
  enabled -->|yes| size{"bytes > MAX_INLINE_TOOL_OUTPUT_BYTES?"}
  size -->|no| inline["inline result"]
  size -->|yes| skeleton["jsonShapeSkeleton + largeOutputHint"]
  skeleton --> write["writeFile(AgentChat)"]
  write --> envelope["return { spilled, outputRef, shape, hint }"]
  envelope --> model[Model]
  model --> nav["extract_json_path / search_output / code_interpreter (by fileId)"]
```

## Part 1 — Navigation tools (consumer)

- `extract_json_path`: extracts a sub-tree from a spilled JSON file by a
JSONPath-lite expression (dot/bracket access, array slicing,
single-level wildcard), with `maxItems`/`maxDepth` bounding. No filters
or recursive descent — those belong to `code_interpreter`.
- `search_output`: grep-like line search with context lines and
stateless `offset` pagination (`{ matches, totalMatches, hasMore }`).
- Both read from `FileFolder.AgentChat` by `fileId`, enforce their own
output byte cap, and are registered in `ActionToolProvider` (always
available; read-only).

## Part 2 — Spill producer

- Spilling slots in right after the existing `compactToolOutput` step at
the two seams in `ToolRegistryService` (`resolveAndExecute` and the
`hydrateToolSet` execute closure).
- `ToolOutputSpillService.spillIfTooLarge()` measures
`Buffer.byteLength`; over `MAX_INLINE_TOOL_OUTPUT_BYTES` (16 KB ≈ 4k
tokens) it writes the full payload and returns the envelope. Spill
failures never block the call (inline + warning).
- `jsonShapeSkeleton` computes a bounded structural map (depth 4, arrays
as `"array[N] of <type>"`, id-keyed maps collapsed, long leaves as size
markers, hard-capped at 1024 bytes) so the model knows the key paths in
one pass.
- Optional per-tool `largeOutputHint` (on the `Tool` type, threaded via
the descriptor) is used as the hint when present, else a generic hint.
The `shape` is always computed generically.

## Surfaces

Spilling is an opt-in flag (`spillLargeOutput`) mirroring
`compactOutput`:

| Surface | `spillLargeOutput` | Behavior |
| --- | --- | --- |
| AI chat / agent | `true` (in `chat-execution.service.ts`) | Spill on;
nav tools + `code_interpreter` in catalog |
| External MCP clients | unset | Raw output |
| Workflow agents | unset | Raw output |
| `code_interpreter` sandbox bridge | unset (it's an MCP call) | Raw
output |

The sandbox bridge inherits "no spill" for free via the MCP path — no
header sniffing, no `ToolContext.source` field.

## Design constraints (anti-micro-OS)

Exactly two navigation tools, no composition/piping, read-only, bounded
output. The boundary is: expressible as a single path lookup or text
search → nav tool; aggregation/correlation/transform →
`code_interpreter`.

## Notes / deviations from the plan

- `jsonShapeSkeleton` and `ToolOutputSpillService` live under the `tool`
module (not `tool-provider/output-transforms`) to avoid a `tool →
tool-provider` import cycle.
- Spill files use `{ isTemporaryFile: false, toDelete: false }` (same as
`code_interpreter`); `isTemporaryFile` here means files-field promotion,
not a TTL.

## Test plan

- [x] `extract-json-path` + `search-output` util unit tests (23 cases)
- [x] `jsonShapeSkeleton` unit tests (6) and `ToolOutputSpillService`
unit tests (4)
- [x] oxlint + oxfmt clean on changed files; `twenty-server` typecheck
clean (pre-existing unrelated errors aside)
- [ ] Manual: trigger an oversized tool result in chat, confirm the
envelope is returned and `extract_json_path` / `search_output` read the
spilled file by `fileId`

## Why no automated e2e

Spilling is chat-only and the chat path runs a live model, so the
black-box MCP integration harness can't deterministically trigger a
spill (MCP intentionally doesn't spill). The seam is small, explicit
flag-threading mirrored on `compactOutput`, covered by the unit suites.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21982?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 10:46:06 +00:00
Parship Chowdhury c18787350f fix: month and year dropdowns in settings logs date picker (#21529)
### Summary
- Fixes #21514 and Issue 2
- **Issue 1**: when opening the calendar and choosing a month or year,
those lists could appear underneath the calendar, making them impossible
to see and use. (Issue #21514)
- **Issue 2**: after opening the calendar icon menu, clicking the month
or year controls don't work, so you couldn’t actually change the month
or year.

Before:
<img width="355" height="434"
alt="607363028-0d3a302e-9dba-4d9a-b354-ad7cbcd1fba5"
src="https://github.com/user-attachments/assets/b1c357d1-a7cf-4572-8737-721cf4e2597a"
/>

After:


https://github.com/user-attachments/assets/ffc26447-ff34-4f11-a3b4-4c329e446ec4



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

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-23 11:40:27 +02:00
Emmanuel Hernández Bazán e1c962acba feat(meeting-bot): configure Recall recording retention hours (#21978)
## Summary

- Sends an explicit Recall.ai recording retention policy when creating
or rescheduling meeting bots.
- Uses the optional server variable
`MEETING_BOT_RECORDING_RETENTION_HOURS` instead of a workspace/app
variable.
- Defaults to `166` hours (6 days and 22 hours), keeping Twenty-hosted
deployments below Recall.ai's 7-day free storage window while still
allowing self-hosters to configure a longer retention period.

## Why

Recall.ai accounts created after June 12, 2025 retain recording media
forever unless retention is configured. Twenty ingests the meeting
artifacts into its own storage, so Recall.ai media retention should be
bounded by default to avoid unnecessary third-party storage cost.

## Changes

- Replaces the days-based app variable with the server variable
`MEETING_BOT_RECORDING_RETENTION_HOURS`.
- Adds a default retention constant of `166` hours.
- Builds `recording_config.retention = { type: 'timed', hours }`
centrally through `getRecallBotRecordingConfig()`.
- Applies the same recording config to both bot creation and bot
rescheduling.
- Documents the server variable and warns that values above `168` hours
may incur Recall.ai storage charges.
- Updates Recall API tests to assert retention is sent and invalid
values fall back to the safe default.

## QA

- [x] `yarn test:unit`
- [x] `yarn lint`
- [x] `yarn exec tsc --noEmit -p tsconfig.spec.json`
- [x] `git diff --check`
- [x] Live Recall.ai bot payload includes `recording_config.retention =
{ type: 'timed', hours: 166 }`

---------

Co-authored-by: Emmanuel Hernandez <emmanuel.hernandez@clickbalance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: ehconitin <nitinkoche03@gmail.com>
2026-06-23 15:07:08 +05:30
Rashad Karanouh 6f50b9d01e fix(twenty-partners): update Partner view in place on upgrade instead of deleting (#21995)
## Why

Upgrading the already-installed `twenty-partners` app in place (0.5.x →
1.x) via `yarn twenty app:install` aborts during the sync reconcile:

```
view:      INVALID_VIEW_DATA: Cannot delete the only view for this object (379b11d5-…)
viewField: INVALID_VIEW_DATA: Label identifier view field cannot be deleted (21afcc69-…)
```

The marketplace-v2 change deleted `all-partners.view.ts`. On an
installed workspace that view is the Partner object's primary view and
holds the **label-identifier** viewField (the `name` column). Twenty's
manifest sync refuses to delete an object's *only* view or a
label-identifier viewField, so the in-place upgrade fails. (Fresh
installs are unaffected; only upgrades from a version that had
`all-partners` hit this.)

## What

Repurpose the retired `all-partners` identity for `partners-validated`
so the sync performs an **update in place** instead of a delete:

- `partners-validated.view.ts` now uses the old view id `379b11d5-…`,
and its `name` column reuses the old label viewField id `21afcc69-…`.
- Remove the now-dangling `ALL_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER`
constant (its file was already gone).
- Patch bump `1.1.0` → `1.1.1`.

The resulting view is the intended "Partners Validated"; the other
retired Partner view (`validated-partners`) deletes cleanly because the
object keeps other views.

## Revision

**Patch** — migration bugfix, no new behaviour.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21995?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 11:33:11 +02:00
Etienne 4789ba6265 feat(ai): add AI tools to list and inspect workflow runs (#21983)
- Add `get_workflow_run` and `list_workflow_runs` AI tools so the
workflow agent can troubleshoot failed or misbehaving workflow runs —
listing runs with optional filters (workflow, status, limit) and
inspecting a specific run's steps, errors, and failed step logs.
- Enforce `rolePermissionConfig` on all three read tools
(`get_workflow_run`, `list_workflow_runs`,
`get_workflow_current_version`) instead of bypassing permission checks,
consistent with how `create_complete_workflow` and database CRUD tools
work.
- Add unit tests for the three tools covering permission forwarding,
success paths, and error paths.

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

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-23 11:19:42 +02:00
avonian 47b48d83f7 fix(front): include isUIEditable/isRemote in CreateOneObjectMetadataItem so new objects aren't read-only until refresh (#21796)
## Problem

After creating a custom object in **Settings → Data Model**, the new
object's **"New Field"** (and **"Add relation"**) buttons are missing.
They only appear after a hard page refresh.

## Root cause

`CreateOneObjectMetadataItem`
(`packages/twenty-front/src/modules/object-metadata/graphql/mutations.ts`)
selected only a subset of object-level fields and omitted
`isUIEditable`, `isRemote`, `isSystem`, `isUICreatable`,
`universalIdentifier`, `shortcut`, and `duplicateCriteria` — all of
which are present in the shared `ObjectMetadataFields` fragment used by
the bootstrap query.

`useCreateOneObjectMetadataItem` writes the mutation response into the
metadata store via `addToDraft`. Because the mutation resolves *after*
the SSE create event and `addToDraft` replaces entries by `id`, the
reduced mutation response overwrites the fuller record that arrived over
SSE. The stored object then has `isUIEditable === undefined`, so
`isObjectMetadataReadOnly` returns `true` (`!undefined`), and
`ObjectFields` hides the action buttons via its `{!readonly && …}`
guard.

A hard refresh "fixes" it only because the bootstrap query repopulates
the store from `ObjectMetadataFields`, which includes the missing
fields.

## Fix

Add the missing object-level fields to the `CreateOneObjectMetadataItem`
selection so a newly created object matches the bootstrap shape, and
regenerate the metadata GraphQL types. No other code changes required.

## How to test

1. Go to **Settings → Data Model** and create a new custom object.
2. Open the new object's **Fields** tab.
3.  The **"New Field"** button is visible immediately — no refresh
needed.

Before this change, the button was hidden until a manual refresh.

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


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21796?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 10:46:16 +02:00
Félix Malfait 0e6d96bb5e fix(workflow): stop trigger/action filter Conditions from flashing on edit (#21952)
## Problem

Adding a condition to a database-event **trigger** (the new Conditions
section), the **Filter** action, or the **If/Else** action causes the
just-added condition to flash out and back in.

## Root cause

`WorkflowEditActionFilterBodyEffect` seeds the builder's local jotai
atoms from the persisted `defaultValue` through an effect that
**resynced whenever the live atoms differed from `defaultValue`** (the
atoms were in the effect deps and the equality check compared atoms vs
`defaultValue`).

A local edit writes the atoms **synchronously**, then persists through
an **async** mutation — and for an *active* workflow that mutation first
creates a draft version over the network. During that window the atoms
are ahead of the still-stale `defaultValue`, so the effect treated it as
"out of sync" and overwrote the edit back to the stale value, then wrote
it again once the save landed. That round-trip is the flash.

The resync existed for a real reason: the atoms are module-cached per
`instanceId` and persist across mounts, and the trigger shares a single
**constant** `instanceId` (`'trigger'`), so a previous trigger's filters
must be overwritten when a different one is opened. (This is also why
the `?? { stepFilterGroups: [], stepFilters: [] }` fallback was added in
#21868 — to reset builder state deterministically between trigger
edits.) So a naive "init-once" fix would reintroduce that stale-state
leak.

## Fix

Resync from `defaultValue` **only when `defaultValue` itself changes**,
tracked via the last-synced value in `useState` (not the live atoms).
This:

- never clobbers an in-flight local edit → no flash;
- still re-seeds when switching the trigger/action being edited → no
stale-state leak;
- preserves reflecting genuine external `defaultValue` changes.

The `hasInitialized*` flags are no longer needed and are removed (along
with the now-unused `stepId` prop on the effect).

## Tests

Adds a regression test covering: seeding from `defaultValue` on mount,
the **no-clobber-while-stale** invariant (the flash), and resync on a
genuine `defaultValue` change. Verified the no-clobber test **fails**
against the old "resync against live atoms" behavior and passes with the
fix.

## Verification

- `nx typecheck twenty-front` 
- `nx lint:diff-with-main twenty-front`  (0 warnings / 0 errors)
- New unit test: 3 passing 

## Known residual / follow-up

On an *active* workflow, the first edit creates a draft version over the
network; making a second edit before that round-trip completes leaves a
narrow window where the optimistic echo of the first value could
momentarily win. Far narrower than the current flash-on-every-edit.
Eliminating it entirely (and resolving the still-open HIGH-severity
"constant `instanceId`" review flag from #21868) would mean giving the
trigger a unique `instanceId` per workflow version + a React `key` to
reset on remount — proposed as a separate, scoped follow-up.

https://claude.ai/code/session_01XnjtzFepMJX2VFwnQVcQbV

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21952?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 10:44:56 +02:00
neo773 9e31ffdf68 feat(messaging): webhook push sync for Gmail, Calendar and Microsoft (#21970)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21970?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 14:04:16 +05:30
nitin c7ad1ff8ee Route Recall webhooks by workspace metadata (#21991)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21991?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 10:27:52 +02:00
Abdullah. c98620e14c Rework website PipelineVisual to match twenty-front kanban (#21989)
Reworked the product page's `PipelineVisual` to match twenty-front's
kanban — board header, card layout/fields, hover states, and font
weights.

Also loaded and pinned Inter (twenty-front's product font) for the
app-preview mockups, which previously fell back to the system font.

<img width="879" height="625" alt="image"
src="https://github.com/user-attachments/assets/396f0a90-114b-47f9-92a6-2e74d732a91e"
/>
2026-06-23 12:34:56 +05:00
Rashad Karanouh 50c4659cae feat(twenty-partners): import an opportunity from TFT via manual workflow (#21979)
## What

Adds a one-way, manual copy of a single Opportunity from the
**twentyfortwenty (TFT)** workspace into **partners**. No automatic/echo
sync — one record per button press.

A TFT-side **manual Workflow** (a "Run workflow" button on the
Opportunity record) → **HTTP Request** action → `POST /s/opportunities`
on the partners app. The new `import-opportunity-from-tft` logic
function:

- **Shared-secret guard** on the `x-application-secret` header vs the
existing `PARTNER_APPLICATION_SECRET` app variable (the SDK's
`isAuthRequired` only accepts user JWTs, not API keys — same pattern as
`submit-partner-application`).
- **Idempotent on `tftOpportunityId`** (a field the partners Opportunity
object already has); falls back to `name` for manual calls. Re-press →
no duplicate.
- **Find-or-create** the Company (by name) and the point-of-contact
Person (by primary email), then `createOpportunity` with `name / amount
/ closeDate / stage / companyId / pointOfContactId`.

## Out of scope

- The **TFT-side workflow is a UI step** (built once in the TFT
workspace) — it can't live in this repo. The exact HTTP action config +
body mapping lives in the workflow itself.
- Owner/workspace-member copy and stage-enum remapping are intentionally
skipped (YAGNI).

## Files

- `src/logic-functions/import-opportunity-from-tft.logic-function.ts` —
the handler + manifest.
- `src/logic-functions/__tests__/import-opportunity-from-tft.test.ts` —
unit tests (auth reject / idempotent / mapped create).
- `package.json` — version bump **0.5.5 → 0.6.0** (minor; new feature).

## Verification (local bundle)

- `yarn test:unit` 3/3 · `yarn lint` 0/0 · `yarn twenty dev --once` →
`created logicFunction import-opportunity-from-tft`, no manifest
warnings.
- Live `POST /s/opportunities` with the secret → `201
{ok:true,created:true,id}` (confirms the app role can create Opportunity
+ Company + Person). Re-POST same `tftOpportunityId` → `created:false`.
Wrong secret → `unauthorized`.

## Deploy note

Additive (new logic function + HTTP route) — upgrades cleanly with
`deploy` + `install`. `PARTNER_APPLICATION_SECRET` is already set on
prod, so no new application variable to configure.
2026-06-23 08:47:16 +02:00
Priyanshu Bartwal 6e7d8ef96c [Twenty-front]: Record table Header drag and drop functionality (#21304)
Closes #21303 and
https://github.com/twentyhq/core-team-issues/issues/151




https://github.com/user-attachments/assets/45cee1be-464f-467e-a1c0-cf5354ff87db

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-23 08:37:04 +02:00
Félix Malfait 46642c81c9 fix(front): unblock email verification on the central domain (blank modal) (#21980)
## Problem

After clicking the email-verification link on the central domain (e.g.
`app.twenty.com/verify-email?...`), a new user is left staring at a
**blank white auth modal** and onboarding never continues. The email is
actually verified — the user is just never moved off the verify-email
page.

## Root cause

`VerifyEmailEffect` (mounted on `/verify-email`) handles the
central/workspace‑agnostic domain like this:

```tsx
if (!isOnAWorkspace) {
  await verifyEmailAndGetWorkspaceAgnosticToken(emailVerificationToken, email);
  return enqueueSuccessSnackBar(successSnackbarParams);
}
```

It renders nothing of its own in this branch (`return <></>`) and relies
entirely on the auth hook to navigate.

The onboarding workspace-creation refactor (**#21641** "Let users pick
their workspace subdomain during sign-up", refined by **#21723**)
changed `navigateAfterMultiWorkspaceSignInUp`:

- **Before:** a user with `0` workspaces was sent through
`createWorkspace()`, which created the workspace and **redirected to the
workspace subdomain** — navigating away from `/verify-email`.
- **After:** for multi-workspace it now only does
`setSignInUpStep(SignInUpStep.WorkspaceCreation)` (the new
name/subdomain/logo form) — **no navigation**.

`signInUpStepState` is read **only by the `SignInUp` page**
(`/sign-in-up`), which renders `SignInUpWorkspaceCreationForm` for that
step. But the user is on `/verify-email`, whose route renders only
`VerifyEmailEffect` — which knows nothing about the step state and
returns an empty fragment. Nothing bridges the gap
(`usePageChangeEffectNavigateLocation` also won't redirect, because
`/verify-email` is whitelisted in `ONGOING_USER_CREATION_PATHS`), so the
user is stuck on an empty modal.

### Scope of the breakage
- **Broken:** new user, multi-workspace instance (Twenty Cloud central
domain), email verification enabled, signing up to create a workspace
(`0` workspaces). The `2+`-workspaces case (`WorkspaceSelection`) is the
same.
- **Not affected:** the single existing-workspace case (still does a
real `redirectToWorkspaceDomain`), the workspace-subdomain verification
path (`verifyEmailAndGetLoginToken` → `verifyLoginToken`), and
single-workspace self-host.

## Fix

After a successful workspace-agnostic verification, hand off to the
`SignInUp` page so it mounts and renders whatever step the hook just
set:

```tsx
if (!isOnAWorkspace) {
  await verifyEmailAndGetWorkspaceAgnosticToken(emailVerificationToken, email);
  enqueueSuccessSnackBar(successSnackbarParams);
  return navigate(AppPath.SignInUp);
}
```

This is intentionally scoped to `VerifyEmailEffect` (the only entry
point that lives on a route which doesn't host the sign-in-up step UI).
The in-app sign-in/sign-up callers of
`navigateAfterMultiWorkspaceSignInUp` are already on `/sign-in-up`, so
they're untouched — keeping their query params (invite tokens, billing
checkout, returnToPath) intact. For the single existing-workspace edge
case, the hook's redirect still wins.

## Testing

- New `VerifyEmailEffect.test.tsx`:
- central-domain success → navigates to `AppPath.SignInUp` + shows the
success snackbar;
- failure → does **not** hand off to `SignInUp` (error state is shown);
- workspace subdomain → workspace-scoped path is untouched (no
workspace-agnostic call, no `SignInUp` hand-off).
- `nx typecheck twenty-front` , `oxlint --type-aware` + `oxfmt` on
changed files .

https://claude.ai/code/session_017oVwW12hC42RdCgSKK8dFP

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21980?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 08:19:33 +02:00
Rashad Karanouh 24533b510c feat(twenty-partners): marketplace v2 — Application-driven matching workspace (#21816)
## Summary

Restructures the Twenty Partners app into an **Application-driven
matching workspace**: leads post briefs (Opportunities), partners browse
and **self-apply**, admins review applications and assign a winner. The
candidacy funnel lives on `Application.state`; the deal lifecycle lives
on the stock Opportunity `stage`.

Version **1.0.0** — **breaking**: removes the legacy `matchStatus` field
and the auto-match flow. Prod upgrade path is **uninstall → deploy →
install** (not an in-place upgrade).

## How "Apply" works — no workflow, no special permission

Partners apply by **creating an Application directly** from a listed
brief (a normal record write, governed by the Application object
permission). The `on-application-created` logic function (shipped in the
manifest) then resolves the partner from `createdBy`, sets `state =
APPLIED`, stamps `partner`/`partnerUser`/`lastActivityAt`, and dedupes
by (opportunity, partner).

- **No `WORKFLOWS` permission flag.** An earlier iteration used a manual
"Apply" workflow, but running a manual workflow requires the `WORKFLOWS`
flag, which **cannot be granted on an app-owned role** (the manifest
sync drops `role → permissionFlag` links, and the metadata API rejects
out-of-band grants on app roles). Self-apply via record-create sidesteps
this entirely and is prod-viable as-is.
- `Application.state` **defaults to `APPLIED`** so a partner never sees
a misleading "Invited" flicker while the async handler runs. Admin
invites set `INVITED` explicitly.

## ⚠️ Manual setup after install (per workspace)

1. **`yarn rls:configure`** — applies the partner row-level predicates
and verifies field-locks (predicates can't ship in the manifest).
Required for partner scoping.
2. **"Mark as Winner" workflow** — one manual-trigger workflow on
**Application** → *Update Record* that sets `Opportunity.partner`, which
drives the WON/BACKUP cascade. Admins run it (admins bypass the flag via
`canUpdateAllSettings`); equivalent to editing the Opportunity's
`partner` field directly. Steps in `src/workflows/README.md` (the
**Apply** section there is superseded by self-apply).

## What's included

- **Data model:** new `BACKUP` Application state; symmetric cascade
owned by `on-opportunity-partner-won` — assign → winner `WON`, other
applicants `BACKUP`; unassign → all reopen to `APPLIED`.
`Opportunity.partner` is the single source of truth.
- **Removed:** `matchStatus` field + `on-opportunity-auto-match` (dead).
Deal lifecycle now on the stock `stage`.
- **Partner row-level security (B7):** RLS predicates scope partners to
their own `Partner`/`Person`/`Company`/`Application` rows; `Opportunity`
is `(partnerUser IS me) OR (isListed = true)` so listed briefs are
visible to all partners; `Application` is `(partnerUser IS me) OR
(lastActivityAt IS EMPTY)` — the IS-EMPTY branch lets a partner's own
insert pass (partnerUser is stamped just after insert). Field-locks make
Opportunity `stage`/`amount` and most Application fields read-only for
partners (pitch stays editable). Applied via `yarn rls:configure`.
- *Trade-off:* an unstamped application (lastActivityAt null) is briefly
readable by any partner — sub-second window, permanent only if the
handler fails to stamp. Acceptable for an internal marketplace; the
front-component Apply path (below) would remove it.
- **Idempotency:** `on-application-created` dedupes duplicate
applications by (opportunity, partner).
- **Views & navigation**, reorganized into sections:
- **Partner Workspace:** Open Briefs · My Applications · My Profile · My
Deals
- **Matching Admin:** Briefs to Match · Deals (board) · Applications ·
Applications by Opportunity · All Opportunities · Follow-up Applications
· Follow-up Briefs
- **Partners:** per Stage · per Country · Partner Applications ·
Validated (per-group COUNT)
- Opportunity & Partner record **side panels** via FIELDS_WIDGET views
(surface relations incl. `applications`, so a brief shows all its
applications).

## Known issues / follow-ups

- **Pre-existing failing unit test (not introduced here):**
`on-partner-application-created › "posts a Discord embed when an
APPLICATION-sourced partner is created"` — the handler/test are
byte-identical to base; tracked separately.
- **Follow-up views** lack the "older than 7 days" staleness filter — no
confirmed relative date operand in this Twenty version (TODOs left in
the views).
- **Apply UX (future):** a front-component "Apply" button on the brief,
calling an authenticated `/s/apply-to-brief` logic function (runs as the
app), would replace the "create a record" entry point — nicer UX, and it
removes the RLS IS-EMPTY trade-off. Not required to ship.

## Testing

- Partner self-apply verified end-to-end as a partner (create
application from a brief → lands on `APPLIED`).
- Unit tests for the WON/BACKUP cascade + application handlers pass (the
one failing test above is the pre-existing, unrelated Discord handler).
- Lint clean (`oxlint`).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21816?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-light.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 05:53:00 +00:00
neo773 242b989c0e fix(emails): stop reply composer infinite re-render (#21935)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21935?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 00:57:54 +02:00
Paul Rastoin a5aac3c21e Logic function handler name hardened validation (#21956)
# Introduction
Introduce centralized handlerName validation for the logic function
handlerName inside the flat logic function validator

Even if not safe by definition, avoid string interpolation inside the
local driver executor when retrieving the handler name from the parent
module

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21956?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 00:55:42 +02:00
Charles Bochet a884945aca fix(deps): remediate HIGH image vulns (multer, ws, nodemailer) (#21984)
## What

Clears the HIGH-severity AWS Inspector findings on the `twenty-server`
container image. All three have stable, in-range fixes — no prereleases.

| Package | From → To | CVE | Path |
|---------|-----------|-----|------|
| multer | 2.1.1 → **2.2.0** (resolution) | CVE-2026-5038, CVE-2026-5079
(DoS) | transitive via `@nestjs/platform-express` |
| ws | 8.20.1 → **8.21.0** (resolution) | CVE-2026-48779 | pinned by
`@nestjs/graphql` (8.21.0 already in tree) |
| nodemailer | 8.0.10 → **9.0.1** | GHSA-p6gq-j5cr-w38f | nested in
`imapflow`; bumped `imapflow` 1.3.6 → 1.4.2 which depends on nodemailer
9.0.1 |

## Notes

- **multer 2.2.0 is the stable fix.** The advisories
([CVE-2026-5038](https://advisories.gitlab.com/npm/multer/CVE-2026-5038/),
[CVE-2026-5079](https://advisories.gitlab.com/npm/multer/CVE-2026-5079/))
list both `2.2.0` and `3.0.0-alpha.2` as fixed; Inspector reported only
the `3.0.0-alpha.2` prerelease, but we stay on the stable 2.x line.
- **nodemailer:** the top-level dep was already `^9.0.1`; only
`imapflow`'s nested copy was stale. imapflow 1.4.0 still ships
nodemailer 8.0.10 and 1.4.1 ships 9.0.0 (< the 9.0.1 fix), so **1.4.2 is
the minimum** that pulls the patched nodemailer.
- Lockfile-only resolution for multer/ws (they're transitive); imapflow
is a direct dep bump. yarn.lock net-shrinks from deduping.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21984?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 00:54:46 +02:00
martmull 1646bdf35e Add twenty-partners on internal ci apps (#21975)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21975?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-22 21:08:40 +02:00
mfamularopsyc d2083e7a1b Set OpenAI Responses store false for AI chat and agents (#20888)
## Summary

This PR sets `openai.store = false` for Twenty's `@ai-sdk/openai` AI
calls.

This follows the approach discussed in #20877: instead of adding a new
Twenty-specific Zero Data Retention config variable, OpenAI Responses
calls no longer rely on OpenAI-stored response/item references. This
should help Zero Data Retention organizations and may also avoid stale
persisted-item replay errors for non-ZDR OpenAI users.

Changes included:

- Adds a shared OpenAI provider-options helper that merges `openai.store
= false` for `@ai-sdk/openai` models.
- Applies the helper to AI chat `streamText` calls.
- Applies the helper to workflow/agent `generateText` calls.
- Preserves OpenAI encrypted reasoning metadata through DB/UI message
mappers so reasoning context can be replayed without stored OpenAI item
references.
- Does not add a new env/config variable.

Related to issue #20877.

## Behavior / Tradeoffs

This changes OpenAI Responses behavior for all Twenty OpenAI users, not
only ZDR users.

The intended benefit is that Twenty no longer depends on OpenAI-stored
response/item references. The main tradeoff is reduced provider-side
item-reference reuse for non-ZDR OpenAI users.

To reduce the impact for reasoning models, this PR preserves
`providerMetadata.openai.reasoningEncryptedContent` through message
persistence/replay so reasoning context can still be provided without
stored OpenAI item references.

## Tests

- Focused server Jest tests for OpenAI provider-options merging and
reasoning metadata mapping.
- Focused frontend Jest test for reasoning metadata mapping.
- `oxlint` and `oxfmt --check` on changed files.
- `git diff --check`.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-22 17:26:02 +00:00
martmull 2dc6034a6c Add twenty meeting bot to internal application ci (#21974)
- remove useless github-connector (validated with @charlesBochet)
-  add twenty-meeting-bot to internal apps
2026-06-22 18:47:30 +02:00
Raphaël Bosi 5f22908588 Decouple twenty-ui Avatar from app server-URL config (#21968)
Makes `twenty-ui`'s `Avatar` render the `avatarUrl` it receives instead
of building it from `window._env_`/`window.location` at module load, so
the library no longer depends on the app environment. URL resolution
moves to `twenty-front` via a `getAbsoluteImageUrl` helper applied at
the call sites.

Part of making twenty-ui a standalone library.
2026-06-22 18:38:00 +02:00
twenty-pr[bot] e59e102448 chore: bump version to 2.16.0 (#21973)
## 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/21973?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-06-22 18:22:41 +02:00
Marie 96a2987610 Fix/sanitize chart filters on save (#21958)
# Fix: sanitize chart filters referencing deactivated/deleted fields

## Summary

When a field used in a chart (graph) widget filter was later
**deactivated or deleted**, saving the page layout failed with a backend
error such as:

> Chart "...": One of the chart filters uses "...", but it was deleted.
Please remove or replace this filter rule.

This happened even after the user tried to remove the offending filter
rule, because the invalid filter could still end up in the saved
configuration. This PR makes invalid chart filters get cleaned up
reliably — both as the user edits filters and, as a safety net, at save
time.

## Root causes

- **Edit-time persistence kept invalid filters.** `handleFiltersUpdate`
persisted the current filter state to the page layout draft without
sanitizing it against the object's active fields. An invalid filter
(referencing a deactivated/deleted field) was re-saved on every update,
blocking the configuration from being accepted.
- **Save never enforced the cleanup.** `useSavePageLayout` serialized
the draft as-is. The "filters referencing deactivated/deleted fields
will be automatically removed on save" promise shown in the warning
banner was only honored reactively (when the filter panel was actively
edited), never at the actual save boundary. A chart whose filter panel
wasn't touched kept its stale invalid filter in the payload.
- **Query time treated inactive fields as valid.**
`useGraphWidgetQueryCommon` considered all fields (including inactive
ones) valid, so deactivated-field filters were never dropped when
running the chart query.

## Changes

### Edit-time (keeps draft and UI in sync as you edit)
- `ChartFiltersSettings` — sanitize filters in `handleFiltersUpdate`
before writing to the draft, dropping any filter whose `fieldMetadataId`
is not in the active-fields set.
- `dropChartRecordFiltersWithDeletedFields` — enhanced to also clean up
filter groups left orphaned once invalid filters are removed
(iteratively removing empty groups and re-parenting checks).
- `useGraphWidgetQueryCommon` — restrict valid field IDs to `isActive`
fields so deactivated-field filters are silently dropped at query
execution.
- `ChartFiltersDeletedFieldsWarning` — updated copy to mention both
deactivated and deleted fields.

### Save-time safety net (guarantees no invalid filter is ever
persisted)
- New `sanitizeChartFiltersInPageLayoutDraft` util — walks every chart
widget in the draft and drops record filters (and now-orphaned groups)
whose `fieldMetadataId` is not in the widget object's set of active
fields. It leaves non-chart widgets untouched and leaves filters intact
when the object metadata can't be resolved (avoids wiping valid filters
during metadata loading).
- `useSavePageLayout` — builds a `Map<objectMetadataId,
Set<activeFieldId>>` from `useObjectMetadataItems()` and sanitizes the
draft before converting it to the update input.

This layer only ever removes filters whose field is genuinely
deactivated/deleted — the exact set the backend rejects — and never
removes filters pointing at valid fields.

## Tests

- `dropChartRecordFiltersWithDeletedFields.test.ts` — extended coverage
for orphaned filter-group cleanup.
- `sanitizeChartFiltersInPageLayoutDraft.test.ts` — new: drops
deactivated/deleted-field filters on save, keeps valid filters, cleans
up orphaned groups, leaves non-chart widgets alone, and leaves filters
untouched when object metadata is unresolved.


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

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-22 18:15:33 +02:00
Thomas des Francs 6ce7d04f9d Fix field widget textarea focus reset (#21959)
Short description: Keeps the field widget textarea on a local draft
value while focused so record-store rewrites do not reset the active
caret.

# Before

Typing in an editor-mode text field can lose caret position when the
global record store is rewritten by an external record update/refetch.


https://github.com/user-attachments/assets/1ee7a819-2c27-4d09-aae5-814c2cf27181


# After

The focused textarea should preserve the in-progress draft and caret
while still updating sibling previews optimistically and flushing the
final value on blur.


https://github.com/user-attachments/assets/41832493-021f-46e8-bc75-722bbb1cd7b7



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21959?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-22 17:45:49 +02:00
Charles Bochet b1ada79d72 fix(front): scope relation table widget via currentRecordId (#21965)
## Context

Follow-up to #21293 (merged). That PR added a bespoke
`relationTableFilter` to keep a relation field rendered as a
record-table widget scoped to the host record. It turns out to be
redundant.

## Why it's redundant

The relation table widget's view already carries an
`isCurrentRecordSelected` relation filter on the inverse field — it's
baked in by `useAddDraftViewForFieldRelationTableWidget` when the widget
is configured. That filter is resolved through the `currentRecordId`
that `FieldWidgetRelationTable` provides via
`RecordFilterValueDependenciesContext`, and
`turnRecordFilterIntoGqlOperationFilter` turns it into exactly `{
`${inverseField}Id`: { in: [recordId] } }`.

So the hand-built `relationTableFilter` duplicated a filter the existing
mechanism already produces from `currentRecordId`.

## Changes

- Remove `relationTableFilter` from
`RecordFilterValueDependenciesContext`
- Stop reading/applying it in `useFindManyRecordIndexTableParams` and
`useAggregateRecordsForRecordTableColumnFooter`
- Delete the `getRelationTableFilter` util and its test
- `FieldWidgetRelationTable` provides `currentRecordId` only

Net −263 lines; relies on the existing `isCurrentRecordSelected` +
`currentRecordId` scoping path.

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