Commit Graph

13048 Commits

Author SHA1 Message Date
Rashad Karanouh eb53dee3be fix(twenty-partners): opportunity stage constants + Partners per Stage table (v1.1.9) (#22052)
## Summary

Two related cleanup items for the partners workspace:

### 1. Opportunity `stage` field — shared constant + reference catalog

- Adds `src/constants/opportunity-stage-options.ts` exporting
`OPPORTUNITY_STAGE_FIELD_UNIVERSAL_IDENTIFIER` and an
`OPPORTUNITY_STAGE_OPTIONS` catalog (stock stages + **Done** /
**Dead**).
- **`deals-board.view.ts`** imports the shared field id instead of
inlining `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`.
- The catalog is **not** synced by the app manifest — it documents
planned option ids for scripts/reference only.

**Done / Dead on prod:** added manually in Settings → Objects →
Opportunity → Stage (not via app sync). `defineField` extension of the
standard `stage` field was attempted and rejected
(`FIELD_ALREADY_EXISTS`). Deals kanban groups in the manifest stay
NEW–CUSTOMER only; Done/Dead columns appear when the workspace has those
options.

**Prod data cleanup (outside this PR):** all opportunities moved to
**Done** except `ed95573c-cbe6-4dcc-a459-5369a7449636` (Aranya CRM
Migration, kept in **New**).

### 2. Partners per Stage view — TABLE with stage grouping

Replaces the old KANBAN board with a **grouped TABLE** (same pattern as
Partners per Country / Applications):

- **Type:** `TABLE` (was `KANBAN`)
- **Group by:** `validationStage` — Application, Potential, Validated,
Former, Rejected
- **Columns:** Name, Country, Categories (`partnerScope`), Tier
(`partnerTier`)
- **Nav icon:** `IconTable`

View / view-field / group universal ids were aligned to prod after
KANBAN→TABLE install recreated the view (Twenty cannot change view type
in place).

## Version

**`1.1.9`** in this branch. Pre-merge deploys to `partner-twenty-com`
went through **1.1.5 → 1.1.11** while iterating on the view; prod may be
ahead of this branch’s pinned view id — one more id-alignment install
after merge may be needed if nav doesn’t land on the grouped table.

## Files touched (twenty-partners only)

| File | Change |
|---|---|
| `src/constants/opportunity-stage-options.ts` | New — stage field id +
option catalog |
| `src/views/deals-board.view.ts` | Import shared stage field constant |
| `src/views/partners-per-stage.view.ts` | KANBAN → grouped TABLE +
columns |
| `src/navigation-menu-items/partners-per-stage.navigation-menu-item.ts`
| Icon → `IconTable` |
| `package.json` | **1.1.9** |

## Test plan

- [x] `yarn lint` in `packages/twenty-apps/internal/twenty-partners` —
0/0
- [x] Prod: Done/Dead stage options present; Deals kanban shows 7
columns, no duplicates
- [x] Prod: Partners per Stage — TABLE grouped by validation stage with
Name / Country / Categories / Tier
- [x] Prod: Opportunity cleanup — 20 → Done, 1 stays New (Aranya CRM
Migration)
- [ ] After merge + install on fresh workspace: Partners per Stage
grouping renders without manual view fixes
2026-06-24 11:52:23 +02:00
Weiko b7850a6c64 feat(metadata): deterministic universalIdentifiers for server-generated side-effects (#21949)
## Context

Server-generated "side-effect" entities created for every object (system
fields, INDEX view, record-page fields view + view fields, search-vector
index, navigation command, record page layout/tabs/widgets) were minted
with random v4() ids. Because they were non-deterministic, nothing could
reference them by id (e.g. point a view field at an object's createdAt
field).

This PR introduces a single shared rule for deriving these ids
deterministically via uuid v5, so the same (owner app, parent, kind)
always yields the same id, making side-effects referable and
reproducible.

This is the **forward-only foundation** (PR1). Follow-ups:
- PR2: SDK with optional universalIdentifier + expose helpers to app
authors.
- PR3: regenerate the standard-app constants to the same scheme +
workspace backfill.

## The rule
```ts
universalIdentifier = computeOwnerScopedUniversalIdentifier({ ownerAppUID, namespace, value })
                    = v5(value, v5(ownerAppUID, ENTITY_TYPE_NAMESPACE))

value = `${parentUID}:${discriminator}`   // entity scoped under a parent
      = `${discriminator}`                // top-level, app-parented entity
```
- ownerAppUID: The application that owns the entity (already threaded
through every generator as applicationUniversalIdentifier); folded into
the namespace so it both owns and scopes
the id — two apps adding the same-named entity to a shared parent never
collide.
- namespace: Per entity type (ENTITY_TYPE_NAMESPACE_BY_TYPE), so
different types with the same parent+discriminator never collide.
- parentUID: The immediate parent's actual universalIdentifier (omitted
for top-level entities, since the owner app already scopes them).
- discriminator: A stable semantic key (field name, tab/widget title,
generated index name, select-option value, …).

Scope boundary: deterministic v5 applies to system side-effects (unique
by construction) and, later, app-authored manifest entities (uniqueness
enforced at SDK build time).
Entities created through the UI by the workspace "Custom" app (custom
objects/views/fields) keep v4, their natural keys aren't unique and
aren't enforced. A UI-created custom object keeps its v4 id; its
side-effects are deterministic relative to that v4 parent.

Changes

twenty-shared: new application/deterministic-identifier/ module:
- computeDeterministicUuid(value, namespace) primitive + a thin
computeOwnerScopedUniversalIdentifier wrapper (boilerplate only), and
frozen ENTITY_TYPE_NAMESPACE_BY_TYPE.
- One self-contained util per usecase (no central registry, no generic
engine): each util bakes in its own discriminator + namespace, so a key
lives next to the code that uses it and is individually testable. ~28
utils covering side-effect and (future) app-authored entities, e.g.
getFieldUniversalIdentifier, getIndexViewUniversalIdentifier,
getFieldsWidgetViewUniversalIdentifier, getViewFieldUniversalIdentifier,
getIndexUniversalIdentifier, getRecordPageLayoutUniversalIdentifier,
getPageLayoutTab/WidgetUniversalIdentifier,
getNavigationCommandUniversalIdentifier, plus the general
getViewUniversalIdentifier / getPageLayoutUniversalIdentifier and
app-authored
getObject/Role/PermissionFlag/Agent/Skill/…UniversalIdentifier.
- Golden snapshot test locking every util's output for fixed inputs,
plus a cross-type no-collision test.

twenty-server: side-effect generators now derive universalIdentifier via
the helpers (local id PKs stay v4()): system fields + name, INDEX view,
record-page fields (fields-widget) view, default view fields,
search-vector index, nav command, page layout/tabs/widgets. Index ids
key off the generated Postgres index name; extracted
computeFlatIndexNameOrThrow so the name (and therefore the id) is
computed once with no placeholder.

## Timeline

### What actually changes

- New objects (custom objects created via Settings/metadata API) and
fresh standard installs now get deterministic v5 universalIdentifiers
for all side-effect entities (system fields,
views, view fields, search index, nav command, page layout/tabs/widgets)
instead of random v4().
- The nav-command id formula changed (new owner-scoped) for new objects,
fresh standard installs, and the runtime lookup.

### What does NOT change

- Existing objects' side-effect ids — untouched (no migration;
forward-only).
- Standard object UIDs — untouched
- UI-created custom entities' own ids stay v4 (see scope boundary
above).
- Fresh installs are behaviorally a no-op — ids are internal; re-sync
produces no diff (verified). Nothing user-visible.

### The one real-world impact / risk (existing workspaces)

The nav-command runtime lookup (findNavigationCommandMenuItemForObject)
now computes the new formula, but existing workspaces' nav commands were
stored with the old formula. So on an upgraded existing workspace, until
the PR3 backfill:
- Object activate/deactivate toggle for existing objects won't find the
nav command → re-activating can create a duplicate nav command;
deactivating may no-op.
- Object deletion won't find/clean up the old nav command → orphaned
nav-command row.

### What app developers get right now

Nothing usable yet. The helpers exist in twenty-shared but aren't
re-exported from twenty-sdk (PR2), and app-authored objects still get
SDK-derived ids in the old format until PR2
re-mints them. So "reference a server entity by deterministic id"
doesn't work end-to-end until PR2
2026-06-24 11:47:44 +02:00
martmull 21c3574f05 docs(apps): add key-value store guide for logic functions (#22061)
## What

Adds a new docs page under **Developers → Extend → Apps → Logic**
explaining how to give logic functions key-value storage (to persist
intermediate results, cache data, and share state across runs).

Rather than introducing a dedicated storage primitive, the guide shows
how to achieve this with a small **technical object** ("KV Store") that
has a unique `key` field and a `RAW_JSON` `value` field — queried
through the existing typed `CoreApiClient`.

Closes the documentation part of
[core-team-issues#2427](https://github.com/twentyhq/core-team-issues/issues/2427).

## Contents of the new page

- `defineObject` for the `kvStore` object (`key` + `value`)
- A unique index on `key` (the recommended uniqueness primitive)
- `get` / `set` (upsert) / `del` helpers built on `CoreApiClient`
- A worked example: caching an expensive third-party call with a TTL
- Patterns & tips: namespacing, expiry, what to store,
visibility/permissions, per-record scoping

## Files

- `developers/extend/apps/logic/key-value-store.mdx` — the new page
- `navigation/base-structure.json` — navigation source-of-truth
- `docs.json` + `twenty-shared/.../DocumentationPaths.ts` — regenerated
from base-structure

## Notes

- Documentation only — no code/behavior changes.
- This is a convention (a regular custom object), not a new feature, so
it inherits the same sync, permissions, and tooling as the rest of an
app's data.

https://claude.ai/code/session_01XAgXy1mUUMff5BFkFPjtfZ

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22061?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-24 11:45:35 +02:00
Rashad Karanouh 6ae6703e7d Map TFT useCase to partners need on opportunity import (#22054)
## Summary
- Accept optional `useCase` in the TFT → partners
`import-opportunity-from-tft` webhook payload
- Map it to `Opportunity.need` on create (TFT Use Case → partners Needs)
- Add unit tests for happy path and null `useCase` handling
- Bump twenty-partners to 1.1.4 (patch)

## Test plan
- [x] `yarn test:unit` (43 tests passing)
- [x] `yarn lint` (0 errors)
- [ ] Deploy to local/partners workspace with `yarn twenty dev --once`
- [ ] POST smoke test with `useCase` in body; confirm **Need** field
populated
- [ ] TFT workflow: add `"useCase": "{{record.useCase}}"` to HTTP body
(manual)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22054?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-24 11:43:46 +02:00
martmull 269c8ef400 feat(front): allow advanced relation fields in FieldWidget selector (#22005)
## After

<img width="706" height="760" alt="image"
src="https://github.com/user-attachments/assets/d118285f-baab-4187-988c-d0180d61a629"
/>
<img width="707" height="372" alt="image"
src="https://github.com/user-attachments/assets/5676d829-2ec1-494e-a8f0-5998f1f0c3c8"
/>


## Summary

The FieldWidget field-selection dropdown currently filters out relation
fields whose target is a system object, so users can't pick fields like
`calendarEventParticipants` on the CalendarEvent record page. The widget
itself can render them just fine as boxed relations — the restriction
only lives in the picker.

This unblocks the consistency story from #22003 (revert of #21857): once
shipped, participants can be added to the calendar event record page via
the existing FieldWidget mechanism instead of a bespoke side-panel page.

## Changes

- `isFieldCellSupported`: adds an opt-in `includeSystemObjectRelations`
option that skips the `isObjectMetadataAvailableForRelation` system
check.
- `useFieldListFieldMetadataItems`: forwards the option through to
`isFieldCellSupported`. Default is `false`, so all existing callers keep
current behavior.
- `useFieldWidgetEligibleFields`: turns the option on, so the
FieldWidget selector now surfaces fields like
`calendarEventParticipants`, `messageParticipants`, etc.

## Test plan

- [x] `nx typecheck twenty-front`
- [x] `nx lint:diff-with-main twenty-front`
- [ ] CI
- [ ] Manually verify the FieldWidget dropdown now lists
`calendarEventParticipants` on a CalendarEvent record page, and that
selecting it renders a participants list via the existing relation
card/field widget.

https://claude.ai/code/session_01RnMcjL35wdCRzpXN257RLJ

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22005?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-24 10:30:42 +02:00
Paul Rastoin f98f514640 Introduce search field metadata in 2 16 (#22055)
# Introduction

The devpx wasn't prepare for an already existing entity becoming a
syncable entity
Though the search field metadata entity was dormant anw
So considering it has been introduced starting from 2.16 is the quickest
and easiest tradeoff we can get

This PR is also reverting this one
https://github.com/twentyhq/twenty/pull/22039 that was introducing a new
way to decorate an entity at class level. But it did not fixed the issue

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22055?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-24 08:18:59 +00:00
martmull d00d26c4a4 ci: remove redundant twenty-meeting-bot per-app workflow (#22057)
## What

Removes `.github/workflows/ci-internal-app-twenty-meeting-bot.yaml`.

## Why

The generic `ci-internal-apps.yaml` already runs CI for every app under
`packages/twenty-apps/internal/` that has a `package.json`. For each
discovered app it runs:

- `yarn lint`
- `yarn typecheck` (when a `typecheck` script exists)
- `yarn test:unit` (when a `test:unit` script exists)
- `yarn test` integration tests against a spawned Twenty instance (when
a `test` script exists)

`twenty-meeting-bot` defines all four scripts (`lint`, `typecheck`,
`test:unit`, `test`), so it is fully covered by the generic workflow. It
was the **last remaining** per-app workflow — all the other internal
apps (discord, exa, fireflies, self-hosting, for-twenty, linear) were
already migrated to `ci-internal-apps.yaml`.

## Note

The removed workflow had two behavioral differences from the generic
one, which are the same standardized tradeoffs already accepted for
every other internal app:

- It built `twenty-server` from source and ran integration tests against
it, whereas the generic workflow tests against the published
`twentycrm/twenty-app-dev:latest` image via the
`spawn-twenty-app-dev-test` action.
- It also triggered on changes to `twenty-server` / `twenty-sdk` /
`twenty-client-sdk` / `twenty-shared`, whereas the generic workflow only
triggers on `packages/twenty-apps/internal/**` changes.

> [!NOTE]
> If `ci-internal-app-twenty-meeting-bot-status-check` is configured as
a required status check in branch protection, that rule should be
dropped (and `ci-internal-apps-status-check` kept) so PRs aren't blocked
waiting on a check that no longer runs.

https://claude.ai/code/session_013WZuk6jw2RmZT77enuMT2y

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22057?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-24 10:12:24 +02:00
martmull 3943898642 Remove custom widget from calendarEvent page layout (#22046)
## Before
<img width="608" height="759" alt="image"
src="https://github.com/user-attachments/assets/e75f6cc9-aef8-4247-a88d-6992b3936d3d"
/>

## After
<img width="844" height="658" alt="image"
src="https://github.com/user-attachments/assets/446a13f2-e40c-4e1c-a1ed-0920fe5065b3"
/>


remove https://github.com/twentyhq/twenty/pull/22016 custom widget and
replace with regular field widget
does not match figma design but avoid introducing specific behavior for
calendarEvent


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22046?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-24 08:59:14 +02:00
Charles Bochet 5ec98d3d84 fix(filter): guard isMatchingDateFilter against empty date values (#22029)
## Symptom

On the Opportunities **board (kanban)** view, creating or updating *any*
opportunity randomly crashed with:

```
Uncaught (in promise) Cannot read properties of null (reading 'split')
    ...
    at isMatchingDateFilter
    at isRecordMatchingFilter
    at opportunitiesGroupBy   (group-by optimistic effect)
    at createOneRecord
```

Reported in quality-feedbacks as *"Can't update the Opportunity"* —
"happens randomly, no specific path." The randomness is the tell: it
depends on the **view's filter configuration**, not on which record you
edit.

## What runs on create/update

Create/update trigger an **optimistic cache update**. On a board view
that means recomputing which group each record belongs to (the
`opportunitiesGroupBy` field in the trace). To do that, the group-by
optimistic effect re-evaluates **every record in the affected groups
against the view's filters** via `isRecordMatchingFilter`, which walks
the AND/OR filter tree and dispatches each leaf to a per-field-type
matcher (`isMatchingStringFilter`, `isMatchingSelectFilter`,
`isMatchingDateFilter`, …).

## Root cause

`isMatchingDateFilter` passed the record value straight to date-fns
`parseISO` for the `eq`/`neq`/`gt`/`gte`/`lt`/`lte` operators:

```ts
case dateFilter.gte !== undefined: {
  const valueDate = parseISO(value); // value = record[fieldName], declared `string` but actually nullable
  ...
}
```

`parseISO` parses an ISO string by first calling `argument.split(...)`
internally, so `parseISO(null)` runs `null.split(...)` → **`Cannot read
properties of null (reading 'split')`**. That's the three-deep
`utils`-chunk frame in the minified trace: `isMatchingDateFilter` →
`parseISO` → date-fns `splitDateString`.

`value` is `null` whenever a record has an **empty date field** (e.g. an
opportunity with no Close date). So the crash fires only when **both**
hold:

1. the current view has a **date filter**
(`gt`/`gte`/`lt`/`lte`/`eq`/`neq`) on some date field, **and**
2. at least one opportunity in view has that date field **empty**.

That's the "randomness" — purely a function of the view config and which
records have blank dates. The `is: NULL` operator never crashed (it
checks `value === null` before `parseISO`); only the value-parsing
operators were exposed. Sibling matchers (`isMatchingTSVectorFilter`,
`isMatchingRatingFilter`, `isMatchingSelectFilter`) already tolerate
`null` — the date matcher was the odd one out, and its `value: string`
type masked the real nullability.

## Fix

Widen the param type to the truth (`string | null | undefined`) and
guard the empty case up front:

```ts
if (!isDefined(value)) {
  return dateFilter.is === 'NULL';
}
```

Semantics:
- empty value + `is: NULL` → `true` (it *is* null)
- empty value + every other operator (incl. `is: NOT_NULL`) → `false`

The `false` is the *correct* answer, not just crash avoidance: it
mirrors SQL three-valued logic where `NULL > '2024-01-01'` is `UNKNOWN`
and the row is excluded. So the optimistic match now agrees with what
the backend query returns, and a blank-date record groups the same way
before and after the server round-trip.

## Tests

Added regression cases to `isMatchingDateFilter.test.ts` running `null`
and `undefined` through every operator (assert no throw + correct
boolean). These throw without the guard.
2026-06-24 08:53:08 +02:00
Abdullah. b7cd6db458 fix(deps): resolve qs to 6.15.2 (dedupe caret group + scope body-parser) (#22050)
## Summary

Closes [Dependabot alert
#1305](https://github.com/twentyhq/twenty/security/dependabot/1305) — qs
**CVE-2026-8723 / GHSA-q8mj-m7cp-5q26** (vulnerable `>=6.11.1 <=6.15.1`,
fixed `6.15.2`) — via two changes:

1. **`yarn dedupe qs`** collapses the caret-range consumers (gitbeaker,
formidable, superagent, googleapis-common, union, body-parser@2.2.2)
from `6.15.0` onto the `6.15.2` already in the tree. Clean, no
resolution.
2. A scoped **`body-parser/qs: 6.15.2`** resolution for the lone
tilde-pinned holdout.

## Why the one resolution

- After the dedupe, the only vulnerable qs left was `6.14.2`, from
**`body-parser@1.20.4`** which declares `qs ~6.14.0` (capped at 6.14.x).
- body-parser **2.x** uses `qs ^6.15.2`, but that needs **express 5** —
and the `body-parser@1.20.4` here comes from **express 4.22.x**, pulled
by `@mintlify/previewing` + verdaccio (build/dev tooling, not bumpable
to express 5).
- So a scoped `body-parser/qs: 6.15.2` is the right fix — qs `6.14 →
6.15` is a compatible minor. It's grouped with the existing `express/qs`
+ `@cypress/request/qs` entries (same CVE, same express-4.x root cause)
in both the `resolutions` block and the `"//resolutions"` doc.

## Verification

- `yarn install --immutable` passes.
- No qs in `[6.11.1, 6.15.1]` remains — all qs is now `6.15.2`.
- `qs` is build/dev tooling here (mintlify, verdaccio, gitbeaker, etc.),
not the production server runtime.
2026-06-24 08:51:19 +02:00
nitin 9a62cb5a67 add meeting bot app cover (#22049) 2026-06-23 23:29:39 +05:30
Etienne 0f4c4e69a9 fix(ai-tool): make search_output a raw-text occurrence search (#22034)
## Summary

`search_output` (the spilled-output navigation tool) was built around a
JSON-centric, line-based model that breaks for the data it actually
receives. Spilled outputs are written as compact
`JSON.stringify(output)` (single line, escaped newlines), so the tool's
line-by-line matching collapsed to at most one match, and its schema
described searching "the indented JSON representation" even though it
falls back to raw text for non-JSON. It also ran arbitrary,
model-supplied regexes through the native engine with no ReDoS
protection.

This reworks the tool into a `grep -o` style search over the raw file
bytes: it finds every occurrence of a pattern regardless of newlines and
returns a character window around each hit. It works uniformly for
compact/pretty JSON, CSV, HTML, and plain text.

## Changes

- **Occurrence-based matching** (`search-output.util.ts`): search the
raw content for every match via a global-regex `exec` loop (with a
zero-width-match guard), bounded by `offset + maxMatches`. Results are
now `{ charOffset, match, context }` with a character window around each
occurrence and a centered-ellipsis cap for very long single matches. The
line model (`split`, line numbers, line context) is removed.
- **ReDoS hardening**: matching now uses `re2` (already a dependency)
with the global flag, guaranteeing linear-time matching. Unsupported
regex features (lookahead/backreferences) and invalid patterns fall back
to escaped-literal search instead of throwing.
- **No more reserialization** (`search-output-tool.ts`): the
`JSON.stringify(JSON.parse(...))` round-trip is gone; the tool searches
the exact bytes on disk, so there is no coordinate divergence with
`extract_json_paths`.
- **API** (`search-output-tool.schema.ts`): `contextLines` →
`contextChars` (default 100, max 2000); honest descriptions reflecting
raw-text occurrence search and the regex-or-literal fallback. The result
message reports occurrence counts.
- **Cleanup**: removed unused constants
(`default-search-output-context-lines`,
`search-output-max-line-length`); added
`default-search-output-context-chars` and
`search-output-max-match-length`.

`extract_json_paths` and the spill service are untouched.

## Tradeoff

Results use character offsets/windows rather than line numbers and line
context. For an LLM extracting values from a spilled blob this is more
robust (works on single-line content); the cost is no line-based context
for genuinely line-structured content.

## Test plan

- [x] `search-output.util.spec.ts` rewritten for occurrence semantics:
multiple hits on a single newline-free line, zero-width-pattern
termination, catastrophic-backtracking pattern stays fast (RE2),
lookahead/invalid-regex literal fallback, char-window clipping, offset
pagination, long-match truncation. 12/12 pass.
- [x] `npx nx typecheck twenty-server` clean.
- [x] `npx nx lint:diff-with-main twenty-server` clean (lint + format).

## Deploy note

`re2` is a native addon. It was declared in `package.json` but never
imported/built before this PR, so its binary may be absent in some
environments (local install required `npm rebuild re2`). Confirm the
install/build pipeline (CI, Docker images) compiles native modules so
the tool doesn't throw `Cannot find module 're2.node'` at runtime.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22034?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:18:46 +00:00
github-actions[bot] 2e9550914c i18n - docs translations (#22047)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22047?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 19:18:23 +02:00
Abdullah. 9c9041c9f9 fix(deps): bump engine.io + socket.io-adapter to drop vulnerable ws 8.17.1 (#22044)
## Summary

Bumps the transitive **`engine.io`** `6.6.4 → 6.6.9` and
**`socket.io-adapter`** `2.5.5 → 2.5.8` (both within socket.io's
declared ranges — socket.io is pulled by mintlify / react-email build
tooling), which declare `ws ~8.21.0` instead of `~8.17.1`, **evicting
the last vulnerable `ws@8.17.1`**. Resolves two Dependabot alerts:

- [#1502](https://github.com/twentyhq/twenty/security/dependabot/1502)
(high) — GHSA-96hv-2xvq-fx4p, ws memory-exhaustion DoS (`>=8.0.0
<8.21.0`)
- [#1238](https://github.com/twentyhq/twenty/security/dependabot/1238)
(med) — GHSA-58qx-3vcg-4xpx, ws uninitialized memory disclosure
(`>=8.0.0 <8.20.1`)

## Why a parent-bump (not a resolution)

- The only vulnerable ws left was `8.17.1`, pinned by `engine.io@6.6.4`
(`ws ~8.17.1`) and `socket.io-adapter@2.5.5` (`ws ~8.17.1`). (The
earlier koa PR already dropped the dts-plugin `ws@8.18.0`.)
- `engine.io@6.6.9` and `socket.io-adapter@2.5.8` declare `ws ~8.21.0`,
and both bumps are within socket.io's existing ranges — so `yarn up -R`
carries the fix in-range, with no `resolutions` entry to maintain.
- (The pre-existing `@nestjs/graphql/ws: 8.21.0` resolution is unrelated
and untouched.)

## Result

- ws is now `8.21.0` (plus non-vulnerable `7.5.11` / `6.2.4`); nothing
in `[8.0.0, 8.21.0)`.
- `package.json` untouched; engine.io/socket.io are build /
email-preview tooling, not the server runtime.

## Verification

- `yarn install --immutable` passes.
- No vulnerable ws remains in `yarn.lock`; diff is contained to ws /
engine.io / socket.io-adapter (+ a `debug` descriptor cleanup).
2026-06-23 19:17:15 +02:00
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
Abdullah. 63e258a85c fix(deps): bump @module-federation/node to drop the koa-pinning 0.21.4 stack (#22032)
## Summary

Bumps the transitive **`@module-federation/node`** `2.7.23 → 2.7.45`
(within `@nx/module-federation`'s declared `^2.7.21`), which
consolidates the module-federation stack onto `enhanced 2.6.0` and
**prunes the duplicate 0.21.4 sub-stack that pinned koa 3.0.3** —
resolving [Dependabot alert
#547](https://github.com/twentyhq/twenty/security/dependabot/547):
CVE-2026-27959 / GHSA-7gcc-r8m5-44qm (koa Host Header Injection via
`ctx.hostname`, vulnerable `>=3.0.0 <3.1.2`). Lockfile-only, **no
resolution**.

## Why a parent-bump (not a resolution)

- koa 3.0.3 was pinned **exactly** by
`@module-federation/dts-plugin@0.21.4`. Newer dts-plugin (2.5.1, 2.6.0)
**dropped koa entirely**.
- The old 0.21.4 stack survived only because
`@module-federation/node@2.7.23` declared `@module-federation/enhanced:
0.21.4` (a stale internal pin). `@module-federation/node@2.7.45`
declares `enhanced: 2.6.0`, and `@nx/module-federation@22.7.5` already
requires node `^2.7.21` — so 2.7.45 is in range.
- `yarn up -R @module-federation/node` therefore eliminates the
vulnerable dependency honestly, in-range, with no `resolutions` entry to
maintain.

## Result

- `koa@3.0.3` gone, and with it the entire duplicate
`@module-federation/*@0.21.4` stack — **net −678 lines** of lockfile.
- Bonus: the dts-plugin-pinned `ws@8.18.0` dropped too (the remaining
`ws@8.17.1` comes from socket.io/engine.io — a separate, upcoming fix).
- `package.json` untouched. This is **build-tooling** (module-federation
type generation), not the production server runtime.

## Verification

- `yarn install --immutable` passes.
- `koa` is absent from `yarn.lock`; no
`@module-federation/enhanced@0.21.x` remains.
- The bump stays within `@nx/module-federation`'s declared range —
recommend the CI frontend build as the runtime check for the
module-federation tooling.
2026-06-23 17:49:54 +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
Raphaël Bosi de610bc4e7 Rename CI New UI workflow to CI UI (#22030)
Renames the `CI New UI` workflow to `CI UI`, dropping the "new ui"
terminology everywhere it appeared.

- Renamed `.github/workflows/ci-new-ui.yaml` → `ci-ui.yaml`
- Updated the workflow `name`, job names (`ui-task`, `ui-sb-build`,
`ui-sb-test`, `ci-ui-status-check`), and internal `needs`/`if`
references
- Updated `visual-regression-dispatch.yaml` which keys off the workflow
name (`CI UI`)

Note: the required status check in branch protection settings (workflow
name / `ci-ui-status-check`) lives in repo settings and will need
updating by an admin so PRs don't wait on the old check name.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22030?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:24:01 +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. 11218561d0 fix(deps): pin form-data under nx and zapier-platform-core to 4.0.6 (#22023)
## Summary

Adds two scoped `resolutions` (`nx/form-data` +
`zapier-platform-core/form-data` → `4.0.6`) forcing the lone vulnerable
`form-data@4.0.5` up to the patched `4.0.6`, resolving [Dependabot alert
#1506](https://github.com/twentyhq/twenty/security/dependabot/1506) —
CVE-2026-12143 / GHSA-hmw2-7cc7-3qxx (CRLF injection via unescaped
multipart field names/filenames, vulnerable `>=4.0.0 <4.0.6`).

## Why scoped resolutions (not a parent-bump)

- `form-data@4.0.5` is pinned **exactly** by `nx@22.7.5` (root devDep)
and `zapier-platform-core@19.0.0` (twenty-zapier) — and **both still pin
4.0.5 in their latest release**, so no parent upgrade carries the fix.
- Every *other* form-data consumer already resolves `4.0.6` naturally
via its `^4.0.x` range, so the two scoped pins simply **dedupe**
nx/zapier's copy onto that existing 4.0.6 — no new copy introduced.
- Scoped, not global, to match the existing `express/qs` +
`@cypress/request/qs` two-entry pattern (only form-data 4.x is in the
tree).

## Changes

- `package.json`: the two `resolutions` entries **plus** a matching
`"//resolutions"` doc entry (advisory, why-no-parent-bump, scope
rationale, drop condition).

## Verification

- `yarn install --immutable` passes.
- No form-data in `[4.0.0, 4.0.6)` remains in `yarn.lock`.
2026-06-23 19:40:54 +05: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. 558f2509c2 fix(deps): pin undici under @module-federation/dts-plugin to ^7.28.0 (#22014)
## Summary

Adds a **scoped** `resolutions` entry pinning `undici` under
`@module-federation/dts-plugin` to `^7.28.0`, resolving **6 Dependabot
alerts** — #1523 & #1566 (high), #1522 & #1570 (medium), #1565 & #1572
(low); all `undici >=7.0.0 <7.28.0`.

## Why a scoped resolution (not a parent-bump or a global pin)

- The vulnerable `undici@7.24.7` is pinned **exactly** by the transitive
build tool `@module-federation/dts-plugin@2.5.1`. There's no parent to
bump — its parents pin it and it doesn't loosen the pin at latest — so a
committed scoped resolution (matching the existing `@nestjs/graphql/ws`,
`express/qs` pattern) is the right fix.
- A **global** `undici` resolution would be wrong: the tree also has
`undici@6.27.0` (`^6.25.0`, outside the advisory) and the latest undici
is `8.5.0`, so forcing all undici to 7.x would break the 6.x consumer.
- `^7.28.0` resolves to `7.28.0` (highest 7.x), which **dedupes** with
the `undici@7.28.0` already in the tree (via `^7.25.0`) — no new copy is
introduced.

## Result

- `undici@7.24.7` is gone; the only undici 7.x is now `7.28.0`.
`undici@6.27.0` (6.x) is untouched.
- Build-tooling dependency (module-federation type generation) — not in
the production server runtime; `undici 7.24 → 7.28` is a compatible
minor bump.

## Verification

- `yarn install --immutable` passes (lockfile consistent with CI).
- No undici in `[7.0.0, 7.28.0)` remains in `yarn.lock`.
2026-06-23 15:27:12 +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