Compare commits

...
Author SHA1 Message Date
ehconitin 9e6d384e9b add upgrade command 2026-06-05 21:28:38 +05:30
ehconitin 62dce10699 stash 2026-06-05 19:48:06 +05:30
ehconitin a81dd55f9d Merge remote-tracking branch 'upstream/main' into call-recording-api 2026-06-05 18:40:32 +05:30
nitinandGitHub e485b679ea [Call Recording] Add standard object (#21158)
Adds **Call Recording** as a first-class standard object (Twenty's
flat-metadata
standard-object system), with a hidden junction to calendar events and a
backfill
command for existing workspaces. Everything is gated behind the
`IS_CALL_RECORDING_ENABLED` feature flag.

### What's included
- **`CallRecording`**: audio/video files, transcript, status, recording
policy,
timing, external bot/recording ids. Label identifier is
`meetingOccurrenceKey`.
- **`CallRecordingCalendarEventAssociation`**: hidden junction linking a
recording
to a calendar event (dedupes one bot to many subscribers of the same
meeting).
- Full metadata graph via the flat-metadata builders: fields, indexes,
views,
  view fields/groups, record page layout, and navigation items.
- **Metadata-only reverse relation** on `CalendarEvent`: present in
standard
metadata, omitted from the TS entity class to avoid expanding recursive
  nested-insert types.
- **Upgrade command (2.9.0)** backfilling active/suspended workspaces:
  - Creates the full graph; idempotent (skips when it already exists).
- Moves a colliding custom `callRecording` object aside to
`callRecordingOld`
    (numeric suffix if that name is also taken).
- Navigation items (commands) are flag-gated by `universalIdentifier`,
so a custom object
    reusing the name is never gated.

### QA
Run locally against existing workspaces (with and without a name
collision) and a
freshly created workspace:
- [x] Backfill, collision: custom `callRecording` renamed to
`callRecordingOld`;
  standard graph created.
- [x] Backfill, no collision: standard graph created; unrelated custom
object untouched.
- [x] Idempotent: re-run is a no-op, with no duplicate metadata and
counts unchanged.
- [x] New workspace via `init()` produces an identical graph to the
backfill
  (`universalIdentifier` set-diff = 0).
- [x] Label identifier (`meetingOccurrenceKey`) holds position 0 in
non-widget views.
- [x] Nav items gated behind the feature flag; collision-renamed
object's nav
  expression re-pointed to its new name.
- [x] Unit tests cover collision name resolution and nav-gating logic.
2026-06-05 13:02:50 +00:00
ehconitin 22f9c3517b Merge remote-tracking branch 'upstream/call-recording-standard-object' into ehco/intelligent-benz-a7fa3b 2026-06-05 18:25:28 +05:30
ehconitin c629ea858b feat(server): add calendar event recording decision layer 2026-06-05 18:25:11 +05:30
Paul RastoinandGitHub a78b319877 sdk/v* twenty/v* namespaces (#21247)
# Introduction
Removed never used release dispatch workflow
Now assuming that anyone releasing will create both twenty and npm
family tags
Will create a workflow to ease this later

We will now start to have several github releases, one per namespace
2026-06-05 12:40:50 +00:00
ehconitin 05ea7ac120 Scope each call recording collision rename to its entity's application 2026-06-05 18:00:46 +05:30
ehconitin 0c1ca75981 Revert "Use from-to migration for call recording sync"
This reverts commit 312b6969aa.
2026-06-05 17:27:17 +05:30
ehconitin 1f63ff0f3d Revert "fix"
This reverts commit cc93aaa6b4.
2026-06-05 17:27:17 +05:30
Félix MalfaitandGitHub c3dd6b25a6 fix: use canonical oxlint rule id in lint-disable directives (#21253)
## What

Many `oxlint-disable` / `eslint-disable` directives across the repo
carry a corrupted rule id — `@typescripttypescript/<rule>` — most likely
a find-and-replace accident that mangled the eslint-era
`@typescript-eslint/` prefix.

oxlint matches disable directives **loosely by rule name**, so these
still suppress in practice (not a silent no-op), but the id is malformed
and misleading.

## Change

Replace them with the **canonical oxlint id** `typescript/<rule>` —
matching the plugin name and rule keys declared in `.oxlintrc.json` —
**127 files, 262 directives**:

| rule | count |
| --- | ----- |
| `typescript/no-explicit-any` | 250 |
| `typescript/ban-ts-comment` | 6 |
| `typescript/no-misused-promises` | 4 |
| `typescript/no-empty-object-type` | 2 |

- `twenty-server`: 122 files
- `twenty-front`: 5 files

Comment-only — no code or runtime changes.

## Verification

`oxlint --type-aware -c .oxlintrc.json` reports **0 warnings / 0
errors** for both `twenty-server` and `twenty-front`. Every changed line
is exactly the id correction inside a disable directive (262 insertions
/ 262 deletions, no collateral edits).

> Addresses the cubic review, which flagged that the canonical oxlint id
is `typescript/...` (no `@`). Worth noting the original
`@typescripttypescript/` was not actually a silent no-op — oxlint
matches these directives loosely by rule name — but `typescript/` is the
correct, config-aligned id.
2026-06-05 13:52:32 +02:00
ehconitin cc93aaa6b4 fix 2026-06-05 16:56:37 +05:30
ehconitin 312b6969aa Use from-to migration for call recording sync 2026-06-05 16:36:08 +05:30
6f9b59b224 Scaffold twenty-new-ui (#21236)
Scaffolds `twenty-new-ui`, the next-gen replacement for `twenty-ui`, on
**SCSS** Modules + **Base UI** (no Linaria).

- **Tooling**: Vite lib build, subpaths mirror twenty-ui, typed SCSS
Modules, Storybook + axe a11y, size-limit, Nx targets.
- **Theme**: single token source → nx generateTheme emits the CSS vars +
accessor; parity test asserts token-for-token match with twenty-ui.

Migrated a first `Toggle` component with its stories to allow
@charlesBochet to wire the new pixel-diff system.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 10:42:57 +00:00
ehconitin dd46d74d41 throw dont skip 2026-06-05 14:44:52 +05:30
Abdullah.andGitHub b60a91a075 fix(website): render releases from local notes, drop GitHub gate (#21238)
The releases page gated visible notes on fetchLatestGithubReleaseTag();
when that unauthenticated GitHub call was rate-limited at build (common
on shared Cloudflare build IPs, no GITHUB_TOKEN), it returned null and
getVisibleReleaseNotes returned [], rendering 'No releases are visible
yet for the current published version' — the exact prod symptom.
force-static made each deploy a coin flip.

Committed MDX (+images) is the single source of truth, so gate
visibility on nothing: render all local notes deterministically with no
network call. Removes the dev/prod divergence and deletes the now-dead
fetch-latest-release-tag.ts and get-visible-releases.ts.
2026-06-05 08:59:40 +00:00
ehconitin 1118c40864 fix 2026-06-05 14:28:40 +05:30
Charles BochetandGitHub cd540098f1 fix: pass reference_commit to Argos to resolve orphan PR builds (#21245)
## Summary

- Fixes Argos CI builds showing as "Orphan" (no reference branch) for PR
builds
- Computes the merge-base SHA between the PR head and `main` using the
GitHub API (`compareCommitsWithBasehead`) in the dispatch workflow
- Passes `reference_commit` in the `ci-privileged` dispatch payload so
it can be forwarded to the Argos upload API

## Context

PR builds on Argos were showing as "Orphan" because `ci-privileged`
(where the actual Argos upload happens) has no git history of the
`twenty` repo — it cannot compute the merge-base locally. Without a
`referenceCommit`, Argos can't determine which `main` build to compare
against.

The local `visual-diff.sh` script already passes
`ARGOS_REFERENCE_COMMIT` via `git merge-base HEAD main`, but the CI
pipeline was missing this. This PR adds equivalent logic using the
GitHub API (no checkout needed).

## Note for ci-privileged

The `upload-to-argos.ts` script in `ci-privileged` needs a corresponding
update to read `reference_commit` from the dispatch payload and pass it
as `referenceCommit` in the Argos API call:

```typescript
referenceCommit: process.env.REFERENCE_COMMIT || undefined,
```

## Test plan

- [ ] Verify the workflow runs successfully on a PR (merge-base step
computes a SHA)
- [ ] Confirm Argos PR builds are no longer marked as "Orphan" after the
ci-privileged counterpart is updated
2026-06-05 10:51:16 +02:00
Félix MalfaitandGitHub 1b30983307 fix(settings): gate the AI settings page on AI_SETTINGS, not the chat flag (#21239)
## Summary

Closes #21229.

The two AI role permissions behaved **opposite to their labels**. The
trap is that the flag's code name is the inverse of its UI label:

| `PermissionFlagType` | UI label | Section | Means |
|---|---|---|---|
| `AI` | **"Ask AI"** | Actions | End-user: chat with AI |
| `AI_SETTINGS` | **"AI"** | Member / settings | Admin: configure AI
agents |

Before this PR (on `main`):
- `AI` ("Ask AI", chat) gated **both** the AI chat **and** the AI
settings page.
- `AI_SETTINGS` ("AI", configure agents) gated **nothing** the user
could see.

So a chat-only user could reach the whole AI **configuration** page, and
toggling the "AI" settings permission did nothing — exactly the
misalignment reported in #21229.

## Root cause

`PermissionFlagType.AI` *reads* like "the AI permission", so it looks
like the natural gate for the AI settings page — but it's actually the
**chat** flag. The settings page (nav item + route) had been pointed at
`AI` in #21072 to match the Overview stats query
(`findWorkspaceAiStats`), which was itself mis-gated on `AI`. Both the
stats query and the rest of the settings surface are admin/config
features, so they belong on `AI_SETTINGS`.

## Changes

All three move the **AI settings surface** from the chat flag (`AI`) to
the settings flag (`AI_SETTINGS`); chat keeps following `AI`:

- `useSettingsNavigationItems.tsx` — AI nav item → `AI_SETTINGS`
- `SettingsRoutes.tsx` — AI settings route group → `AI_SETTINGS`
- `ai-workspace-stats.resolver.ts` — `findWorkspaceAiStats`
(settings-only, drives the Overview tab) → `AI_SETTINGS`

After this: the "AI" permission controls the AI settings page + its
Overview; the "Ask AI" permission controls the chat. Both toggles now
match their labels.

## Test plan

- [ ] Role with **only "Ask AI"** (`AI`): AI chat tabs/pane visible;
**Settings → AI is hidden** and the route is not reachable.
- [ ] Role with **only "AI"** (`AI_SETTINGS`): Settings → AI is visible,
Overview stats load; chat nav is hidden.
- [ ] Admin (both flags): everything works as before.

## Known follow-ups (out of scope — pre-existing, shared endpoints)

These remain on `AI` because they're shared with non-settings surfaces
and need either OR-gating or a resolver split, so a role with
`AI_SETTINGS` but **not** `AI` still can't use them yet:

- `getAiSystemPromptPreview` (Models/Prompts tabs) lives in the chat
resolver, class-gated `AI`; NestJS guards are additive so it can't be
cleanly method-overridden — it should be pulled into a settings
resolver.
- Agent reads `findManyAgents` / `findOneAgent` (agent create/edit
forms) are class-gated `AI` and shared with the **Workflow** editor and
**Roles** pages; these want a guard that accepts `AI ∨ AI_SETTINGS ∨
WORKFLOWS`.
2026-06-05 08:18:20 +00:00
ehconitin 817cf18075 Merge remote-tracking branch 'upstream/main' into call-recording-standard-object 2026-06-05 13:42:15 +05:30
martmullandGitHub 128d2d394d feat: allow apps to add view fields to existing views (defineViewField) (#21160)
## Summary

Lets a Twenty application add **view fields (columns) to an existing
view it does not own** — including standard views like the People index
view — without redeclaring/owning that view. This mirrors the existing,
working pattern by which an app adds a custom field to a standard object
via `defineField` + `objectUniversalIdentifier`.

The asymmetry being removed was purely in the manifest schema:
`ViewFieldManifest` only existed *nested* inside
`ViewManifest.fields[]`, so adding a view field forced declaring a
`ViewManifest` — which the sync treats as a view the app creates and
owns, and rejects when the UID is a standard view's. Validation,
persistence, the FK aggregator machinery, and uninstall cleanup were
already generic and cross-app-safe, so no engine changes were needed.

### Changes
- **twenty-shared:** new top-level `StandaloneViewFieldManifest`
(`ViewFieldManifest & { viewUniversalIdentifier }`),
`Manifest.viewFields`, and a `SyncableEntity.ViewField` member.
- **twenty-sdk:** `defineViewField` (validates `universalIdentifier` +
`viewUniversalIdentifier` + `fieldMetadataUniversalIdentifier`), CLI
manifest assembly of a top-level `viewFields` list, and `dev:add
viewField` scaffolding.
- **twenty-server:** one top-level loop over `manifest.viewFields` that
reuses the existing `fromViewFieldManifestToUniversalFlatViewField`
converter (already parameterized by `viewUniversalIdentifier`). No
validator/persistence/aggregator changes.

### Notes for maintainers
- Confirm the `Manifest.viewFields` optionality convention — implemented
as a **required** array to mirror `fields`/`views`.
- Two different apps adding a column for the same field to the same view
conflicts on the existing unique `(fieldMetadataId, viewId)` partial
index; the existing `flat-view-field-validator` duplicate check surfaces
this as a structured validation error.
- `dev:add viewField` scaffolding is included (was optional in the
plan).

## Test Plan
- [x] `twenty-shared` typecheck
- [x] `twenty-sdk` 364 unit tests + `buildManifest` assembly test
(rich-app fixture) + typecheck + prettier
- [x] `twenty-server` typecheck + `lint:diff-with-main`
- [x] **Server integration suite**
`successful-manifest-update-view-field.integration-spec.ts` (4/4):
- standalone view field attaches to the standard `allPeople` view
without recreating it (sync succeeds, no
`INVALID_VIEW_DATA`/`ENTITY_ALREADY_EXISTS`)
- uninstall removes the contributed column while the standard view + its
columns remain intact
  - duplicate `(view, field)` rejected with `METADATA_VALIDATION_FAILED`
  - unknown target view rejected
- [x] Sibling `successful-manifest-update-field.integration-spec.ts`
still green (no harness regression)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-05 08:06:30 +00:00
ff9b5a5cad chore: sync AI model catalog from models.dev (#21242)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-06-05 09:22:38 +02:00
979047d004 fix: allow email change verification on self-hosted instances (#20123)
fixes #20117 

## Technical Details
Flow after fix:
1. User submits email change request
2. user.service.ts:517-524 calls sendVerificationEmail() with
verificationTrigger: EMAIL_UPDATE
3. Guard checks: verificationTrigger === SIGN_UP → false → guard skipped
4. Verification token generated, email rendered and sent via
emailService.send()
5. User receives confirmation email at new address
6. User clicks confirmation link → email update completes
---
Impact
- Minimal change: Only 3 lines modified in a single file
- No breaking changes: Sign-up verification behavior unchanged
- Security preserved: Email changes always require verification (correct
security behavior)
- Self-hosted friendly: Instance admins can disable sign-up verification
while keeping email change verification active

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-04 18:56:15 +00:00
ehconitin 544ebd487a Merge remote-tracking branch 'upstream/main' into call-recording-standard-object 2026-06-04 23:04:13 +05:30
EtienneandGitHub 8bd4cbc3fd fix(ai) - optim (#21233)
1. tool-registry.service.ts, Pass precomputed catalog to
resolveSchemas()

resolveSchemas() now accepts an optional precomputedCatalog parameter.
Both getToolsByName() and getToolInfo() pass the catalog they already
fetched, eliminating a redundant getCatalog() rebuild inside
resolveSchemas().

2. database-tool.provider.ts, Skip field lookup when schemas=false

When building the catalog index (includeSchemas=false),
getFlatFieldsFromFlatObjectMetadata() is no longer called for each of
the 25 objects. The hasGroupByToolInputSchema() check is also skipped,
group_by tools are always included in the index, with the real
eligibility check deferred to learn_tools time.

--> 100/150ms gain on learn/execute_tool execution
2026-06-04 17:27:25 +00:00
f899660a40 i18n - docs translations (#21237)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-04 19:24:06 +02:00
Paul RastoinandGitHub 0877ba2ffd Fix getApplicationSubAllFlatEntityMaps to prune unrelated appIds universal identifier aggregators (#21234)
# Introduction
Atm when computing the `fromAllFlatEntityMaps` we're retrieving all the
applicationIds related metadata entities to build a flat entity maps
scoped to them ( atm always the applicationId + twenty-standard
application id ) only inter app dependency we manage for the moment

A flat entity contains universal identifier aggregator to its related
entities

The issue was that the `getApplicationSubAllFlatEntityMaps` wasn't
pruning the aggregator by app

Now added a new process phase after the initial one that will check that
all the aggregators contains universal identifiers that has been
retrieve from the appId + appId standard intersection

## TDD test
Created a very human readable ( that's a joke ) test
2026-06-04 16:53:25 +00:00
Thomas TrompetteandGitHub 4e1cc2d831 fix: prevent workflow from disappearing after activation (#21231)
## Summary
- Fixes a regression from #21176 where activating a workflow caused it
to disappear until page refresh
- Root cause: when a draft is activated (status DRAFT→ACTIVE),
`useEffectiveDraftVersionId` incorrectly treated it as a discard because
the cached version was no longer DRAFT, filtering it from the versions
list
- Fix: only set `lastDiscardedDraftId` when `deletedAt` is actually set
on the cached version, not when the status simply changes

## Test plan
- [x] Open a workflow with a DRAFT version
- [x] Activate the workflow → verify it does NOT disappear
- [x] Discard a draft → verify header does NOT flicker between
DRAFT/ACTIVE
2026-06-04 16:42:05 +00:00
martmullGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
c2ca90c255 feat(sdk): add runAgent() to run app agents from logic functions (#21157)
<img width="948" height="593" alt="image"
src="https://github.com/user-attachments/assets/d990fa98-3cfd-469d-ab7f-0b2d4ccf3afc"
/>

<img width="1361" height="802" alt="image"
src="https://github.com/user-attachments/assets/1091f598-49f3-4c16-92ea-1e1c200181e2"
/>


## Add `runAgent()` to the Logic Function SDK

Lets an app's logic function run one of its own AI agents server-side
and get the result back synchronously — reusing the existing agent
executor instead of a new bespoke transport.

  ### Backend
- New **`runAgent` GraphQL mutation** (metadata schema) in
`ai-agent-execution`, wrapping the existing
`AgentAsyncExecutorService.executeAgent`. Scopes the agent lookup to the
calling
  application and runs it under an application auth context.
- New `@AuthApplication()` param decorator (mirrors `@AuthWorkspace()`)
— first GraphQL resolver authenticated by an **application access
token**.
- Guarded by `WorkspaceAuthGuard` +
`SettingsPermissionGuard(PermissionFlagType.AI)`: the app's role must
grant the `AI` permission flag.

  ### SDK
- `runAgent({ agentUniversalIdentifier, prompt })` posts the mutation to
`/metadata` with the app token via a new runtime GraphQL transport.
Returns `{ result, hasNoMoreAvailableCredits
  }`.
- Refactored the connections helpers onto a shared `postAppEndpoint`
util (removes duplicated transport logic).

  ### Frontend
- App install permission modal now shows an explicit consent line —
_"Run AI agents and bill AI credits to your workspace"_ — when the app's
role requests the `AI` flag.

  ### Docs
- Documented `runAgent` and its `AI` permission-flag requirement in
_Skills & Agents_.
- Fixed outdated role-permission examples in _Roles & Permissions_
(`permissionFlags` → `permissionFlagUniversalIdentifiers`,
`PermissionFlag` → `SystemPermissionFlag`).

  ### Test plan
- [x] SDK unit tests (`run-agent.spec.ts`) — request shape, GraphQL/HTTP
error handling, missing env vars
- [x] `twenty-server`, `twenty-front`, `twenty-shared` typecheck + lint
- [ ] Manual: install an app granting the `AI` flag, call `runAgent()`
from a logic function, confirm the agent runs and credits are billed

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-06-04 16:18:27 +00:00
Raphaël BosiandGitHub 36b654bab3 Scaffold people data labs enrichment app (#21175)
# Scaffold People Data Labs enrichment app

Defines the data model for enriching **Person** and **Company** with
People Data
Labs data. **Scaffold only** — the enrichment logic (the "mapper")
follows
separately; see the package README.

## Included
- **Fields** on Person & Company (PDL base data set).
- **Enums as SELECT / MULTI_SELECT** validated against PDL canonical
files (v34.1).
- **Standard-field mapping**: no `pdl*` shadow where a standard field
exists.
- **Location → ADDRESS**; **relation** `pdlCurrentCompany` ↔
`pdlCurrentEmployees`.
- **Metadata**: `pdlId`, `pdlLikelihood`, `pdlEnrichmentStatus`,
`pdlLastEnrichedAt`, `pdlRawPayload`.
- Shared option constants + helper, indexes, and a view per object.
2026-06-04 15:54:38 +00:00
Raphaël BosiandGitHub 41d5d80a65 Migrate Company and Person standard fields in preparation for the enrichment app (#21171)
# Migrate Company and Person standard fields in preparation for the
enrichment app

## Why

Our standard `Person`/`Company` objects accumulated fields that aren't
generic to every
business, while missing a more universal revenue field that essentially
every CRM ships.
This PR makes the **Standard application** hold a tighter, more
universal set of fields,
and sets the stage for a follow-up PR that introduces a **People Data
Labs enrichment app**
to populate them.

## What changes

### Standard fields

**Demoted (Standard → Workspace Custom application)** — not generic
enough to ship as standard:

| Object  | Field                          | Type     |
| ------- | ------------------------------ | -------- |
| Company | annualRecurringRevenue (ARR)   | CURRENCY |
| Company | employees                      | NUMBER   |
| Company | idealCustomerProfile (ICP)     | BOOLEAN  |
| Company | xLink (X/Twitter)              | LINKS    |
| Person  | xLink (X/Twitter)              | LINKS    |
| Person  | city                           | TEXT     |

**Added (new generic Standard field)** — present in
Salesforce/HubSpot/Zoho, PDL-populatable:

| Object | Field | Type |
| ------- | ------------- |
-------------------------------------------------------- |
| Company | annualRevenue | CURRENCY (generic total revenue; replaces
the niche ARR) |

### Behavior by workspace

* **New workspaces:** demoted fields are gone; `annualRevenue` is
**active**.
* **Existing workspaces:** demoted fields are **preserved as active
custom fields, data intact**;
`annualRevenue` is created **inactive (opt-in)** with its column ready,
so a later activation
  is a metadata-only toggle.

### Upgrade commands (v2.9)

Three idempotent, per-workspace commands, run in timestamp order:

1. **`upgrade:2-9:move-demoted-standard-fields-to-custom-application`**
(1799000040000) —
re-owns the 6 demoted fields to the workspace custom application
(`isCustom = true`,
new `applicationId` + fresh `universalIdentifier`), keeping their data
and active state.
2. **`upgrade:2-9:rename-conflicting-custom-fields`** (1799000045000) —
if a workspace already
has a *custom* field named `annualRevenue`, renames it to
`annualRevenueCustom`
(data preserved via column rename) so the standard field can be added.
Skips non-custom matches.
3. **`upgrade:2-9:add-inactive-generic-standard-fields`**
(1799000050000) — creates
`Company.annualRevenue` on existing workspaces as inactive, guarded to
skip workspaces
   missing the target object or where the name is still taken.

**Failure model:** the workspace iterator isolates failures per
workspace (one workspace failing
never affects others); within a workspace the runner records per-command
status and resumes on the
next run, and every command is idempotent, so partial runs self-heal.

### Supporting changes

* **Field-option color palette:** widened the `TagColor` union
(`twenty-shared` `FieldMetadataOptions`
+ the field-metadata `options.input` DTO) from 10 colors to the full
theme palette, benefiting any
  future SELECT/MULTI_SELECT field.
* **Dev seeder:**
* The default "Annual Recurring Revenue" dashboard widget now points at
the generic
    `annualRevenue` field (renamed to "Annual Revenue").
* Removed the "Companies by Size (Stacked by City)" widget (relied on
the demoted `employees`).
* `employees` is dropped from company data seeds and re-added as a
**custom** field seed, so dev
workspaces still get an `employees` column matching the demoted
behavior.

### Cleanup

Front-end record types (`Company.ts`/`Person.ts`), the
`getDisplayNameFromParticipant` test mock,
metadata integration specs, the Zapier `crud_record` test, and the
regenerated
`get-standard-object-metadata-related-entity-ids` snapshot.

## ⚠️ Breaking change (intentional)

Removes standard fields `Company.annualRecurringRevenue`,
`Company.employees`,
`Company.idealCustomerProfile`, `Company.xLink`, `Person.xLink`, and
`Person.city` from the core
GraphQL schema (replaced by `Company.annualRevenue`).

This is why the breaking-changes check reports a large number of
removals — `graphql-inspector`
flags any removed object field plus its derived
aggregate/order-by/filter/update types.

**Mitigation:** the
`upgrade:2-9:move-demoted-standard-fields-to-custom-application` command
re-owns these fields as custom fields per workspace, preserving their
name and data, so existing
tenants keep working. New workspaces won't have them.
2026-06-04 15:54:04 +00:00
e0d42323af Add more control on http trigger (#21216)
add "new Response" utils to define response code or content type of http
route triggered logic function responses

follow up of https://github.com/twentyhq/twenty/pull/21214

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:34:10 +00:00
Abdullah.andGitHub d3a1781a59 Use serialize-javascript for JSON-LD serialization on twenty-website (#21223)
Our scanner flags the `dangerouslySetInnerHTML` in `JsonLd.tsx` as a
potential XSS sink. Since JSON-LD must be emitted as raw `<script
type="application/ld+json">` text (rendering it as a React child
HTML-entity-escapes it and corrupts the JSON, and the site is statically
generated so it must be in the SSG HTML for crawlers),
`dangerouslySetInnerHTML` is the correct, Next.js-documented approach
(the real fix is sanitizing the payload). This PR swaps our hand-rolled
`JSON.stringify().replace(/</g, ...)` for
[`serialize-javascript`](https://www.npmjs.com/package/serialize-javascript)
in `isJSON` mode, the maintained library [Next.js explicitly
recommends](https://nextjs.org/docs/app/guides/json-ld) for this, so the
script-unsafe characters are escaped by a vetted serializer rather than
custom code.
2026-06-04 14:38:18 +00:00
Thomas des FrancsandGitHub 70066cdcf4 Update Google Workspace and integration logo assets (#21185)
## Summary
- Replace the Gmail and Google Calendar assets with their May 2026 icon
refreshes.
- Update the shared Google, Google Meet, Microsoft Outlook, Microsoft
Calendar-provider, Docusign, Stripe, Tally, and Zapier logo assets.
- Regenerate matching website/front raster assets at their existing
dimensions.

## Preview

GitHub's file diff can render some tiny transparent SVG/WebP logo diffs
as nearly empty. This grid uses the final raster assets from the PR
branch; Stripe and Zapier use their official favicon marks.

| Logo | Preview |
| --- | --- |
| Gmail | <img
src="https://raw.githubusercontent.com/twentyhq/twenty/bonapara/update-gmail-icon/packages/twenty-website/public/images/shared/companies/logos/gmail.webp"
width="48" height="48" alt="Gmail logo" /> |
| Google Calendar | <img
src="https://raw.githubusercontent.com/twentyhq/twenty/bonapara/update-gmail-icon/packages/twenty-website/public/images/shared/companies/logos/calendar.webp"
width="48" height="48" alt="Google Calendar logo" /> |
| Google | <img
src="https://raw.githubusercontent.com/twentyhq/twenty/bonapara/update-gmail-icon/packages/twenty-website/public/images/shared/companies/logos/google.webp"
width="48" height="48" alt="Google logo" /> |
| Google Meet | <img
src="https://raw.githubusercontent.com/twentyhq/twenty/bonapara/update-gmail-icon/packages/twenty-website/public/images/shared/companies/logos/meet.webp"
width="48" height="48" alt="Google Meet logo" /> |
| Microsoft Outlook / Calendar | <img
src="https://raw.githubusercontent.com/twentyhq/twenty/bonapara/update-gmail-icon/packages/twenty-website/public/images/shared/companies/logos/outlook.webp"
width="48" height="48" alt="Microsoft Outlook / Calendar logo" /> |
| Docusign | <img
src="https://raw.githubusercontent.com/twentyhq/twenty/bonapara/update-gmail-icon/packages/twenty-website/public/images/shared/companies/logos/docusign.webp"
width="48" height="48" alt="Docusign logo" /> |
| Stripe | <img
src="https://raw.githubusercontent.com/twentyhq/twenty/bonapara/update-gmail-icon/packages/twenty-website/public/images/shared/companies/logos/stripe.webp"
width="48" height="48" alt="Stripe logo" /> |
| Tally | <img
src="https://raw.githubusercontent.com/twentyhq/twenty/bonapara/update-gmail-icon/packages/twenty-website/public/images/shared/companies/logos/tally.webp"
width="48" height="48" alt="Tally logo" /> |
| Zapier | <img
src="https://raw.githubusercontent.com/twentyhq/twenty/bonapara/update-gmail-icon/packages/twenty-website/public/images/shared/companies/logos/zapier.webp"
width="48" height="48" alt="Zapier logo" /> |


## Notes
- Microsoft Calendar now uses the current Outlook/M365 icon, matching
the provider users connect for Microsoft calendar sync.
- Docusign keeps the compact Nexus mark for the small square logo slot,
regenerated from the current official Docusign lockup.
- Stripe now uses the official favicon mark from stripe.com.
- Zapier now uses the official square favicon mark from zapier.com.

## Sources
- Gmail 2026:
https://upload.wikimedia.org/wikipedia/commons/8/8f/Gmail_icon_%282026%29.svg
- Google Calendar 2026:
https://upload.wikimedia.org/wikipedia/commons/f/fa/Google_Calendar_icon_%282026%29.svg
- Google gradient G:
https://blog.google/company-news/inside-google/company-announcements/gradient-g-logo-design/
- Google Meet 2026:
https://commons.wikimedia.org/wiki/File:Google_Meet_icon_(2026).svg
- Microsoft 365 icon refresh:
https://techcommunity.microsoft.com/blog/microsoft365insiderblog/new-microsoft-365-icons-for-the-ai-era/4458674
- Docusign logo guidelines: https://brand.docusign.com/logo
- Stripe favicon:
https://images.stripeassets.com/fzn2n1nzq965/1hgcBNd12BfT9VLgbId7By/01d91920114b124fb4cf6d448f9f06eb/favicon.svg
- Tally press kit: https://tally.so/help/press-kit
- Zapier favicon: https://zapier.com/favicon.ico

## Validation
- Confirmed touched UI SVGs transform through SVGR successfully.
- Confirmed regenerated raster dimensions and alpha metadata with Sharp.
- Reviewed a generated contact sheet at 72px, 40px, and 16px.
- Ran `git diff --check`.
- Ran `packages/twenty-ui` Vite build with ARM Node v24.5.0:
`/Users/thomascolasdesfrancs/.nvm/versions/node/v24.5.0/bin/node
../../node_modules/vite/bin/vite.js build`.

Local note: `npx vite build` fails in `packages/twenty-ui` on this
machine because that directory resolves `/usr/local/bin/node` as x64
v23.5.0 while the Yarn install links Rollup's ARM optional package. The
direct ARM Node build above passed.
2026-06-04 14:19:49 +00:00
ehconitin 5672a090f8 fix(call-recording): resequence upgrade command timestamp 2026-06-04 19:30:34 +05:30
Charles BochetandGitHub a7fed47932 fix(twenty-ui): prevent local visual diffs from polluting CI baselines (#21225)
## Summary

- **Username-prefix branches**: Local visual-diff builds now use
`charles/main` instead of `main` as the branch name, preventing local
runs from creating auto-approved reference builds that could overwrite
CI baselines.
- **Local merge-base computation**: Computes `ARGOS_REFERENCE_COMMIT`
via `git merge-base HEAD main` locally, so the Argos SDK skips `git
fetch origin <branch>` — fixing the "fatal: couldn't find remote ref"
error when running from non-pushed branches.
- **Pass `referenceCommit` to vitest plugin**: Ensures the locally
computed merge-base is forwarded to the Argos upload.

## Test plan

- [x] Verified local visual-diff works from `main` branch (branch
becomes `charles/main`, not auto-approved)
- [x] Verified local visual-diff works from a non-pushed branch
(`test/local-only-visual-diff` → build uploaded successfully)
2026-06-04 15:42:32 +02:00
ehconitin 8a8310dee3 Fix CallRecording standard object upgrade collisions 2026-06-04 19:12:28 +05:30
ehconitin 97c6b3a4b8 fix(call-recording): move upgrade command to 2-10 after main bumped to 2.10.0 2026-06-04 18:59:22 +05:30
Charles BochetandGitHub f4da7767f8 chore: remove Chromatic dependencies and configuration (#21221)
## Summary

- Remove `chromatic` and `@chromatic-com/storybook` devDependencies from
twenty-front
- Remove global `chromatic` Nx target from nx.json and twenty-front
project.json override
- Remove commented Chromatic Storybook addon from twenty-front
- Remove `CHROMATIC_PROJECT_TOKEN` from .env.example
- Update README to remove Chromatic sponsor reference (image was already
missing)
- Update stale Chromatic comment in toSpliced.ts

## Context

Visual regression testing has moved from Chromatic SaaS to self-hosted
Argos at `argos.twenty-internal.com`. These are dead references that are
no longer used by any CI workflow.

**Note:** Story `parameters.chromatic: { disableSnapshot: true }`
entries are intentionally kept — the Argos plugin reads them as a
fallback.

## Test plan

- Verify `yarn install` succeeds after dependency removal
- Verify no workflow references `chromatic` or `nx chromatic`
2026-06-04 13:15:23 +00:00
Charles BochetandGitHub badeaddd37 fix(ci): ensure ui-sb-test runs on push to main (#21222)
## Summary

- Add explicit `if: always() && needs.ui-sb-build.result == 'success'`
to `ui-sb-test` job

## Context

After merging #21217, the main-branch Argos baseline pipeline doesn't
work: `ui-sb-test` is silently skipped on push to main, so no
`argos-screenshots-twenty-ui` artifact is produced.

**Root cause:** `changed-files-check` is skipped on push events
(PR-only). `ui-sb-build` handles this with `if: always() && ...`, but
`ui-sb-test` has no explicit `if` — GitHub Actions propagates the skip
through the transitive dependency chain (`changed-files-check` →
`ui-sb-build` → `ui-sb-test`).

## Test plan

- Merge this PR and verify the next push to main produces the
`argos-screenshots-twenty-ui` artifact
- Verify `dispatch-main` successfully triggers ci-privileged with the
artifact
2026-06-04 15:11:38 +02:00
ehconitin 8ce69d52ab refactor(call-recording): operation-based 2.9 upgrade sync with tested utils 2026-06-04 18:30:29 +05:30
ehconitin 9f76d44c22 fix(call-recording): use IconVideo for the call recording standard object 2026-06-04 18:30:29 +05:30
Charles BochetandGitHub 9042e8a542 feat(ci): Argos main baselines + local visual diff support (#21217)
## Summary

**CI: Main-branch Argos baselines**
- Run storybook build + screenshot capture on `push` to `main` in CI UI
workflow
- Add `dispatch-main` job in visual regression dispatch to forward
main-branch screenshots to ci-privileged
- Simplify `dispatch-pr` by inlining the artifact name and removing
unused `project` output

**Local visual diff support**
- Add `scripts/visual-diff.sh` for running Argos uploads locally via
tunnel
- Add `storybook:visual-diff` Nx target wrapping the script (depends on
`storybook:build`)
- Honor `STORYBOOK_URL` env in `vitest.config.ts` to reuse pre-served
static builds (mirrors twenty-front pattern)
- Support `ARGOS_BUILD_NAME`, `ARGOS_REFERENCE_BRANCH` env overrides in
vitest plugin config

## Context

Argos builds on PRs are all "Orphan" because there's no reference build
on `main` to compare against. The CI changes add the missing piece:
every merge to main now produces screenshots and uploads them to Argos
as reference builds.

The local visual diff script enables developers to run visual regression
checks from their machine against the self-hosted Argos instance via
`kubectl port-forward` (set up by the twenty-infra `argos-tunnel`
command).

## Related

- twentyhq/twenty-argos#1 (backend config for self-hosted HTTPS
redirect)
- twentyhq/twenty-infra#709 (argos-tunnel super CLI command +
self-hosted mode)

## Test plan

- [ ] Verify CI UI runs on next push to main and produces the
`argos-screenshots-twenty-ui` artifact
- [ ] Verify `dispatch-main` triggers and uploads screenshots to Argos
- [ ] Verify subsequent PR builds show diffs against the main baseline
instead of "Orphan"
- [ ] Run `ARGOS_TOKEN=<token> npx nx storybook:visual-diff twenty-ui`
locally with tunnel active
2026-06-04 14:55:08 +02:00
100b08e827 i18n - website translations (#21220)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-04 14:47:01 +02:00
437eed0862 fix(messaging): fix reply-quotation stripping that emptied email bodies (#21118)
some synced messages were stored with empty bodies, others with the
entire reply thread re-quoted, planer was stripping entirely quoted
forwards down to nothing and not trimming inline reply history at all

switched plaintext quote stripping to `email-reply-parser`, falling back
to the full text when it strips everything so forwards don't end up
blank. kept planer for the html path, and normalized body whitespac

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-06-04 12:31:53 +00:00
Rashad KaranouhandGitHub 5a55021e26 feat(website): add /partners/apply standalone page (#21219)
## Summary

- Adds a standalone `/partners/apply` page — a shareable URL that opens
the partner application wizard full-page (no modal, no nav, no footer),
on a plain black background
- Adds a `slots` prop to `PartnerApplicationWizard` so it can render
outside a `Dialog.Root` context (Base UI), keeping the existing modal on
`/partners` completely untouched
- Surfaces logic function errors to the user: the API route now checks
the webhook response body for `ok: true`, so a silent backend failure no
longer shows a false success state

## Test plan

- [ ] `yarn jest --no-coverage` — 365/365 passing
- [ ] `npx oxlint -c .oxlintrc.json .` — 0 errors (1 pre-existing
warning unrelated to this PR)
- [ ] `npx oxfmt --check .` — clean
- [ ] `npx tsc --noEmit` — clean
- [ ] Visit `/partners/apply` — wizard loads full-page on black
background, no menu, no footer
- [ ] Complete the wizard and submit — redirects to `/partners/list`
- [ ] Visit `/partners` — "Become a partner" modal still opens normally
2026-06-04 12:30:26 +00:00
Raphaël BosiandGitHub 63435a7937 Twenty new ui README (#21186)
Generated a twenty new ui implementation plan README after the
discussion with @charlesBochet
2026-06-04 12:14:43 +00:00
6a2c948e84 chore: bump version to 2.10.0 (#21218)
## Summary

- Moves current version to previous versions array
- Sets TWENTY_CURRENT_VERSION to the new version
- Updates TWENTY_NEXT_VERSIONS with the next minor version
- Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same
version

## Checklist

- [ ] Verify version constants are correct
- [ ] Verify npm package versions match

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-06-04 14:14:18 +02:00
Charles BochetandGitHub b53f1832d8 feat(ci): simplify visual regression dispatch to twenty-ui only (#21215)
## Summary

- Only trigger visual regression on `CI UI` workflow (drop `CI Front`)
- Remove tarball re-packaging step — `ci-privileged` now downloads the
artifact directly via GitHub API
- Remove `mode`/`project` parameters from the dispatch payload
(hardcoded to twenty-ui in ci-privileged)
- Pass `run_id` of the triggering CI UI workflow so ci-privileged can
fetch the correct artifact

## Context

Part of the fast visual regression CI initiative. The `ci-privileged`
workflow has been simplified to only handle `twenty-ui` screenshots
uploaded directly to Argos.

## Test plan

- [x] Full E2E verified on production: screenshots → Argos build → diff
results → PR comment
2026-06-04 13:49:44 +02:00
ehconitin e36d6290ea feat(call-recording): use snapshotted title as label identifier 2026-06-04 17:01:50 +05:30
ehconitin 33f3b4fbf9 fix(call-recording): inline immutable replace after upstream removed flat-entity util 2026-06-04 16:43:57 +05:30
ehconitin d8d02cb388 Merge remote-tracking branch 'upstream/main' into call-recording-standard-object 2026-06-04 16:24:20 +05:30
Paul RastoinandGitHub 3d49642d12 [AUDIT] Run knip over twenty-server (#21159)
# Introduction
Run [knip](https://knip.dev/) over twenty-server
Used config:
```json
{
  "$schema": "https://unpkg.com/knip@5/schema.json",
  "workspaces": {
    "packages/twenty-server": {
      "entry": [
        "src/main.ts",
        "src/command/command.ts",
        "src/queue-worker/queue-worker.ts",
        "src/database/scripts/setup-db.ts",
        "src/database/scripts/truncate-db.ts",
        "src/database/clickHouse/migrations/run-migrations.ts",
        "src/database/clickHouse/seeds/run-seeds.ts",
        "src/instrument.ts",
        "lingui.config.ts",
        "test/integration/graphql/codegen/index.ts",
        "test/integration/utils/setup-test.ts",
        "test/integration/utils/teardown-test.ts",
        "scripts/**/*.ts",
        "**/*.spec.ts",
        "**/*.integration-spec.ts"
      ],
      "project": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts"],
      "ignore": [
        "src/database/typeorm/**/migrations/**",
        "src/database/typeorm/**/*.entity.ts",
        "**/*.workspace-entity.ts",
        "**/logic-function-resource/constants/seed-project/**"
      ],
      "ignoreDependencies": ["@types/psl", "@types/aws-lambda"],
      "ignoreBinaries": ["nest", "lingui", "typeorm"]
    }
  }
}
```
2026-06-04 10:05:22 +00:00
ehconitin 73e619174f fix(call-recording): rename all colliding custom objects, not just the first 2026-06-04 15:10:37 +05:30
martmullandGitHub 4ad8d8e98e Set 200 code for post requests (#21214)
as title, nestJS use to set 201 for post requests but some services
(like google) requests 200 response code

See
https://discord.com/channels/1130383047699738754/1511054250971758642/1511785364027609099
for context
2026-06-04 09:14:51 +00:00
ehconitin 426f87dfe6 update field name sourceApplicationId to applicationId 2026-06-04 14:37:39 +05:30
ehconitin 807ef86907 Merge remote-tracking branch 'upstream/main' into call-recording-standard-object
# Conflicts:
#	packages/twenty-server/src/database/commands/upgrade-version-command/2-9/2-9-upgrade-version-command.module.ts
2026-06-04 14:29:20 +05:30
nitinandGitHub 475cd9e7fe fix(upgrade): resequence workflow step logs command (#21213) 2026-06-04 08:27:43 +00:00
ehconitin 7e7ff345da fix(call-recording): simplify failure state and calendar retention 2026-06-04 13:41:05 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f0be78e629 chore(deps-dev): bump prettier from 3.4.2 to 3.8.3 (#21205)
Bumps [prettier](https://github.com/prettier/prettier) from 3.4.2 to
3.8.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/prettier/prettier/releases">prettier's
releases</a>.</em></p>
<blockquote>
<h2>3.8.3</h2>
<ul>
<li>SCSS: Prevent trailing comma in <code>if()</code> function (<a
href="https://redirect.github.com/prettier/prettier/pull/18471">prettier/prettier#18471</a>
by <a href="https://github.com/kovsu"><code>@​kovsu</code></a>)</li>
</ul>
<p>🔗 <a
href="https://github.com/prettier/prettier/blob/3.8.3/CHANGELOG.md#383">Changelog</a></p>
<h2>3.8.2</h2>
<ul>
<li>Support Angular v21.2</li>
</ul>
<p>🔗 <a
href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md#382">Changelog</a></p>
<h2>3.8.1</h2>
<ul>
<li>Include available <code>printers</code> in plugin type declarations
(<a
href="https://redirect.github.com/prettier/prettier/pull/18706">#18706</a>
by <a href="https://github.com/porada"><code>@​porada</code></a>)</li>
</ul>
<p>🔗 <a
href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md#381">Changelog</a></p>
<h2>3.8.0</h2>
<ul>
<li>Support Angular v21.1</li>
</ul>
<p><a
href="https://github.com/prettier/prettier/compare/3.7.4...3.8.0">diff</a></p>
<p>🔗 <a href="https://prettier.io/blog/2026/01/14/3.8.0">Release note
&quot;Prettier 3.8: Support for Angular v21.1&quot;</a></p>
<h2>3.7.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix comment in union type gets duplicated by <a
href="https://github.com/fisker"><code>@​fisker</code></a> in <a
href="https://redirect.github.com/prettier/prettier/pull/18393">prettier/prettier#18393</a></li>
<li>Fix unstable comment print in union type by <a
href="https://github.com/fisker"><code>@​fisker</code></a> in <a
href="https://redirect.github.com/prettier/prettier/pull/18395">prettier/prettier#18395</a></li>
<li>Avoid quote around LWC interpolations by <a
href="https://github.com/kovsu"><code>@​kovsu</code></a> in <a
href="https://redirect.github.com/prettier/prettier/pull/18383">prettier/prettier#18383</a></li>
</ul>
<p>🔗 <a
href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md#374">Changelog</a></p>
<h2>3.7.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix <code>prettier.getFileInfo()</code> change that breaks VSCode
extension by <a
href="https://github.com/fisker"><code>@​fisker</code></a> in <a
href="https://redirect.github.com/prettier/prettier/pull/18375">prettier/prettier#18375</a></li>
</ul>
<p>🔗 <a
href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md#373">Changelog</a></p>
<h2>3.7.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix string print when switching quotes by <a
href="https://github.com/fisker"><code>@​fisker</code></a> in <a
href="https://redirect.github.com/prettier/prettier/pull/18351">prettier/prettier#18351</a></li>
<li>Preserve quote for embedded HTML attribute values by <a
href="https://github.com/kovsu"><code>@​kovsu</code></a> in <a
href="https://redirect.github.com/prettier/prettier/pull/18352">prettier/prettier#18352</a></li>
<li>Fix comment in empty type literal by <a
href="https://github.com/fisker"><code>@​fisker</code></a> in <a
href="https://redirect.github.com/prettier/prettier/pull/18364">prettier/prettier#18364</a></li>
</ul>
<p>🔗 <a
href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md#372">Changelog</a></p>
<h2>3.7.1</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md">prettier's
changelog</a>.</em></p>
<blockquote>
<h1>3.8.3</h1>
<p><a
href="https://github.com/prettier/prettier/compare/3.8.2...3.8.3">diff</a></p>
<h4>SCSS: Prevent trailing comma in <code>if()</code> function (<a
href="https://redirect.github.com/prettier/prettier/pull/18471">#18471</a>
by <a href="https://github.com/kovsu"><code>@​kovsu</code></a>)</h4>
<!-- raw HTML omitted -->
<pre lang="scss"><code>// Input
$value: if(sass(false): 1; else: -1);
<p>// Prettier 3.8.2
$value: if(
sass(false): 1; else: -1,
);</p>
<p>// Prettier 3.8.3
$value: if(sass(false): 1; else: -1);
</code></pre></p>
<h1>3.8.2</h1>
<p><a
href="https://github.com/prettier/prettier/compare/3.8.1...3.8.2">diff</a></p>
<h4>Angular: Support Angular v21.2 (<a
href="https://redirect.github.com/prettier/prettier/pull/18722">#18722</a>,
<a
href="https://redirect.github.com/prettier/prettier/pull/19034">#19034</a>
by <a href="https://github.com/fisker"><code>@​fisker</code></a>)</h4>
<p>Exhaustive typechecking with <code>@default never;</code></p>
<!-- raw HTML omitted -->
<pre lang="html"><code>&lt;!-- Input --&gt;
@switch (foo) {
  @case (1) {}
  @default never;
}
<p>&lt;!-- Prettier 3.8.1 --&gt;
SyntaxError: Incomplete block &quot;default never&quot;. If you meant to
write the @ character, you should use the &quot;&amp;<a
href="https://redirect.github.com/prettier/prettier/issues/64">#64</a>;&quot;
HTML entity instead. (3:3)</p>
<p>&lt;!-- Prettier 3.8.2 --&gt;
<a href="https://github.com/switch"><code>@​switch</code></a> (foo) {
<a href="https://github.com/case"><code>@​case</code></a> (1) {}
<a href="https://github.com/default"><code>@​default</code></a> never;
}
</code></pre></p>
<p><code>arrow function</code> and <code>instanceof</code>
expressions.</p>
<!-- raw HTML omitted -->
<pre lang="html"><code>&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/prettier/prettier/commit/d7108a79ec745c04292aabf22c4c1adbd690b191"><code>d7108a7</code></a>
Release 3.8.3</li>
<li><a
href="https://github.com/prettier/prettier/commit/177f90898170d363ef64fde663e4d13170688bfe"><code>177f908</code></a>
Prevent trailing comma in SCSS <code>if()</code> function (<a
href="https://redirect.github.com/prettier/prettier/issues/18471">#18471</a>)</li>
<li><a
href="https://github.com/prettier/prettier/commit/1cd40668c3d6f2f4cf9d87bbc9096d92361b2606"><code>1cd4066</code></a>
Release <code>@​prettier/plugin-oxc</code><a
href="https://github.com/0"><code>@​0</code></a>.1.4</li>
<li><a
href="https://github.com/prettier/prettier/commit/a8700e245038cd8cc0cf28ef06ffedbcb3fc2dfc"><code>a8700e2</code></a>
Update oxc-parser to v0.125.0</li>
<li><a
href="https://github.com/prettier/prettier/commit/752157c78eca6f0a30e5d5cb513b682c5ecfa01e"><code>752157c</code></a>
Fix tests</li>
<li><a
href="https://github.com/prettier/prettier/commit/053fd418e180b12fa2014260212fae831f5fc5ec"><code>053fd41</code></a>
Bump Prettier dependency to 3.8.2</li>
<li><a
href="https://github.com/prettier/prettier/commit/904c6365ec46726fd0e21021c52ae934b7e5abc6"><code>904c636</code></a>
Clean changelog_unreleased</li>
<li><a
href="https://github.com/prettier/prettier/commit/dc1f7fcc508d116cbf1644d69a1f0eb93e40d4a4"><code>dc1f7fc</code></a>
Update dependents count</li>
<li><a
href="https://github.com/prettier/prettier/commit/b31557cf331a02acf83e7e29d1001b070189a0d9"><code>b31557c</code></a>
Release 3.8.2</li>
<li><a
href="https://github.com/prettier/prettier/commit/96bbaeda0525bf758e464aed2f939d739a85c315"><code>96bbaed</code></a>
Support Angular v21.2 (<a
href="https://redirect.github.com/prettier/prettier/issues/18722">#18722</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/prettier/prettier/compare/3.4.2...3.8.3">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for prettier since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=prettier&package-manager=npm_and_yarn&previous-version=3.4.2&new-version=3.8.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 09:45:41 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fe5337b7d0 chore(deps-dev): bump @types/passport-microsoft from 2.1.0 to 2.1.1 (#21204)
Bumps
[@types/passport-microsoft](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/passport-microsoft)
from 2.1.0 to 2.1.1.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/passport-microsoft">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/passport-microsoft&package-manager=npm_and_yarn&previous-version=2.1.0&new-version=2.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 09:45:19 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f50e471385 chore(deps): bump @mantine/hooks from 8.3.15 to 8.3.18 (#21203)
Bumps
[@mantine/hooks](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/hooks)
from 8.3.15 to 8.3.18.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/mantinedev/mantine/releases">@​mantine/hooks's
releases</a>.</em></p>
<blockquote>
<h2>8.3.18</h2>
<p>This is the last 8.x release. You are welcome to test 9.0 alpha
version and provide feedback before its release on March 31 – <a
href="https://alpha.mantine.dev/changelog/9-0-0/">https://alpha.mantine.dev/changelog/9-0-0/</a></p>
<ul>
<li><code>[@mantine/core]</code> PasswordInput: Fix styles api props not
resolving correctly in theme (<a
href="https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/hooks/issues/8716">#8716</a>)</li>
</ul>
<h2>8.3.17</h2>
<h2>Changes</h2>
<ul>
<li><code>[@mantine/core]</code> Stepper: Fix Google Translate
compatibility issues (<a
href="https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/hooks/issues/8744">#8744</a>)</li>
<li><code>[@mantine/hooks]</code> use-list-state: Add memoization to all
handlers (<a
href="https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/hooks/issues/8739">#8739</a>)</li>
</ul>
<h2>8.3.16</h2>
<h2>What's Changed</h2>
<ul>
<li><code>[@mantine/modals]</code> Fix <code>onClose</code> being called
multiple times (<a
href="https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/hooks/issues/8727">#8727</a>)</li>
<li><code>[@mantine/core]</code> Tooltip: Fix component not throwing
erro when used with string (<a
href="https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/hooks/issues/8694">#8694</a>)</li>
<li><code>[@mantine/core]</code> NumberInput: Fix incorrect decimal
separator parsing in <code>onPaste</code></li>
<li><code>[@mantine/core]</code> AppShell: Fix
<code>layout=&quot;alt&quot;</code> not working with
<code>mode=&quot;static&quot;</code></li>
<li><code>[@mantine/stotlight]</code> Fix actions list being rendered
when nothing found message was not set (<a
href="https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/hooks/issues/8592">#8592</a>)</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/mantinedev/mantine/compare/8.3.15...8.3.16">https://github.com/mantinedev/mantine/compare/8.3.15...8.3.16</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/mantinedev/mantine/commit/530249feb498d74d5ea849d2984c86efd072f38c"><code>530249f</code></a>
[release] Version: 8.3.18</li>
<li><a
href="https://github.com/mantinedev/mantine/commit/bac61d6fc21be84bd80d1d9232a605c05692ae75"><code>bac61d6</code></a>
[release] Version: 8.3.17</li>
<li><a
href="https://github.com/mantinedev/mantine/commit/6b3fdee137584aece43b88b62d8c207a58c35295"><code>6b3fdee</code></a>
[refactor] Fix formatting</li>
<li><a
href="https://github.com/mantinedev/mantine/commit/c048f996cc1a8287bd9eaba1bb59bc6637316b90"><code>c048f99</code></a>
[<code>@​mantine/hooks</code>] use-list-state: Add memoization to all
handlers (<a
href="https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/hooks/issues/8739">#8739</a>)</li>
<li><a
href="https://github.com/mantinedev/mantine/commit/dbb8732ca3350259e0cf7b2536d0bcc885b1b587"><code>dbb8732</code></a>
[release] Version: 8.3.16</li>
<li>See full diff in <a
href="https://github.com/mantinedev/mantine/commits/8.3.18/packages/@mantine/hooks">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@mantine/hooks&package-manager=npm_and_yarn&previous-version=8.3.15&new-version=8.3.18)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 09:45:10 +02:00
Félix MalfaitandGitHub a3a44c8315 fix(front): settings skeleton, app-detail header & empty favorites (#21209)
Three small post-redesign UI fixes. Each is an independent commit, so
they can be split into separate PRs if preferred.

## 1. Settings loading skeleton — match the rounded-card layout

The redesign (#21131) moved settings chrome into a rounded card
(`SettingsPageLayout`: bordered header with breadcrumb + centered title,
optional secondary bar, 760px body), but `SettingsSkeletonLoader` still
rendered the old flat `PageHeader` + `PageBody` — so pages painted as a
full-width flat bar then snapped into the card.

- `SettingsSkeletonLoader` now reproduces the card and **reuses the real
`SettingsPageHeader` + `SettingsPageContainer`**, so the frame aligns by
construction; the card CSS is replicated (not `SettingsPageLayout`) to
avoid the layout's side effects (hotkeys, side panel, info banner).
- It's **composed with `SettingsSectionSkeletonLoader`** so the loading
body is identical whether or not chrome is present. Rule: no chrome on
screen yet → full-page skeleton; chrome already on screen → body-only
`SettingsSectionSkeletonLoader` (the admin Enterprise tab now uses it,
matching its sibling tabs). A short comment on each component documents
this.

## 2. Application detail header — pass a plain title

`SettingsApplicationDetails` / `SettingsAvailableApplicationDetails`
passed a custom `SettingsApplicationDetailTitle` (avatar + name +
multi-line description, fixed width) into `SettingsPageLayout`'s
**centered single-line title slot**, which broke the header. They now
pass the app's display name like every other page. The available-app
"unlisted" notice moves into the body as a reusable `InlineBanner`; the
now-unused `SettingsApplicationDetailTitle` is removed.

## 3. Navigation — hide Favorites when empty

Always rendering the Favorites section (#21087) left a stray "Favorites"
title above Workspace for users with no favorites. It now renders only
when at least one favorite exists (redundant per-child guards dropped).
Note: the "+ add favorite" entry point therefore appears once you have
≥1 favorite; the first favorite is created from a record/view as before.

## Verification
- `nx typecheck twenty-front`  · `oxlint` + `oxfmt --check` on changed
files 
- i18n catalogs intentionally untouched — handled by the repo's separate
i18n pipeline.
2026-06-04 09:32:36 +02:00
e99116b0a5 chore: sync AI model catalog from models.dev (#21212)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-06-04 09:31:01 +02:00
ehconitin 69219d8871 fix(call-recording): resolve upgrade command timestamp collision with upstream 2026-06-04 12:22:04 +05:30
Félix MalfaitandGitHub 2ac515894b feat(settings): add Logs as a dedicated tab in General settings (#21180)
## What & why

The audit-log viewer lived as a full-screen page reachable only via a
"View Logs" button buried in the **Security** tab. This surfaces it as
the **third tab in General settings** (`General | Security | Logs`),
consistent with the other tabs.

## Changes

- **Relocated** the event-logs module
`pages/settings/security/event-logs/` → `modules/settings/event-logs/`
and render it as tab content instead of a `FullScreenContainer` page.
Dropped `SettingsPath.EventLogs`, its route, and the fullscreen handling
in favor of the `general#logs` hash tab.
- **Security tab:** removed the "View Logs" entry; kept the
log-retention setting there.
- **In-tab gating** (shown to users with the Security permission):
Enterprise upgrade card when not entitled, a clear "ClickHouse not
configured" placeholder otherwise (derived from client config), and the
query is skipped when disabled. Replaces a bespoke error component that
string-matched error messages with the shared `SettingsEmptyPlaceholder`
/ `SettingsEnterpriseFeatureGateCard`.
- **Layout:** boxed content column with the table selector + filters
grouped in a `Card` and the results table below, matching settings
conventions. Kept the existing fixed filters (page/event name, member,
period) rather than recreating the record-view filter chips (those are
tightly coupled to record/view context).

Frontend + `twenty-shared` only — no changes to the log query or data.

## Test plan

- [x] `npx nx typecheck twenty-front` and `npx nx lint twenty-front`
pass
- [x] Settings → General shows three tabs; Logs is the third; breadcrumb
stays "Workspace / General"
- [x] With Enterprise + ClickHouse: table selector, filters, refresh,
and the paginated table work
- [x] Non-Enterprise: Enterprise upgrade card shown; no failing query
fires
- [ ] Enterprise without ClickHouse: shows the "ClickHouse not
configured" placeholder
- [ ] Security tab still shows the log-retention setting and the "View
Logs" button is gone
- [ ] A user without the Security permission sees neither the Security
nor Logs tab
2026-06-04 08:47:23 +02:00
Charles BochetandGitHub e6614299c6 feat(ci): integrate Argos visual regression via vitest screenshots (#21210)
## Summary

- Adds `@argos-ci/storybook` vitest plugin to `twenty-ui` for automatic
screenshot capture during vitest storybook tests
- Uploads captured screenshots (PNG, ~5MB) as a CI artifact instead of
passing the full storybook build
- Updates the visual regression dispatch workflow to pass
`mode=argos-screenshots` to ci-privileged, which then uploads
screenshots to Argos via CLI

This replaces the 10-minute Storybook screenshot capture with a ~30s
vitest browser-mode approach. The heavy screenshot work happens on free
public runners, while ci-privileged only handles the Argos API upload
(keeping secrets private).

## Architecture

```
twenty (public, free runners)          ci-privileged (private)
─────────────────────────────          ────────────────────────
1. Build storybook-static              4. Download screenshots artifact
2. Vitest captures screenshots         5. `argos upload` → Argos API
3. Upload screenshots artifact         6. Poll for results
                                       7. Post PR comment
```

## Test plan

- [x] Verified locally: vitest captures 225 screenshots in ~28s
- [x] Verified `@argos-ci/cli upload` successfully creates Argos build
from captured screenshots
- [x] Argos diffs computed and results visible via API
- [ ] CI runs end-to-end on a PR
2026-06-04 08:46:37 +02:00
ehconitin 7a60706437 Merge remote-tracking branch 'upstream/main' into call-recording-standard-object
# Conflicts:
#	packages/twenty-client-sdk/src/metadata/generated/schema.graphql
#	packages/twenty-client-sdk/src/metadata/generated/schema.ts
#	packages/twenty-server/src/database/commands/upgrade-version-command/2-9/2-9-upgrade-version-command.module.ts
#	packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
#	packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/__snapshots__/get-standard-object-metadata-related-entity-ids.util.spec.ts.snap
#	packages/twenty-shared/src/types/FeatureFlagKey.ts
2026-06-04 12:02:17 +05:30
Abdullah.andGitHub ccffc4a1ea Fix axios related dependabot alerts generated against root yarn.lock (#21187)
Fixes the following Dependabot alerts:
https://github.com/twentyhq/twenty/security/dependabot?q=is%3Aopen+package%3Aaxios+manifest%3Ayarn.lock+has%3Apatch

Upgraded the referenced version in root yarn.lock. Creating a separate
PR for the nested ones to keep the updates isolated (e.g.
/seed-dependencies/yarn.lock).
2026-06-03 23:19:42 +00:00
ehconitin 62473fc5ea refactor(call-recording): collapse junction into direct CalendarEvent relation 2026-06-04 02:19:32 +05:30
685c0ddd12 i18n - docs translations (#21195)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-03 21:43:27 +02:00
EtienneandGitHub 15eaabdbc1 fix(ai) - optimize crud tools (#21133)
- **Add delete many**, `delete_many_{object}` added alongside the
existing `delete_one_{object}`.
- **Uniformize naming**, crud module, type names, and MCP helper
constants renamed for consistency.
- **Optimize tool schema (learn phase)**
  - `find_many(_companies)`: **7 158 → 2 700 tokens**
  - `find_one(_company)`: **280 → 126 tokens**
  -  ....
- Main mechanism: `reused: 'ref'` (line 7 of
`to-tool-json-schema.util.ts`). Zod walks the schema tree, tracks which
Zod schema instances appear more than once, and emits each reused
instance exactly once in `$defs`, replacing all subsequent occurrences
with a `$ref`. Works because filter and value schemas are now extracted
as shared objects.

- **Optimize system prompt (tool catalog)**, DATABASE_CRUD section
restructured to list operation patterns (`find_many_{object}`, …) once +
objects once, instead of the full N×M cross-product of tool names.
- **Optimize execute_tool**, shared record-properties schema (same
`$defs` deduplication applies at call time); introduced `upsert_many`;
added `selectedFields` to `find_*` so the agent only fetches the fields
it needs.
2026-06-03 17:57:40 +00:00
Abdullah.andGitHub 50c9b68e81 Update axios in package.json of seed-dependencies (#21190)
Fixes the following Dependabot alerts:
https://github.com/twentyhq/twenty/security/dependabot?q=is%3Aopen+package%3Aaxios+manifest%3Apackages%2Ftwenty-server%2Fsrc%2Fengine%2Fcore-modules%2Fapplication%2Fapplication-package%2Fconstants%2Fseed-dependencies%2Fyarn.lock+has%3Apatch

Follow up to the alerts not addressed in
[this](https://github.com/twentyhq/twenty/pull/21187) PR.
2026-06-03 17:49:02 +00:00
34d893f9fd i18n - translations (#21192)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-03 19:57:05 +02:00
Priyanshu BartwalGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>Etienne
d3a7ea0790 Fix: Not able to add multiple handles to Blocklist in settings/accounts (#21049)
Fixes: #21031

### Root cause:
This feature was never fully implemented even though the placeholder
text suggested it was supported. It could only update one handle at a
time.

### Fix
Fixed the zod validation to validate each handle separately.
Used `useCreateManyRecords` to update multiple handles at the same time.

### Before:
<img width="2032" height="1162" alt="Screenshot 2026-05-29 at 3 25
12 PM"
src="https://github.com/user-attachments/assets/ae6b6ae3-ed38-4410-801e-11f514773681"
/>

### After:


https://github.com/user-attachments/assets/69354129-422a-41de-baf7-fa5a28f01f3f

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-03 17:34:51 +00:00
49828d9379 i18n - translations (#21191)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-03 19:17:44 +02:00
EtienneandGitHub dd0039ca1c feat(mcp) - optimize instruction prompt and hide get_tool_catalog (#21183)
Workspace-aware initialize.instructions

- Deleted the static mcp-server-instructions.const.ts
- Created build-mcp-server-instructions.util.ts — a comprehensive system
prompt with identity, object list, tool grammar, routing decision tree,
intent mapping, skills vs tools, safety constraints, and data efficiency
guidelines
- Created McpInstructionBuilderService — fetches workspace-specific
object names + skill names and injects them into the instructions


Hide/deprecate get_tool_catalog

Benefit : skip first MCP call (tools are included in instruction)
2026-06-03 16:58:32 +00:00
MarieandGitHub 4b15b949f3 Provide additional logsobservability to workflow runs (per node) (#21142)
Surfaces per-step "Logs" tabs in the workflow run side panel so users
can see what each step actually did (model + tokens + tool calls for AI,
console output for serverless functions, request/response for HTTP,
recipients/body for Email).

<img width="546" height="501" alt="ai_agent_without_websearch"
src="https://github.com/user-attachments/assets/c6ca3518-9489-4484-a570-3d0569ff3b03"
/>

## Storage

- New `stepLogs` JSONB column on the `workflowRun` workspace entity,
typed as `Record<string, WorkflowRunStepLog>` (keyed by step id).
- Schema lives in `twenty-shared`: `workflowRunStepLogSchema` with a
discriminated `details.type` union for `AI_AGENT | CODE | HTTP_REQUEST |
EMAIL` — frontends and backends consume the same Zod-inferred type.
- Field is added to existing workspaces via a workspace upgrade command
(`2-9 add-workflow-run-step-logs-field`); the standard-object metadata
declares it for new workspaces.
- Writes happen atomically per step in
`WorkflowRunStepLogWorkspaceService.setStepLog` using `jsonb_set`. That
lets concurrent steps in the same run write their own keys without
contending with the existing lock around `workflowRun.state`.
- Per-step payload is hard-capped at 256 KB; anything larger is dropped
with a `logger.warn`, so a pathological tool call can never bloat a row.
See below for more information.

## How logs are produced

**Aalmost everything was already being collected; this PR mostly
persists and renders it.**

- **AI agent** — `AgentAsyncExecutorService` already tracked token
usage, model id, native web-search count, and the AI SDK's `steps[]`. We
map those into the log via `mapAiStepsToToolCallLogs` (`searchVector`
stripped from record outputs, per-call input/output capped at 32/64 KB,
max 200 tool calls per step). The only new measurement is a wall-clock
`durationMs` taken around `executeAgent`, and we now fold native
web-search cost into the displayed `totalCostInDollars` (it was already
billed, just not shown).
- **Code / serverless function** — reuses the `console.log` output the
function runner already returns (`logsByLevel`);
`build-code-step-log.util` only repackages it.
- **HTTP request** — built from the action's existing input/output via
`build-http-request-step-log.util`. No new signals collected.
- **Email (send / draft)** — added `sanitizedHtmlBody` + `plainTextBody`
to the existing tool outputs (a small additive change), then
`build-email-step-log.util` consumes them.

No additional AI inference or external calls are made for logging — the
cost is a small CPU overhead per step plus the JSONB write.

## Security

The log surface intentionally shows whatever the workflow touched, which
made redaction and sanitization the main design concern.

- **HTTP — secrets in headers**: existing `SENSITIVE_HEADER_NAMES` set
(Authorization, Cookie, …) replaced with `[redacted]` in both request
and response.
- **HTTP — secrets in URLs**: `SENSITIVE_URL_PARAM_NAMES` (e.g.
`api_key`, `token`, `access_token`) replaced in the query string via
`URL`-based parsing.
- **HTTP — secrets in bodies**: `SENSITIVE_BODY_KEY_REGEX` deep-walks
JSON request/response bodies (object input or stringified JSON) and
redacts matching keys. Applied to the `error` field too, since
transport-layer errors sometimes embed structured payloads.
- **Email — XSS risk in body preview**: tool outputs now expose a
server-side `sanitizedHtmlBody`; the log builder prefers it over the raw
user-authored `input.body`, with `plainTextBody` as a second fallback.
The original raw body is only used if sanitization didn't happen (e.g.
tool failed before composing).
- **AI — internal/noisy data**: `searchVector` (Postgres tsvector
strings) is stripped from record outputs returned by Twenty tools to
avoid leaking internal full-text-search payloads.
- **DB bloat / runaway agents**: 256 KB per-step cap + 32 KB / 64 KB
per-tool-call input/output cap + 200 tool calls per step.

<img width="547" height="307" alt="logic_function"
src="https://github.com/user-attachments/assets/dd4a3d16-67f2-434b-95b3-bdcaf9ed053d"
/>

## More details on Log size & truncation

Logs are stored in `workflowRun.stepLogs` (JSONB), keyed by `stepId`.

### Per-step cap

Each step's log is hard-capped at **256 KB** (`MAX_STEP_LOG_BYTES` in
`WorkflowRunStepLogWorkspaceService.setStepLog`).

For ~99% of workflows this is roomy — typical real-world sizes:
- Code / serverless function: 1–20 KB
- HTTP request: 5–70 KB
- Email: 5–30 KB
- AI agent (a handful of tool calls): 5–50 KB

### Two layers of bounding

1. **Per-field truncation** in each builder (before writing):
   - **Code**: ≤ 500 entries, ≤ 4 KB per message, ≤ 8 KB stack trace
   - **HTTP**: ≤ 32 KB per body (request + response), UTF-8 byte-aware
   - **Email**: ≤ 8 KB body preview, UTF-8 byte-aware
- **AI agent**: ≤ 32 KB tool input, ≤ 64 KB tool output, ≤ 200 tool
calls/step

2. **Global per-step safety net** at write time: if the assembled
`stepLog` still exceeds 256 KB, the write is **dropped entirely** with a
`logger.warn`. The workflow itself keeps running unaffected.

### What this means in practice

- **Safe**: workflow execution, step results, downstream steps — never
blocked by log size.
- **Safe**: iterators (each iteration overwrites the previous log for
that `stepId`, so they can't accumulate).
- **Safe**: step retries (same `stepId` is overwritten, not appended).
- **Possible**: an AI agent step with many large tool outputs (e.g., 50+
heavy `web_search` calls) can exceed 256 KB → the **entire** step's log
is dropped, side panel shows "No logs were recorded for this step". The
user has no explicit signal that the log was dropped due to size (only
server-side warn).
- **Possible** (theoretical): a workflow with hundreds of distinct steps
could push the row toward Postgres's internal ~256 MB jsonb limit.
Beyond that, individual `jsonb_set` writes would error and be swallowed
by the action's try/catch — workflow still completes.

### Possible future hardening (not in this PR)

- Replace "drop entire log" with a stub that preserves the summary card
(cost, duration, status) and marks `truncated.reason = 'size_cap'`.
- Surface size-drops in the UI (similar to the existing
`<StyledTruncatedNotice>`).
- Emit a metric so dropped logs are observable in dashboards.
2026-06-03 16:53:47 +00:00
martmullandGitHub 0671ff3de5 Fix lambda error (#21179)
- move twenty-sdk from dependencies to devDependencies
- add documentation about breaking
- add warning about moving the package to dev dependencies
2026-06-03 16:45:51 +00:00
EtienneandGitHub 7fcbc45017 perf(ai) - per-tool schema resolution in resolveSchemas (#21188)
learn_tools(["find_many_companies"]) goes from ~170ms → ~2ms (85x
faster, measured with 40 objects / empty fields — real workspaces would
see even bigger savings). Previously it generated schemas for all ~250
tools and discarded 249;

now it generates exactly the requested one(s).
2026-06-03 16:29:26 +00:00
c7365b09e2 i18n - translations (#21189)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-03 17:49:40 +02:00
Thomas TrompetteandGitHub ac0368e876 fix: eliminate workflow editing flicker on active-to-draft transitions (#21176)
## Before

https://github.com/user-attachments/assets/5108a9d8-2017-41d5-855c-98714cbd4237

## After

https://github.com/user-attachments/assets/0a78d1e1-354f-4f3f-8ec4-6f46517619e4

## Summary
- Fixes visual flickering/glitching in the workflow show page header and
canvas when editing an active workflow or discarding a draft
- Root cause: SSE events re-added discarded drafts to Apollo cache, and
multiple hook instances had independent state causing version
oscillation between DRAFT and ACTIVE
- Rewrites `useWorkflowWithCurrentVersion` with a module-level
`discardedDraftId` variable shared across all instances, Apollo cache
seeding in mutation callbacks, and `lastValidResult` caching to prevent
null renders

## Test plan
- [x] Open a workflow show page with an ACTIVE workflow
- [x] Drag a node to change position → verify no flicker, status shows
DRAFT smoothly
- [x] Discard the draft → verify header does NOT flicker between
DRAFT/ACTIVE, position resets cleanly
- [x] Click on manual trigger and edit settings → verify the edit works
(draft created, settings saved)
- [x] Repeat discard + edit cycle multiple times to confirm stability
2026-06-03 15:38:38 +00:00
WeikoandGitHub cc76b7bc50 Fix fields widget new field visibility (#21111)
Fixes https://github.com/twentyhq/twenty/issues/21043

## Context
Newly created fields were never added to FIELDS widgets, regardless of
the "Set fields created in the future as visible" toggle. The widget's
newFieldDefaultVisibility was null on widgets that never explicitly set
it (it was never populated at creation), so the backend skipped them and
no view field was created.

## Implementation
Keep newFieldDefaultVisibility nullable with false (not visible) as the
behavior when not provided.
The FE now reflects that properly and shows "un-toggled" when it's null
(iso with BE behavior).

The fix also ensures the value is explicitly set to true wherever it
should be:
- Set newFieldDefaultVisibility: true at every FIELDS widget creation
path (backend default record-page layout, frontend
createDefaultFieldsWidget + useTemporaryFieldsConfiguration);
- Added a 2-9 workspace upgrade command that backfills true onto
existing standard FIELDS widgets where the value is null.
2026-06-03 15:29:57 +00:00
e64e5662e5 fix(ai-chat): refresh JWT token on SSE reconnect to prevent login red… (#20176)
Closes #18928

## Problem

When a JWT access token expires while the AI chat is streaming a
response, the SSE connection drops and `graphql-sse` calls the retry
callback. The previous implementation would wait, then destroy the SSE
client but never refreshed the token. On the next connection attempt the
client reused the same expired token, eventually triggering an
`UNAUTHENTICATED` error that redirected the user to the login screen.

## Solution

Add proactive token renewal inside `useHandleSseClientConnectionRetry`
before each reconnect attempt:

- Uses a module-level `let renewalPromise` variable to deduplicate
concurrent renewal requests , the exactpattern used in
`ApolloFactory.ts`
- Calls `renewToken` via `retryWithBackoff` against the `/metadata`
endpoint
- Writes the fresh token pair into the Jotai store ,the SSE client's
`headers()` callback picks it up automatically on reconnect
- If renewal fails -> falls back to destroying the SSE client as before

## Files changed

-
`packages/twenty-front/src/modules/sse-db-event/hooks/useHandleSseClientConnectionRetry.ts`

## Notes

This addresses the two issues from the previous review:
- No `useRef`  using module-level variable instead
- CI passing  removed the `CombinedGraphQLErrors` import

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-03 15:27:30 +00:00
ehconitin ff7819a353 fix 2026-06-03 20:48:07 +05:30
Paul RastoinandGitHub 164a5b1e8d Refactor email composer (#21177)
# Introduction
Gate what connected account can be ingested in case of ai mcp user
workspace agnostic funnel to only the workspace shared connected account

Added a quick win intregration tests on seeded connected accounts ( that
wasn't covered but already protected fix impacts only the mcp )

Refactored the API slightly too

## Notice
This mean there's a breaking change in the product behavior
Whereas before a non user workspace related mcp interaction would might
have fallback on any private user connected account it will now only
search for workspace visible listed ones
2026-06-03 15:15:51 +00:00
ehconitin 94badfd1c6 collision navigation command menu 2026-06-03 20:26:40 +05:30
ehconitin 9a8e3e8bc6 gate navigation commands with featrue flag 2026-06-03 19:41:26 +05:30
ehconitin 0a510a68de Merge remote-tracking branch 'upstream/main' into call-recording-standard-object 2026-06-03 18:36:53 +05:30
ehconitin bfe1046af4 feat(call-recording): sync full metadata graph in upgrade command, refine views, extract collision helpers 2026-06-03 18:36:02 +05:30
Rashad KaranouhandGitHub 8c3a93871e fix: update yarn.lock after removing dotenv and zod from twenty-sdk (#21174)
## Summary

- Commit b330105470 removed `dotenv` and `zod` from
`packages/twenty-sdk/package.json` dependencies but did not run `yarn
install`, leaving the lockfile out of sync.
- `yarn install --immutable` in CI was failing with `YN0028: The
lockfile would have been modified by this install` on those two
packages.
- This PR just runs `yarn install` to sync the lockfile — no logic
changes.

## Root cause

[Failing CI
run](https://github.com/twentyhq/twenty-infra/actions/runs/26883193920/job/79288298420):
```
YN0028: -    dotenv: "npm:^16.4.0"
YN0028: -    zod: "npm:^4.1.11"
YN0028: The lockfile would have been modified by this install, which is explicitly forbidden.
```

## Changes

`yarn.lock` only — removes the `dotenv@^16.4.0` range and the
`twenty-sdk` metadata entries for `dotenv` and `zod`.
2026-06-03 12:23:48 +00:00
0a86a6c6db i18n - website translations (#21165)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-03 14:10:35 +02:00
martmullandGitHub b330105470 Fix Unzipped size must be smaller than 250Mb error (#21172)
move dev dependencies into devDependencies
2026-06-03 11:39:11 +00:00
ehconitin be9b7a5222 Merge remote-tracking branch 'upstream/main' into call-recording-standard-object 2026-06-03 15:04:28 +05:30
Rashad KaranouhandGitHub c2ad3f3614 (partners): bump app version 0.3.3 -> 0.3.4 (#21167)
Forgot to bump the version when merging #21162 from `0.3.3` to `0.3.4`
2026-06-03 07:34:28 +00:00
0d9f7673c9 chore: sync AI model catalog from models.dev (#21170)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-06-03 09:34:21 +02:00
Rashad KaranouhandGitHub 6b5a956c85 feat(website): partner marketplace UI fixes & CTA improvements (#21163)
## Summary

Partner marketplace UI fixes and CTA improvements on the website
(`twenty-website`).

## Changes

- **Find a partner buttons** → link to `/partners/list` (hero + signoff
sections) instead of opening the contact modal.
- **Chip row layout fix** → align partner chip rows to the first chip's
baseline. In the narrow 3-column grid, category chips wrap to multiple
lines; centering floated the label to the middle and visually lifted the
first chip into the row above (e.g. "Custom Development" appeared under
Languages). Baseline keeps the label pinned beside the first chip.
- **Card CTA** → replace the conditional "Book a call" calendar CTA with
a "View profile" link to the partner profile page, shown on every card
regardless of whether a calendar link exists. Keeps card heights
consistent across the grid.
- **Card avatar** → show the partner's real profile picture when a safe
`http(s)` URL is present, keeping the initials block as the fallback.
- **Profile "Contact <partner>" CTA** → when a partner has no booking
(calendar) link, show a "Contact <partner>" button (alongside LinkedIn
if present) that opens a `mailto:rashad@twenty.com` with a partner-named
subject and a pre-filled body prompt.

## Tests

- New: `PartnerAvatar`, `PartnerProfileCtas` unit tests.
- Updated: `PartnerCard` tests for the View-profile CTA.
- All `Partner*` website tests pass.

## Screenshots 
<img width="1440" height="816" alt="Screenshot 2026-06-02 at 23 40 55"
src="https://github.com/user-attachments/assets/bd88a9be-3297-4d7f-891c-c9d403d2b4d9"
/>
<img width="1441" height="818" alt="Screenshot 2026-06-02 at 23 40 47"
src="https://github.com/user-attachments/assets/b315424f-48d1-4d29-9e97-1fcf4d8c47f2"
/>
<img width="458" height="505" alt="Screenshot 2026-06-02 at 23 40 18"
src="https://github.com/user-attachments/assets/2a4e0ec2-8e0a-4220-84ec-177c788aa580"
/>
2026-06-02 20:22:41 +00:00
Rashad KaranouhandGitHub ff5d082e7c feat(partners): remove Project Budget Typical field, rework partner views & nav order (#21162)
## Summary

Partners-app changes spanning the Partner object, its data scripts,
table views, and sidebar navigation.

### Remove the "Project Budget Typical" field
Dropped the `projectBudgetTypical` currency field from the Partner
object and every reference to it:
- `get-partner-by-slug` and `list-available-partners` logic-function
selections
- the seed script (type, write mapping, and per-partner data)
- the `import-from-tft` mapping (also dropping the now-unused
`partnerBudgetAverage` TFT source selection)

`projectBudgetMin` is intentionally kept.

### Rework partner views
- **Partners** (all-partners) view: replaced the **Deployment
Expertise** column with **Categories** (the `partnerScope` field).
- **Validated partners** view: added a **Languages Spoken** column.
- Set view `position`s so the in-object view switcher orders **Validated
→ Applications → Partners**.

### Navigation order
Reordered the "Partners" folder navigation items so the sidebar reads
**Validated partners → Partner applications → Partners** (Partner
content stays last).

### Also included
The previously-pushed fix that excludes partners with an empty slug from
the available-partners list.

## Notes
- No deploy/sync performed. The view-column and navigation-ordering
changes take effect once the app manifest is synced (`yarn twenty dev
--once` locally).
- The `deploymentExpertise` field itself is unchanged — only its column
was removed from the all-partners view.
2026-06-02 20:19:07 +00:00
ehconitin f770acbdee restore call-recording-calendar-event-association index view 2026-06-03 01:26:41 +05:30
ehconitin c68ff85f49 strip out unecessary code 2026-06-03 00:47:37 +05:30
ehconitin 7cc89c0811 regen 2026-06-02 23:01:42 +05:30
ehconitin 81da1bc2ac conflicts 2026-06-02 22:52:04 +05:30
ehconitin c2d3edb0f6 Merge remote-tracking branch 'upstream/main' into call-recording-standard-object 2026-06-02 22:51:55 +05:30
ehconitin c821869f9e chore: regenerate graphql and sdk metadata client for IS_CALL_RECORDING_ENABLED 2026-06-02 22:28:17 +05:30
ehconitin c36c1c7675 feat: add CallRecording standard object + 2.9 workspace sync command with custom-object collision rename 2026-06-02 21:59:45 +05:30
Thomas TrompetteandGitHub 015b5b6919 fix(twenty-server): fix AI tools description for SELECT view filter values (#21156)
Fixes https://github.com/twentyhq/twenty/issues/20840

## Summary
- Fix the AI tools factory (`view-filter-tools.factory.ts`) description
that incorrectly described SELECT filter values as plain strings,
causing agents to write values like `"CLOSED_LOST"` instead of the
required array format `["CLOSED_LOST"]`

This led to production crashes when later trying to update/delete select
options on fields that had view filters created by AI agents with
invalid format.

## Test plan
- No behavior change in existing code — only the tool description is
updated to guide AI agents correctly
- Manual: confirm AI agents now create SELECT filters with array values
2026-06-02 15:59:26 +00:00
Charles BochetandGitHub f6a17bc5f7 refactor(server): split LambdaDriver and LocalDriver into focused sub-services (#21153)
## Why

`LambdaDriver` (1.4k lines) and `LocalDriver` (709 lines) each mixed
many unrelated concerns in a single class.

## What changed

**This PR only moves code** — every line is byte-for-byte the same as
`main` (same logic, same comments, same constants, same lock keys/TTLs,
same error mapping). The drivers are now thin orchestrators wiring
co-located sub-services + pure utils (each with unit tests).

| File | Before | After |
|---|---|---|
| `lambda.driver.ts` | 1425 | 252 |
| `local.driver.ts`  | 709  | 231 |
| Largest sub-service | — | 371 (`lambda-executor-manager`) |
| Unit tests on these paths | 0 | 15 |

Lambda split: `lambda-aws-client` / `lambda-tool-functions` /
`lambda-layer-manager` / `lambda-executor-manager` + `constants` +
`types` + 5 pure utils.

Local split: `local-layer-manager` / `local-child-process-runner` /
`local-prebuilt-bundle` + `constants` + `types` + 3 pure utils.

No behavior change, no signature change, no public-API change.
2026-06-02 15:43:11 +00:00
Raphaël BosiandGitHub 1833fa84a5 Fix front component pointer/mouse event coordinates (#21117)
Fixes https://github.com/twentyhq/twenty/issues/21000

Front-component event handlers read standard event fields
(event.clientX, event.offsetX, …), but these were always undefined. On
the remote side, serialized event data was passed only as the
CustomEvent's detail — and CustomEvent ignores every constructor option
except detail, so the values lived at event.detail.clientX and never on
the event object itself.

- Added `applySerializedEventProperties`, to copy a curated allowlist of
event-level keys onto the event. Element/target state (value, checked,
files, scroll, media props) stays in
`applySerializedEventTargetProperties`, applied to this (the dispatch
element = event.target).
- Added x/y to `SerializedEventData` and to host-side serialization in
`createHtmlHostWrapper`.
- Added an `svg-pointer `story + `createHtmlTagPointerStory`

Note: Also pinned @types/react to v18 so the renderer stops dragging in
React 19 types and breaking typecheck.
2026-06-02 15:04:11 +00:00
1642be86f5 Bonapara/twenty codex plugin (#20857)
@martmull v2.0 ;)

---------

Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
2026-06-02 14:39:14 +00:00
Charles BochetandGitHub cb744b2eeb refactor(twenty-server): collapse workspace migration build orchestrator (#21144)
## Why

`WorkspaceMigrationBuildOrchestratorService.buildWorkspaceMigration` was
1117 lines of 30 near-identical copy-pasted blocks (one per metadata
entity) plus a 165-line ordered `actions: [...]` literal. Adding a new
entity meant copying ~30 lines and hoping the boilerplate stayed in
sync; the actual per-entity execution order was buried in the noise.


## Gain

- Orchestrator: **1117 → 388 lines**
- Net diff: **-558 lines**
- Adding a new entity is now a single line in the registry
- Execution order is visible at a glance
2026-06-02 14:16:10 +00:00
Anish PaudelandGitHub b422550fcc refactor(server): merge duplicate TypeOrmModule.forFeature calls in MessagingMessageCleanerModule (#21150)
Combined two separate `TypeOrmModule.forFeature()` calls into one. Both
registered entities on the default data source, so no behavioral change.
Repositories for WorkspaceEntity and MessageChannelEntity remain
injectable as before.
Cleaner imports, one less redundant call.
2026-06-02 14:10:36 +00:00
3aae4330b6 i18n - website translations (#21137)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 16:10:15 +02:00
Félix MalfaitandGitHub 1e336dbad1 feat: allow many-to-one relations as advanced filter leaves (#21147)
## What

Lets a many-to-one relation be selected as the **leaf** of an advanced
(nested) filter. Previously the nested-field submenu excluded relations,
so you could filter `Opportunities WHERE company.Name contains X` but
not `Opportunities WHERE company.accountOwner = me`.

## How it works

Selecting a relation leaf filters by its **foreign key** —
`company.accountOwnerId = X` — a single hop the backend already resolves
on the joined table (`{ company: { accountOwnerId: { in: [...] } } }`).
It is **not** a multi-hop traversal: filtering on a *scalar field of*
the related record (e.g. `company.accountOwner.name`) stays excluded,
since that needs a second join the backend caps at one hop.

Two changes:
- **`AdvancedFilterRelationTargetFieldSelectMenu`** — stop excluding
many-to-one relations from the nested-field submenu.
- **`ObjectFilterDropdownRecordSelect`** — resolve the record picker's
object from the *leaf* relation's target (e.g. WorkspaceMember,
including the "Me" pin) rather than the source relation's object. The
source-field fallback applies only when there is no leaf.

## Testing
- Added `turnRecordFilterIntoRecordGqlOperationFilter` unit cases
asserting a relation leaf (and `= me`) compiles to the FK form — 59/59.
- typecheck + lint green (twenty-front, twenty-shared).

Seeding an onboarding view that uses this filter will follow in a
separate PR.
2026-06-02 15:52:08 +02:00
c14422473b i18n - translations (#21154)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 15:36:10 +02:00
120793f69f fix: block self-impersonation in admin panel (#21130)
## Issue
- From Settings -> Admin Panel -> Workspace -> Members, impersonating
the currently logged-in user still issued an impersonation login token.
Token exchange produced invalid impersonation JWTs
(`impersonatorUserWorkspaceId === impersonatedUserWorkspaceId`). JWT
validation then failed with `User cannot impersonate themselves`,
leaving the app in an endless loading state until cookies were cleared.
- Closes #21086

## Approach
I was first thinking of to only hide the impersonate button for the
logged-in user in the admin, since they can not click what isn’t shown
(as I thought it was just a frontend issue).

But that was not enough:
- The `impersonate` mutation can still be called directly (GraphQL
client, scripts, devtools).
- Before this fix, the mutation could succeed and only fail later at JWT
validation, which led to invalid tokens and a broken session.

So the PR does both:
- Frontend: hide/disable self-impersonation in the UI and avoid
reloading on failed token exchange (UX).
- Backend: reject self-impersonation in `ImpersonationService` and at
token exchange (enforcement, fail fast before bad tokens).

Hiding the button is the right product behavior; the backend change is
what makes the rule real and safe.

## How to test
Manual:
- Log in as a user with admin impersonation.
- Go to Settings -> Admin panel -> Workspace -> open your workspace ->
members.
- Confirm your row has no Impersonate button; other members still do.
- Open Admin Panel -> User for yourself -> confirm no impersonate
button.
- Open Settings -> Members -> your own member profile -> confirm no
Impersonate action.
- Impersonate another member -> should work as before

Automated: `npx jest impersonation.service.spec`

### Before:
<img width="830" height="413" alt="Screenshot 2026-06-02 122224"
src="https://github.com/user-attachments/assets/46f38a74-8bd6-4ffa-b749-500ce18314f1"
/>

### After:
<img width="795" height="369" alt="Screenshot 2026-06-02 122333"
src="https://github.com/user-attachments/assets/62ece4a8-d38b-4f91-817f-792ff49b146b"
/>

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-02 13:15:44 +00:00
Raphaël BosiandGitHub 4d520a312f Allow functional iframes in front components while blocking sandbox escapes (#21145)
Fixes https://github.com/twentyhq/twenty/issues/19899

Front component iframes were previously forced to `sandbox=""`, which
fully locks them down: no scripts, no forms, no popups. That broke any
legitimate embedded content (maps, widgets, embeds) developers tried to
render.

But we can't just trust the app-provided sandbox value either: tokens
like allow-same-origin or allow-top-navigation would let a malicious
embed escape the sandbox and hijack the host Twenty tab.

- Add `sanitizeIframeSandbox`, which keeps the iframe useful while
enforcing security:
applies a safe default (allow-scripts allow-forms allow-popups) when no
sandbox is set
always forces allow-scripts so embeds work
- strips dangerous tokens (`allow-same-origin`, all
`allow-top-navigation`*, `allow-popups-to-escape-sandbox`),
case-insensitively
- Wire it into `createHtmlHostWrapper` so every `iframe` rendered by a
front component is sanitized.
- Add unit tests for the sanitizer and Storybook interaction tests
asserting dangerous sandboxes are stripped.
2026-06-02 13:01:18 +00:00
Rashad KaranouhandGitHub ea84aabe4c chore(twenty-partners): refine design-doc skill doctrine (#21151)
## Summary

Iterative refinements to the partner design-doc doctrine after running
it on a second lead (TADA) and reviewing output side by side. Touches
only the `twenty-partner-design-doc` skill files (doctrine + Claude Code
wrapper); no runtime / app code.

**What changed**

- **Flag system:** emoji + short text label pairs only (`🔮 inf.`, **
open**, **⚠️ heavy**, **🛑 blocker**). Replaces the prior text-tag-only
system; scannable, unambiguous.
- **Section structure:** split into **Required** (always present) and
**Conditional** (Views, Automations, Integrations, Reporting). Include
conditional sections only when the client grounded them in the source.
Number sequentially, no gaps.
- **No filler placeholders:** banned `X was not named` / `left out on
purpose` lists in body sections. Unknowns belong in Open questions, not
as their own section or bullet.
- **Functional cross-refs:** every `§N` reference is now a markdown
anchor link `[§N](#n-section-slug)`, so a partner skimming the doc can
navigate. Bare `§N` is banned.
- **Bullets and tables over paragraphs**, with **Open questions** kept
as a numbered list (so the partner can read items 1, 2, 3 with the
client).
- **Views & navigation** rendered as a tight `Surface | Shows |
Audience` table. No view-type column — table / kanban / page layout is
the partner's call, not a scoping decision.
- **Data-model table** gains a `Source` column (`client` / `inf.`) for
at-a-glance fact-vs-inference visibility.
- **Business decisions over technical mechanics:** cut SDK / runtime
internals that don't move the quote (Docker version, OAuth flavour,
auto-system relations, env-var names, CI/CD workflow detail).
- **Common-mistakes table** updated with rows for the new rules.
- **SKILL.md self-check** expanded so the wrapper enforces all of the
above before saving.

## Test plan

- [ ] Re-read doctrine end-to-end for internal consistency
- [ ] Verify the four canonical emoji + text pairs appear and no stray
emoji flags remain
- [ ] Confirm Required vs Conditional structure is internally consistent
(no section listed in both)
- [ ] Confirm functional-cross-ref rule appears in both Rules and
Formatting and is reflected in the SKILL.md self-check
- [ ] Confirm Views & navigation entry mandates the three-column table
and bans a Type column
- [ ] Confirm Common-mistakes table covers each new rule
2026-06-02 12:47:56 +00:00
3a67b35486 i18n - translations (#21149)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 14:40:31 +02:00
939e0b350e i18n - translations (#21148)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 14:32:17 +02:00
Félix MalfaitandGitHub c18f8d6cf7 Always show Favorites section and add favorites via the side panel (#21087)
## What & why

The left-sidebar **Favorites** section was hidden whenever the user had
no favorites, so it was effectively undiscoverable — and personal
favorites could only be created via the record-level "Add to favorites"
action or drag-drop.

This PR:
- **Always shows the Favorites section**, with an empty-state **"Add a
favorite"** call-to-action.
- Adds a **"+" on the Favorites header that opens the same "New menu
item" side panel** the Workspace section already uses, so users can add
personal **Objects, Views, Records, Links and Folders** directly from
the sidebar.

## How

Favorites and workspace navigation are the same `NavigationMenuItem`
entity (a personal favorite simply has `userWorkspaceId` set). Rather
than build a separate favorites-only flow, the shared add/edit
side-panel subsystem is made **section-aware**
(`NavigationMenuItemSection = 'workspace' | 'favorite'`):

- a new `navigationMenuItemEditSectionState` atom records which section
the panel is operating on;
- a new `useNavigationMenuItemEditController` forks persistence — the
**workspace** section stages changes in the draft (saved on
layout-customization exit), while the **favorite** section
creates/updates/deletes personal items **immediately** with
`userWorkspaceId = current member`. This mirrors the existing
`useHandleNavigationMenuItemDragAndDrop` fork.

The existing add/edit hooks, pickers and title editors were rerouted
through the controller and a section-aware items hook, so they work for
both sections with no behavior change to the workspace flow.

**Backend: no changes** — `canUserCreateNavigationMenuItem` already
authorizes personal navigation menu items of every type for any
authenticated user.

## Decisions & tradeoffs

- **Folder button → unified "+":** the folder-only header button is
replaced by the single "+" (Folder is one of the panel's options),
matching the Workspace section. This removed the inline folder-create
code path.
- **Click-to-add only in v1:** dragging items from the panel directly
into Favorites is deferred — those drag handles are disabled in the
favorite section (the drag path is hardwired to workspace layout mode),
with a defense-in-depth no-op in the drop handler.
- **Persist-on-commit:** favorite title/URL edits hit the network once
on blur/enter, never per keystroke.
- **Personal color edits** change only the favorite's own color, never
the shared object metadata (that remains a workspace-customization
behavior).
- The change touches ~37 files because it generalizes the shared
subsystem rather than duplicating it; net diff is slightly negative
(+605 / −636).

## Testing

- `npx nx typecheck twenty-front` — passes
- `npx nx lint twenty-front` (oxlint + oxfmt) — passes
- `navigation-menu-item` unit tests — pass (incl. an updated
`computeInsertIndexAndPosition` test covering personal items)
- Manual end-to-end walkthrough still recommended before merge.
2026-06-02 14:28:10 +02:00
Félix MalfaitandGitHub 431f6ae98f feat(settings): move settings chrome into a single rounded card (#21131)
## What

Replaces `SubMenuTopBarContainer` with a settings-specific
`SettingsPageLayout` that puts the whole page chrome — breadcrumb,
centered title, actions, an optional secondary bar (tabs or wizard
step), and the 760px body — inside **one rounded card**, with
`SidePanelForDesktop` as a sibling. Title, tabs and body content share
one centered vertical axis at every card width.

Supersedes #21122. One PR, no feature flag.

## New components (`@/settings/components/layout/`)

- **SettingsPageLayout** — owns the rounded card + side-panel sibling,
`useCommandMenuHotKeys`, mobile command menu
- **SettingsPageHeader** — breadcrumb · centered title · actions in a
symmetric `1fr auto 1fr` grid (symmetric padding throughout)
- **SettingsSecondaryBar** — the secondary row, bracketed by top +
bottom borders
- **SettingsTabBar** — centered tabs reusing `activeTabIdComponentState`
+ `TabListFromUrlOptionalEffect` for URL-hash sync (does not touch the
shared `TabList`)
- **SettingsWizardStepBar** — back arrow · "N. Label" · optional
trailing slot

## Migrations

- Bulk rename across ~80 call sites (`SubMenuTopBarContainer` →
`SettingsPageLayout`); old component deleted.
- 5 tab pages (AI, APIs & Webhooks, Applications, Members, Role) + the
Data Model object-detail page render their tabs in `secondaryBar`
(object-detail keeps "See records" / "New Field" in the header actions).
- The 2 role object-level steps render the wizard step bar with working
back navigation.
- Accounts consolidated into **General / Emails / Calendars** tabs;
standalone `SettingsAccountsEmails` / `SettingsAccountsCalendars` pages
+ routes + stories removed. `SettingsPath.AccountsEmails` /
`AccountsCalendars` now resolve to `accounts#emails` /
`accounts#calendars`, so existing `getSettingsPath()` links deep-link to
the right tab via the existing hash sync — no call-site changes.

## Verification

- `nx typecheck twenty-front` and `nx lint twenty-front` both clean.
- Browser (logged-in workspace): title / tab / body / card centers align
on a single axis at multiple widths — width-invariant, so alignment
holds when the AI side panel (a sibling) shrinks the card. Rounded card
with even gaps on all four sides; tab row bracketed by two 1px lines;
no-tab pages render header → body with no lines; wizard back navigation
works; `…/accounts#emails` opens the Emails tab.

The shared `PageHeader` and `TabList` are untouched. The settings side
panel itself isn't wired to open yet — that's a follow-up PR.
2026-06-02 14:22:57 +02:00
Rashad KaranouhandGitHub e721ebe300 chore(twenty-partners): bump app version to 0.3.3 (#21140)
Bumps the `twenty-partners` SDK app version 0.3.2 → 0.3.3 so `main`
tracks what's deployed to prod.

This is the deploy version for the partner-app changes that just landed:
marketplace `partnerScope` exposure (#21126), the
`submit-partner-application` endpoint + new Partner categories +
migration (#21040), the marketplace card rebind (#21127), and the signup
wizard (#21039).

No code changes — version bump only.
2026-06-02 11:42:06 +00:00
2048efb75d fix(record-table): keep column header dropdown open after Move Left/Right (#21015)
Fixes #20999

## Summary

Fixes a UX issue where clicking **Move left** or **Move right** in the
column header
dropdown immediately closed the menu, forcing users to reopen it for
every single move.

## Problem

`handleColumnMoveLeft` and `handleColumnMoveRight` both called
`closeDropdownAndToggleScroll()` unconditionally at the top of their
handlers — before
even checking `canMoveLeft` / `canMoveRight`. This immediately set the
Jotai atom
`isDropdownOpenComponentState` to `false`, unmounting the dropdown.

Since move actions are **repeatable** — a user might want to shift a
column several
positions — they were forced into a frustrating loop: click header →
click move → click
header → click move → repeat for every step.

## Fix

Removed the two `closeDropdownAndToggleScroll()` calls from the move
handlers in
`RecordTableColumnHeadDropdownMenu.tsx`.

```diff
  const handleColumnMoveLeft = () => {
-   closeDropdownAndToggleScroll();
-
    if (!canMoveLeft) return;
    moveTableColumn('left', recordField.fieldMetadataItemId);
  };

  const handleColumnMoveRight = () => {
-   closeDropdownAndToggleScroll();
-
    if (!canMoveRight) return;
    moveTableColumn('right', recordField.fieldMetadataItemId);
  };
```

All other handlers — **Filter, Sort, Hide** — are untouched and still
close the dropdown
correctly, since those are one-shot or navigation actions.

## Changes

| File | Change |
|---|---|
| `RecordTableColumnHeadDropdownMenu.tsx` | Remove 2
`closeDropdownAndToggleScroll()` calls from move handlers |
| `RecordTable.stories.tsx` | Add `HeaderMenuStaysOpenAfterMoveRight`
regression story |

## Testing

**Storybook interaction test** — `HeaderMenuStaysOpenAfterMoveRight`:
clicks "Move right" then asserts the menu is still visible.

**Manual checklist:**
- [x] Move right → menu stays open
- [x] Move right again → column moves again, menu still open
- [x] Move left → menu stays open
- [x] Move rightmost column → "Move right" disappears, menu stays open
showing "Move left"
- [x] Filter → menu closes *(unchanged)*
- [x] Sort → menu closes *(unchanged)*
- [x] Hide → menu closes *(unchanged)*
- [x] Click outside → menu closes *(unchanged)*
- [x] Escape → menu closes *(unchanged)*
- [x] TypeScript: zero new errors (`tsc --noEmit`)

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-02 11:23:26 +00:00
Raphaël BosiandGitHub b2539f5b6a Prevent conditional availability variables from being used at runtime (#21110)
Fixes https://github.com/twentyhq/twenty/issues/21094

Conditional availability variables (`objectMetadataItem`,
`numberOfSelectedRecords`, `objectPermissions`, operators like
`everyEquals`/`none`, etc.) are compile-time-only constructs used in
`conditionalAvailabilityExpression`. They were previously exported from
`twenty-sdk/front-component`, which let developers mistakenly import
them into runtime component code where they have no value.

- Move conditional availability variables from
`twenty-sdk/front-component` to `twenty-sdk/define`.
- Add a build-time manifest validation
(validate-conditional-availability-usage) that fails the build if these
variables are imported/used outside of
`conditionalAvailabilityExpression`.
- Update the github-connector example app to register commands via
dedicated *.command-menu-item.ts files instead of inline command config
in front components.
- Update docs (all locales) and test mocks to reflect the new import
paths.
2026-06-02 11:22:38 +00:00
2375c2f59c i18n - translations (#21141)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 13:35:06 +02:00
Charles BochetandGitHub 58907b733c feat(logic-function): add LIVE / PREBUILT execution modes (#20873)
## Summary

### Why

1. Sending the code to the lambda (~1Mb usually) is heavy on network and
results to a constant traffic of ~30Mb/s on AWS which results into TB of
network data every month
2. eval(1MB of code) is not that fast, it's heavy on memory and CPU on
lambda side

### High level

Adds two execution modes for logic functions, gated behind the new
`IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED` workspace feature flag (off
everywhere by default):

- **LIVE** (current behavior, preserved bit-for-bit): the compiled
bundle is read from object storage and shipped in every Lambda invoke
payload. Used for fast iteration in the workflow editor / Settings test
runs.
- **PREBUILT** (new): the bundle is installed onto the per-function
Lambda alongside the unified executor, and invocations carry only `{
params, env, handlerName }` — saving JSON payload egress and warm-start
`import()` cost on every call.

### Key design choices

- **Unified Lambda handler** (`constants/executor/index.mjs`) dispatches
at runtime: `event.code` present ? LIVE (write to `/tmp`, dynamic
import) : `import('./prebuilt-logic-function.mjs')`. Both code paths
always coexist on the deployment package, so the same Lambda can serve
either mode without redeploying.
- **Install runs inside the `validateBuildAndRun` migration pipeline**,
not at execute time. `Create/UpdateLogicFunctionActionHandlerService`
calls `driver.installPrebuiltBundle` when `executionMode` flips
LIVE?PREBUILT or `checksum` changes while PREBUILT, gated on
`isBuildUpToDate=true` and a fresh checksum.
- **Strict execute, no reconciliation**:
`LogicFunctionExecutorService.execute` resolves `effectiveExecutionMode`
(caller override > feature flag > entity column). For PREBUILT it asks
the driver `getInstalledBundleChecksum` (Lambda `twenty:bundle-checksum`
tag for AWS, sidecar file locally) and throws
`LOGIC_FUNCTION_PREBUILT_BUNDLE_NOT_INSTALLED` on mismatch.
- **Feature flag gates every side effect**: with the flag off the
executor forces LIVE, the action-handler install hooks bail before AWS,
and workflow activation does not flip the mode. Rollback is just turning
the flag off.

### Lifecycle

- New workflow CODE step ? `LIVE`, no install.
- Workflow activated ? build + activation flips `executionMode=PREBUILT`
? action-handler installs the bundle + sets the Lambda tag.
- Draft from active version ? duplicated logic function reset to `LIVE`.
- App install ? manifest converter sets `PREBUILT`, create-action
handler installs.
- Test runs (`executeOneFromSource`, workflow editor) pass
`executionMode=LIVE` explicitly.

### Observability

`[lambda-timing]` log lines now include `effectiveExecutionMode` and
`payloadBytes`; the action handler logs `install_duration_ms` for each
install.

## Test plan

- [x] `npx nx typecheck twenty-server` ? passes
- [x] `npx oxlint --type-aware` on all changed files ? 0 warnings, 0
errors
- [x] `npx nx test twenty-server` ? 588 suites / 5009 tests pass (no
regressions vs main)
- [x] New unit suite `flat-logic-function-validator.service.spec.ts` ?
9/9
- [x] Existing
`workflow-version-step-operations.workspace-service.spec.ts` ? 8/8
(verified the new token-based DI avoids a circular-import regression)
- [x] Snapshot for
`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY` updated
to include `executionMode`
- [x] Integration suite `logic-function-execution.integration-spec.ts`
extended to assert `executionMode=LIVE` on newly-created functions and
continues to exercise the LIVE happy path
- [ ] Manual staging rollout: flip
`IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED` per workspace, observe
`[lambda-timing]` `payloadBytes` drop + `install_duration_ms`, then ramp
in prod.
2026-06-02 11:14:39 +00:00
1ae00d6753 fix(ai): correct find-records tool description (top-level filter fields) (#21109)
## Problem

The AI `find_<object>` tool builds its input schema with
`generateFindToolInputSchema`, which **spreads field filters at the args
root** (alongside `limit`/`offset`/`orderBy`/`and`/`or`/`not`).
`tool-executor.service.ts` then maps the raw model args to a filter
with:

```ts
const { limit, offset, orderBy, ...filter } = args;
```

The zod schema is only used to generate the JSON schema *shown* to the
model (`z.toJSONSchema(...)`) — it is **never used to validate the args
coming back**. So when the model emits a bare operator where a field
name belongs, e.g. `{ ilike: "Foreman" }`, it passes straight to the
query runner, which throws and burns a retry mid-turn:

```
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "ilike" field.
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "eq" field.
```

Two contributing faults:
1. **The tool description actively misleads the model** — it says ``use
filter: { id: { eq: "record-id" } }``, a `filter` wrapper the
root-spread schema doesn't have, inviting the malformed shape.
2. **No server-side validation** — invalid root keys reach the query
runner instead of being rejected against the advertised contract.

## Fix

1. **`FindRecordsService` prunes invalid filter keys before querying.**
Using the same filter shape the tool schema advertises
(`generateRecordFilterSchema(...).filterShape`), it drops any key that
is neither a real field nor a logical operator (`and`/`or`/`not`),
recursing through `and`/`or`/`not`. A model that sends `{ ilike:
"Foreman" }` now gets a valid (empty) filter rather than an exception.
Extracted as a pure, unit-tested util `pruneFilterToAllowedKeys`.
2. **Corrected the `find_<object>` tool description** to describe the
real top-level-field shape and explicitly warn against a `filter`
wrapper and bare root operators.

## Test

`__tests__/prune-filter-to-allowed-keys.util.spec.ts` covers: valid
filters untouched, bare root operators dropped, valid siblings
preserved, `and`/`or`/`not` recursion, and non-object input.

## Notes

- Defensive for all `FindRecordsService` callers; `find_one` (`{ id: {
eq } }`) and workflow find-records pass valid filters and are
unaffected.
- Companion to #21106 (RICH_TEXT composite filters). Both surfaced from
the same `"Tom Foreman's notes"` AI-chat repro; this PR addresses the
root-level-operator half.

---------

Co-authored-by: Rich Roberts <rich.roberts@talentpipe.ai>
2026-06-02 10:58:13 +00:00
e1a00ea42f fix(twenty-front): enable text selection for display-mode fields (#21068)
## Description
This PR resolves a usability issue where scalar field values (emails,
phone numbers, dates, IDs, text, etc.) rendered in display-mode or
read-only mode in the record detail side panel could not be highlighted,
selected, or copied natively with the cursor.

## Root Cause
Both `RecordInlineCellContainer` and
`RecordInlineCellHoveredPortalContent` wrapper elements had
`user-select: none;` hardcoded in their styled-component definitions.
This styling propagated down to all nested display widgets, locking
their content and preventing native text selection.

## Changes
- Updated `StyledInlineCellBaseContainer` in
`RecordInlineCellContainer.tsx` to use `user-select: text;` instead of
`none;`.
- Updated `StyledInlineCellBaseContainer` in
`RecordInlineCellHoveredPortalContent.tsx` to use `user-select: text;`
instead of `none;`.

These changes restore natural browser text selection capabilities for
record detail widgets without altering interactive edit-mode behaviors.

## Verification
- Verified styling changes.
- Tested locally to ensure that text highlighting and copy-pasting
function correctly when dragging over read-only fields.

Closes #21056

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-02 10:53:00 +00:00
94d2e386e8 i18n - website translations (#21135)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 12:43:26 +02:00
Rashad KaranouhandGitHub 4f47885054 feat(twenty-partners): submit-partner-application HTTP logic function (#21040)
## Summary

Adds a public `POST /partner-applications` HTTP logic function on the
twenty-partners SDK app that receives applications from the website
wizard and idempotently upserts the Partner / Person / Company graph in
the partners workspace. Also introduces the validated **Category**
taxonomy on `partnerScope` (additive, prod-safe) plus the legacy→new
migration tooling.

Companion PR (website side): #21039

### Logic function
- `defineLogicFunction({ httpRouteTriggerSettings: { path:
'/partner-applications', httpMethod: 'POST', isAuthRequired: false,
forwardedRequestHeaders: ['x-application-secret'] } })`.
- Authenticates via shared-secret header (`X-Application-Secret` ↔
`PARTNER_APPLICATION_SECRET` workspace variable). Twenty's
`isAuthRequired: true` only accepts user-session JWTs, so the handler
enforces auth itself.
- Idempotent upsert keyed on `Person.emails.primaryEmail`:
  - missing email → create Company → Person → Partner
  - existing Person, no Partner → create Company + Partner, link
- existing Person + Partner → update Partner fields; preserve
staff-owned columns (`validationStage`, `reviewed`, `ranking`,
`partnerTier`, `lastMatchAt`) by omitting them from the update
- Create-time defaults preserved on resubmit: `slug =
slugify(companyName)` ("YC Agency" → "yc-agency"), `reviewed = false`,
`partnerTier = 'NEW'`.
- Currency conversion to `{ amountMicros, currencyCode: 'USD' }` for
`hourlyRate` + `projectBudgetMin`.

### Categories (`partnerScope`) — additive, prod-safe
- Adds 5 validated category options — `ADVISORY`, `SOLUTIONING`,
`DEVELOPMENT`, `HOSTING`, `SUPPORT` — to the `partnerScope` MULTI_SELECT
**without removing** the legacy options (there is production data on
them). Field relabeled **"Categories"**. The website form only emits the
new values.
- **Migration tooling** (run deliberately, *not* in CI):
`scripts/migrate-partner-scope.ts` remaps existing records legacy→new —
dry-run by default, `MIGRATE_APPLY=1` to write, two-pass
(collect-then-apply, no mutate-while-paginating).
`scripts/partner-scope-map.ts` is the single mapping source;
`import-from-tft.ts` now routes imported scope through it so the TFT
import never re-introduces retired values. Removing the legacy options
is deferred until after the migration has run + been verified.

### applicationNotes
- New `applicationNotes` TEXT field holds the wizard's single free-text
"anything else" note (the handler passes it through directly).
`deploymentExpertise` was dropped from the handler
input/validation/builders (the column is retained for now, pending the
same migration cleanup).

### Application variable
- Declares `PARTNER_APPLICATION_SECRET` with `isSecret: true` so each
workspace sets the value via Settings → Apps → Twenty Partners →
Variables. Twenty encrypts at rest and merges the decrypted value into
the handler's `process.env` at execution time (workspace value wins over
container env).

### Code quality (from review)
- One shared `slugify` (`scripts/slugify.ts`, the import's algorithm)
used by both the handler and the import, so the `slug` identity key
can't diverge across paths.
- Unit-test tier: `vitest.unit.config.ts` (no `globalSetup`) + `yarn
test:unit`, so the pure `mapLegacyScope` test runs without a live server
(the integration suite stays server-backed).

## Demo

📹 _Screen recording of the wizard end-to-end (open → walk steps → submit
→ Partner record lands):_


https://github.com/user-attachments/assets/7458dd86-e3ff-47b5-9878-0eb134ff38e3

### Tests
- Integration tests against a local Twenty workspace:
missing-/wrong-secret auth rejections, create flow (asserts slug +
`reviewed: false` + `partnerTier: 'NEW'`), update-on-resubmit +
staff-column preservation, new category values stored,
`applicationNotes` stored, bad-input shape.
- Pure `mapLegacyScope` unit test via `yarn test:unit` (no server).

## Test plan

- [ ] Install / upgrade the app on the target workspace; set
`PARTNER_APPLICATION_SECRET` in Settings → Apps → Twenty Partners →
Variables
- [ ] `curl -i -X POST <workspace-url>/s/partner-applications -H
'X-Application-Secret: <secret>' -H 'Content-Type: application/json' -d
'{"firstName":"Test","lastName":"User","email":"test@example.com","companyName":"YC
Agency","partnerScope":["ADVISORY"],"applicationNotes":"hi"}'` →
`HTTP/1.1 201` + `{"ok":true,"created":true,"partnerId":"..."}`
- [ ] Partner record shows `name: "YC Agency"`, `slug: "yc-agency"`,
`validationStage: APPLICATION`, `reviewed: false`, `partnerTier: 'NEW'`,
`partnerScope: ["ADVISORY"]`, `applicationNotes: "hi"`
- [ ] Re-curl same email with `city: "Paris"` → `created: false`,
`Partner.city` updated, staff-owned columns untouched
- [ ] Wrong / missing secret → `200` +
`{"ok":false,"reason":"unauthorized"}`
- [ ] `yarn test:unit` green (no server); `yarn migrate:partner-scope`
dry-run lists any legacy→new remaps without writing
2026-06-02 10:35:28 +00:00
Rashad KaranouhandGitHub 7e034f711f feat(website): surface partner Categories (partnerScope) in marketplace, drop deploymentExpertise facet (#21127)
## What

Rebinds the marketplace's expertise facet from `deploymentExpertise`
(Cloud / Self-host) to **`partnerScope`** — the five partner Categories:
Advisory & Discovery · Solutioning · Custom Development · Hosting &
Infrastructure · Training & Adoption.

Moves the card chip, the profile facts row, the dropdown filter, the
`?categories=` URL param, and the API-boundary normalization onto
`partnerScope`. The standalone Cloud/Self-host facet is **dropped**
(hosting is now the `HOSTING` category), per the harmonization decision.

## Depends on

- The app exposing `partnerScope` — companion app PR #21126.
- The new `partnerScope` options + data migration — signup app PR
#21040.

## Tests

TDD red→green on: `filter-partners`, both API normalizers,
`filter-url-helpers`, `PartnerCard`, `use-filter-state`. 53/53 pass;
typecheck + lint + format clean.

## Merge order (we'll decide)

Independent diff. Suggested last of the four, after the signup PRs
(#21039 / #21040) and the app PR (#21126). Run `lingui:extract` once
after #21039 merges so the `.po` files don't conflict twice. Deploy the
app + migrate before the website ships.
2026-06-02 10:32:54 +00:00
Rashad KaranouhandGitHub a3557373e6 feat(twenty-partners): expose partnerScope on list + by-slug endpoints (#21126)
## What

Adds `partnerScope` (the partner **Categories** multi-select) to the
output of the two public partner endpoints:
- `list-available-partners` (`/s/partners`)
- `get-partner-by-slug` (`/s/partner-by-slug`)

Additive only — `deploymentExpertise` is kept, so existing consumers
(the current live marketplace) are unaffected.

## Why

Part of the partner marketplace rework. The website marketplace
(companion branch `rk-rework-marketplace-cards`) consumes `partnerScope`
to show/filter partner Categories. The new options + migration live in
the signup app PR #21040.

## Merge order (we'll decide)

Independent diff — can merge in any order. Couplings to keep in mind:
- **Version line:** this branch and #21040 both bump the app
`package.json` version; whoever merges second re-bumps.
- **Deploy (not merge):** the partners app is deployed manually. Deploy
the final combined app (this + #21040) and run `yarn
migrate:partner-scope:prod` **before** the website is deployed.
2026-06-02 10:23:51 +00:00
d0e0e27035 [Website] Partner application wizard + logic-function handover (#21039)
## Summary

Replaces the single-screen partner-application modal with a **4-step
wizard** on the public form, and points the route's upstream at the new
`submit-partner-application` HTTP logic function in the twenty-partners
SDK app.

After design review, the Expertise step landed on the validated
**Category + Skills** model: a small set of *stable* macro categories
the partner operates in, plus a *free, semi-structured* Skills field for
the concrete things that differentiate them (React, SAP, Shopify, …).

Companion PR (partners-app side): #21040

## ⚠️ Deployment notes

Before this can ship to prod, the website worker needs a new env var:

- **Add `PARTNER_APPLICATION_SECRET`** to the deploy config at
https://github.com/twentyhq/twenty-infra/tree/main/cloudflare/website.
Without it the route returns `503` ("Partner application endpoint is not
configured.").
- The value must **match** the `PARTNER_APPLICATION_SECRET` workspace
variable set in the partners workspace UI (Settings → Apps → Twenty
Partners → Variables) — that's how the handler authenticates the
incoming `X-Application-Secret` header.
- `PARTNER_APPLICATION_WEBHOOK_URL` also needs repointing from the TFT
webhook to the logic-function URL
(`https://partner.twenty.com/s/partner-applications` or equivalent) at
the same time.

## Wizard

- 4 steps inside `Modal.Root`: **Identity → Profile → Expertise →
Commercials**. Step-dot indicator, per-step required-field gating, reset
on close. The big serif hero shows **only on step 1**; later steps use
the compact `STEP n OF 4 · NAME` strip to reclaim vertical space.
- **Profile** captures Type of team (Solo/Agency), LinkedIn, City,
Country, Languages. Country uses the searchable Select (placeholder-only
label).
- **Expertise = Category + Skills + Notes:**
- **Category** — multi-select cards over 5 macro categories (`ADVISORY`,
`SOLUTIONING`, `DEVELOPMENT`, `HOSTING`, `SUPPORT`), each with a
one-line description + examples. (Replaces the old draft `partnerScope`
enum; the backend keeps the field name — see #21040.)
- **Skills** — free tag input with a clickable suggestion row + keyboard
autocomplete (↑/↓/Enter/Esc) and "add your own". Empty by default.
- **Notes** — one free textarea (merges the former `workspaceUrl` +
`customerReferences`), reviewed manually.
- `deploymentExpertise` removed from the form (covered by the Hosting
category).
- **In-modal success view** on submit ("Thanks, / we'll be in touch!")
with a Close button — replaces the old silent close.
- Removes the partners-page "Which partner program is right for you?"
three-cards section.

## Design-system primitives

- **`Form.Select`** — searchable popup whose dropdown is **portaled to
`<body>`** (fixed, anchored to the trigger, flips up, height-capped) so
the modal's `overflow`/`transform` can't clip it; pointer events are
stopped so clicking inside it doesn't dismiss the dialog.
- **`Form.TagInput`** — optional `suggestions` prop adds the suggestion
row + autocomplete menu (used by Skills); behaviour unchanged when no
suggestions are passed.
- **`CategoryCardSelect`** — compact multi-select cards.
- `Form.MultiSelect`, `Form.Currency`.

## Validation & payload

- **Single validation source:** client and server share Zod field
schemas (`partner-application-field-schemas.ts`). The reducer validates
via those instead of hand-rolled regexes, so client and server agree by
construction (e.g. both reject non-TLD URLs).
- **Typed request body:** `buildPartnerApplicationRequestBody(state)`
returns a typed `PartnerApplicationRequest` (unit-tested);
`handleSubmit` just serializes it.
- Payload is camelCase matching the logic-function input;
`applicationNotes` replaces `workspaceUrl`/`customerReferences`.
- Auth: the upstream call carries an `X-Application-Secret` header
backed by `PARTNER_APPLICATION_SECRET` (handler-enforced — the SDK's
`isAuthRequired` only accepts user-session JWTs, not workspace API
keys). The webhook-URL env uses `z.url()` (not `z.httpUrl()`) so
`http://localhost:2020/...` dev destinations parse.

## Demo

📹 _Screen recording of the wizard end-to-end (open → walk steps → submit
→ Partner record lands):_


https://github.com/user-attachments/assets/7458dd86-e3ff-47b5-9878-0eb134ff38e3

## Tests

- **62 passing** across reducer, Zod schema, route, the new
payload-builder suite, and Form helper suites. `npx tsc` clean, `nx
lint:diff-with-main` clean, Lingui catalogs regenerated (French slots
are a follow-up).

## Test plan

- [ ] `/partners` → "Become a partner" → wizard opens on Step 1 (full
hero)
- [ ] Identity: name / work email / company → Next
- [ ] Profile: pick **Type of team**; search country ("fra" → France);
pick languages → Next (compact header from here on)
- [ ] Expertise: select 1+ **Category** cards; add **Skills** (click a
suggestion, type one + Enter, drive the ↑/↓ autocomplete); optionally
fill **Notes**
- [ ] Country dropdown opens without being clipped by the modal, and
clicking inside it does **not** close the wizard
- [ ] Commercials → Submit → **in-modal "Thanks, we'll be in touch!"**;
Network shows POST `/api/partner-application` `200`
- [ ] Partner record lands with the chosen categories in `partnerScope`,
plus `skills`, `applicationNotes`, `slug` from company, `reviewed:
false`, `partnerTier: 'NEW'`
- [ ] Re-submit same email + different city → Partner updates;
`validationStage`/`reviewed`/`partnerTier` preserved
- [ ] Back/Next preserves entered values; Reset on close; mobile
single-column / chips wrap

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-02 10:23:31 +00:00
Paul RastoinandGitHub 6a908b7876 Front component s3 redirect (#21116)
# Introduction
Unload the server of the file stream when possible
Also fix inconsistent pipeline exception management

Needs to highly be QA, not sure how the cors will behave here
2026-06-02 09:35:58 +00:00
445c6fe9f6 feat: expose CURRENCY field settings (format/decimals) in shared types (#21090)
## What

Add a `CURRENCY` entry to `FieldMetadataSettingsMapping` (a
`FieldMetadataCurrencySettings` type of `{ format?: 'short' | 'full';
decimals?: number }`) so `FieldMetadataSettings<CURRENCY>` resolves to
the real settings shape instead of `null`.

## Why

The currency **format** (Short/Full) and **decimals** selectors already
ship in the field settings UI and persist through the generic `settings`
jsonb column — they render via
[`CurrencyDisplay.tsx`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-front/src/modules/ui/field/display/components/CurrencyDisplay.tsx)
reading `settings.format` / `settings.decimals` (added in #12542 and
#16439).

But `twenty-shared` never got a `CURRENCY` entry in the settings
mapping, so `FieldMetadataSettings<CURRENCY>` is `null`. The SDK's
`defineField` derives its types from this mapping, so an app author
cannot set these from code — `universalSettings: { format: 'full',
decimals: 2 }` on a CURRENCY field is a type error, even though the
server stores and the frontend honours it. This aligns the type layer
with the already-shipped runtime behaviour.

## Changes

- `twenty-shared`: add `FieldMetadataCurrencySettings` +
`FieldCurrencyFormat`, wire the `CURRENCY` mapping entry, export
`FieldCurrencyFormat`.
- `twenty-server`: move `CurrencyFieldMetadata` from the
`NotDefinedSettings` assertions to a defined-settings assertion in the
field-metadata entity type test.

No runtime change — the server already accepts and stores these settings
via the generic jsonb column; this only makes them visible to the type
system and the SDK.

## Test plan

- [ ] `npx nx typecheck twenty-shared` / `twenty-server` pass
- [ ] In an app, `defineField({ type: FieldType.CURRENCY,
universalSettings: { format: 'full', decimals: 2 }, ... })` type-checks
and deploys
- [ ] Field renders with 2 decimals in full format, matching the
equivalent UI configuration

> Follow-up (not in this PR): the frontend keeps its own local
`fieldMetadataCurrencyFormat` / `FieldCurrencyFormat`; it could import
the shared `FieldCurrencyFormat` to de-duplicate.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:42:28 +02:00
neo773andGitHub 1ba7a3fa54 fix: lowercase OAuth handle to prevent duplicate connected accounts (#21120)
Google and Microsoft connect flows stored the provider-returned email
verbatim, so a re-auth with different casing (Sam@ vs sam@) missed the
existing-account lookup and created a duplicate. Normalize the handle to
lowercase at extraction, matching the SSO controller.

Note: not doing backfill for now
2026-06-02 09:27:10 +00:00
Félix MalfaitandGitHub 7d7f32b243 docs: remove the self-host cloud providers page (#21134)
## What

Removes the community-maintained **"Other methods"** cloud-providers
page from the self-host docs (it covered Kubernetes/Terraform/Coolify
community deployments).

## Changes

- **Deleted** `developers/self-host/capabilities/cloud-providers.mdx`
and its 13 localized copies (ar, cs, de, es, fr, it, ja, ko, pt, ro, ru,
tr, zh).
- **Removed the slug** from `navigation/base-structure.json` (the source
of truth) and regenerated the derived files via the repo's own
generators (`yarn docs:generate`, `yarn docs:generate-paths`):
  - `docs.json` — nav entries dropped for every locale.
- `twenty-shared/.../DocumentationPaths.ts` —
`DEVELOPERS_SELF_HOST_CAPABILITIES_CLOUD_PROVIDERS` constant dropped
(was unused elsewhere).
- **Removed the "Cloud Providers" card** from the `self-host` overview
pages across all locales.
- **Dropped the dangling redirect**
`/developers/self-hosting/cloud-providers` (its destination no longer
exists).
- Cleared the matching entry from the unused `navigation-schema.json`
for consistency.

Net: 68 line deletions across config (pure removal); no insertions.

## Verification

- `grep` confirms **0** remaining references to `cloud-providers`
anywhere in the repo.
- All touched JSON files parse; `oxlint` on twenty-docs reports 0
errors.
- Generators (not hand edits) produced `docs.json` and
`DocumentationPaths.ts`.

> Note: `mintlify broken-links` can't run to completion on this branch
due to a **pre-existing** MDX parse error in the unrelated
`l/ar/.../contribute/contribute.mdx`; the grep above is the equivalent
guarantee that no link points at the removed page.
2026-06-02 11:17:37 +02:00
5f0096c464 i18n - website translations (#21088)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 10:45:36 +02:00
6ac797a69c fix: REST cursor encoding for nested order_by composite fields (#20974)
## Summary

fix: REST cursor encoding for nested order_by composite fields

Closes #20109

---
AI was used for assistance.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-02 07:10:20 +00:00
3d6bcc3102 i18n - translations (#21128)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 07:30:02 +02:00
Félix MalfaitandGitHub 75df1f3997 chore(settings): address review comments from PR 21072 (#21121)
## Summary

Round through bosiraphael's 31 review threads on the merged PR #21072
(discovery hero + ephemeral playground token). The user asked to apply
each suggestion only where it adds value, so this PR is split into three
buckets.

### Comments (~17 threads)

- Tightened security-rationale / CSS-gotcha / API-doc comments to one or
two factual lines
- Kept (shortened) the comments above `RequireAccessTokenGuard` call
sites — without them a future reader could remove the guard and silently
reopen the escalation hole
- Kept (shortened) the in-memory-only rationale on
`playgroundApiKeyState` for the same reason
- Kept `flex: 1 + min-height: 0` CSS gotcha on `SubMenuTopBarContainer`
— non-obvious and easy to break

### Structure / extraction

- Move `WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS` to its own constants
file (one-export-per-file)
- Split `SettingsAgentToolsTab` and `SettingsAgentToolsTable` across
queries/, hooks/, types/, utils/:
  - `graphql/queries/findManyApplicationsForToolTable.ts`
  - `graphql/queries/findManyMarketplaceAppsForToolTable.ts`
  - `hooks/useSettingsAgentToolsTable.ts` (data loading + index merging)
  - `types/SettingsAgentToolItem|Application|MarketplaceApp`
  - `utils/getToolApplicationId|getToolLink`
- Extract `SettingsAiModelsTab` optimistic mutations into
`hooks/useSettingsAiModelsActions` (handleModelFieldChange,
handleUseRecommendedToggle, handleModelToggle,
handleToggleAllVisibleModels)
- Extract `SettingsAI.handleCreateTool` into `hooks/useCreateTool`
- Drop unnecessary `useMemo` wrappers on `heroTabs` arrays
(SettingsObjects, SettingsLayout)
- Simplify `MenuItemToggle` handler in SettingsAgentSkillsTab:
`onToggleChange={setShowDeactivated}` (no longer wrapping with arrow +
read of stale `!showDeactivated`)

### Hero assets

- Replace placeholder `customize-illustration` with per-page exports
- Rename `layout/customize-illustration-{light,dark}.png` →
`layout/cover-{light,dark}.png`
- Add `cover-{light,dark}.png` for **applications** and **members**
(they were both pointing at the layout placeholder as a TODO)
- Overwrite `data-model/cover-*.png`, `playground/cover-*.png`,
`ai/ai-tools-cover-*.png` with the new exports

## Test plan

- [ ] `npx nx typecheck twenty-front` 
- [ ] `npx nx typecheck twenty-server` 
- [ ] `npx nx lint twenty-front`  (oxlint + oxfmt, 0 warnings/errors)
- [ ] `/settings/layout`, `/settings/data-model`,
`/settings/applications`, `/settings/ai`, `/settings/api-webhooks`,
`/settings/members` each render the new hero illustration (light + dark)
- [ ] AI tab: tool list still loads, search + Custom/Managed/Standard
filters still work, "New Tool" still navigates to detail
- [ ] AI tab: Models tab — smart/fast model select, "Use best models
only" toggle, per-model checkboxes, toggle-all all still
optimistic+revert on error
- [ ] Skills tab: "Deactivated" toggle still flips show/hide
- [ ] Webhooks table still uses the 1fr 28px grid
2026-06-02 07:23:14 +02:00
Paul RastoinandGitHub d6b3527552 Public assets server s3 redirection (#21108)
# Introduction
Avoid overloading the server on file streaming
Take profit of the different origin implied by the redirection to the s3
Only concern being the expiration date on a public file which is
acceptable

closes https://github.com/twentyhq/private-issues/issues/483
related https://github.com/twentyhq/private-issues/issues/491
2026-06-01 16:25:58 +00:00
Paul RastoinandGitHub 989b45db15 Strictly type encryption rotation key site maps constants through entity type derivation (#21085)
# Introduction
Followup https://github.com/twentyhq/twenty/pull/21001

Now that the typeorm entities provide grains over their
`encryptedString` value, we can strictly type the sitemaps of the
encrypted string to rotate in case of encryption key rotation and also
the integration tests tests cases
2026-06-01 15:25:58 +00:00
WeikoandGitHub d86e827563 fix: return proper FORBIDDEN GraphQL errors from ApiKeyResolver (#21107)
## Context
CI is broken on main, regression introduced in
https://github.com/twentyhq/twenty/pull/21072

Guard-rejected ApiKey mutations returned malformed GraphQL responses.
RequireAccessTokenGuard (and SettingsPermissionGuard) throw plain
AuthException/PermissionException classes, which are not GraphQLErrors.
ApiKeyResolver had no @UseFilters, so these exceptions were never
translated, they surfaced as request-level errors with no data key
(data: undefined) and a non-FORBIDDEN code, instead of data: null +
FORBIDDEN.

This broke the `createApiKey › should reject a non-ACCESS token even
with API key permission` integration test
(expect(res.body.data).toBeNull() received undefined). The sibling
generateApiKeyToken test passed only because it lives on AuthResolver,
which already declares these filters.

## Fix
Add the standard exception filters to ApiKeyResolver, matching the idiom
used by other guard-protected resolvers
```ts
@UseFilters(AuthGraphqlApiExceptionFilter, PermissionsGraphqlApiExceptionFilter)
```
2026-06-01 18:08:02 +02:00
Paul RastoinandGitHub 6ad6fcce0f Bump playwright (#21113)
Playwright installation is infinite looping in the ci
seems like to be a global outage
2026-06-01 18:06:55 +02:00
ad47b2972c i18n - translations (#21112)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-01 17:42:34 +02:00
ba493e5a23 fix: show relation field changes in the timeline (#21052)
## Summary
- Fixes #20970 
- When we changed only a relation field (e.g. company on a person):
- The diff builder skipped all RELATION fields -> empty diff -> no
timeline row.
- If we change company and something else, only the scalar field
appeared; the company change was missing.
- Even with a diff, the UI validated keys by field name (`company`)
while join columns (`companyId`) could be filtered out.

## Solution
- For `MANY_TO_ONE`, compare join column values (`companyId`) and store
the diff under the relation field name (`company`):
```
"company": {
  "before": { "id": "<old-id>" },
  "after": { "id": "<new-id>" }
}
```
- Frontend
- `filterOutInvalidTimelineActivities`: resolve diff keys by field name
or join column via `findFieldMetadataItemByDiffKey`, so relation diffs
are not stripped.
- `EventRelationFieldDiffValues`: resolve related record labels by id;
show only the new value in the row (same pattern as other fields:
Company -> airSlate).
- Tooltip (relations only): on hover, show before -> after with readable
names (e.g. Microsoft -> Apple). Scalar and composite fields (e.g.
Updated by) are unchanged and do not get this tooltip.
- `EventFieldDiff`: route RELATION diffs to the relation renderer; all
other field types keep the existing FieldDisplay behavior.

### What you’ll see
On a person (or opportunity) timeline after changing company:
```
You updated Company → airSlate
(tooltip: Microsoft → Apple)
```

## Test plan
- Change only company on a person -> timeline shows a company update
with names.
- Change company and name in one save -> both appear in the diff.
- Clear company -> row shows Empty; tooltip reflects previous -> empty
if applicable.
- Same we can do for the Opportunities also
- Scalar / ACTOR fields (e.g. Updated by) - no new tooltip; display
unchanged.

## Screenshots

### Before
<img width="527" height="124" alt="Screenshot 2026-05-29 155123"
src="https://github.com/user-attachments/assets/e067f19a-8184-4e50-9cd0-9135e06188b8"
/>
<br><br>
<img width="535" height="181" alt="Screenshot 2026-05-29 155149"
src="https://github.com/user-attachments/assets/3c513a1e-c5c5-4cff-ae1c-5fed62837798"
/>
<br><br>

### After
<img width="564" height="200" alt="Screenshot 2026-06-01 154539"
src="https://github.com/user-attachments/assets/459ad6b4-af4c-4f7a-b749-30762c979627"
/>
<img width="567" height="187" alt="Screenshot 2026-06-01 154555"
src="https://github.com/user-attachments/assets/6bc4711c-afca-43d8-b874-f51fc0f374df"
/>
<img width="563" height="188" alt="Screenshot 2026-06-01 154639"
src="https://github.com/user-attachments/assets/f275b9fd-a1c3-4c4b-9538-8042657eb593"
/>
<img width="556" height="180" alt="Screenshot 2026-06-01 154654"
src="https://github.com/user-attachments/assets/2e614368-8fd7-4c73-8876-2223f2f98e67"
/>
<img width="560" height="237" alt="Screenshot 2026-06-01 154802"
src="https://github.com/user-attachments/assets/0437aef4-7b2c-4d35-a2e9-f617b90a1beb"
/>

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-01 14:34:54 +00:00
Thomas TrompetteandGitHub 627b488556 Fix else branches not properly skipped in nested if/else workflows (#20938)
## Summary

- Extract `findParentSteps` utility that recognizes IF-ELSE steps as
parents of their branch children (via
`settings.input.branches[].nextStepIds`), used in all parent detection
sites (`shouldSkipStepExecution`, `shouldExecuteStep`,
`shouldFailSafely`, and their iterator variants)
- Centralize next-step resolution in `getNextStepIdsToExecute` via
extracted `getNextStepIdsForIterator` and `getNextStepIdsForIfElse`
utils — Iterator now properly returns loop children as
`nextStepIdsToSkip`/`nextStepIdsToFailSafely` when skipped
- Refactor `skipAndFailSafelyStepsThenContinue` to delegate to
`getNextStepIdsToExecute` instead of duplicating type-specific
propagation logic

Fixes #20934

## Test plan

- [x] New unit tests for `findParentSteps` (7 tests covering IF-ELSE
branch parent detection)
- [x] New IF-ELSE-specific tests added to `shouldSkipStepExecution`,
`shouldExecuteStep`, `shouldFailSafely` test suites
- [x] Updated Iterator skip/fail-safely tests in
`workflow-executor.workspace-service.spec.ts`
- [x] All 300 workflow executor tests pass
- [x] `lint:ci` passes
2026-06-01 17:03:50 +02:00
WeikoandGitHub b9e5ff2065 fix: broadcast timeline activities to live SSE subscriptions (#21104)
## Context
Timeline activities never updated in real time. They were explicitly
excluded from the database-event pipeline
(formatTwentyOrmEventToDatabaseBatchEvent early-returned for the
timelineActivity object), so no SSE event was ever broadcast, and the
frontend timeline only refreshed on mount/manual refetch.

## Implementation
Backend
- Feat: Stop dropping timeline-activity events in
formatTwentyOrmEventToDatabaseBatchEvent.
Instead route them through EntityEventsToDbListener, which publishes
them directly to live subscriptions (but still skipping webhook/audit
handling).
- Fix: Harden ObjectRecordEventPublisher: wrap nested-relation
enrichment in try/catch so a failure broadcasts the event without
relations instead of dropping it (logs a warning).
- Fix: Skip unreadable relation targets in CommonSelectFieldsHelper when
the role lacks canReadObjectRecords, preventing errors while computing
selected fields.
- Fix: Support MORPH_RELATION alongside RELATION in RLS row-level
permission predicate matching (timeline activities use morph targets).

Frontend
- Feat: useTimelineActivities now registers the timeline query with the
SSE system via useListenToEventsForQuery and refetches on incoming
timeline-activity record operations.
- Feat: Add a skip option to useListenToEventsForQuery so the listener
isn't registered when the object has no timeline field.

## Test


https://github.com/user-attachments/assets/ed1d1c66-d6ea-434d-ac9c-9b83d2b78338


Note: "UpdatedBy" seems to be listen to and visible in the timeline
activity summary, this is probably a bug that we want to fix
2026-06-01 16:32:08 +02:00
EtienneandGitHub 381ca32055 Billing - Fix credit upgrade invoice error (#21097)
In createImmediateUpgradeInvoice, the invoice is finalized with
auto_advance: true, which causes Stripe to automatically attempt payment
asynchronously. Then the explicit stripe.invoices.pay(invoice.id) call
races against that auto-payment — if Stripe already paid it, this throws
"Invoice is already paid".

The fix is to finalize with auto_advance: false and keep the explicit
pay call
2026-06-01 13:55:06 +00:00
nitinandGitHub 6029847491 fix navigation item tree breadcrumb active state (#21101)
before - 



https://github.com/user-attachments/assets/9a35e07c-def3-47eb-aab4-0bdcaf302d38



after - 


https://github.com/user-attachments/assets/bf452b5a-7a31-4838-83ca-27cae598ef4d
2026-06-01 13:53:42 +00:00
MarieandGitHub 66afd5a1de Fix array-typed parameters in code/logic-function action forms (#21102)
## Problem

`any[]` type prevented value input:
<img width="544" height="349" alt="Screenshot 2026-06-01 at 14 08 47"
src="https://github.com/user-attachments/assets/956238d0-6fea-4be1-b75a-ab0e6e6424ac"
/>

In the workflow Code action (and the Logic Function action), parameters
typed as any[], string[], etc. rendered as an empty grey box instead of
an "Enter value" text input. After any debounced save, even a properly
initialised array field would also collapse into an empty container.

The Array<T> / ReadonlyArray<T> generic form fell through to a generic
text input by accident (which "looked" right, but for the wrong reason —
no schema info downstream).

## Root causes
Three places treated arrays as plain objects via @sniptt/guards'
isObject (which is true for arrays):

1. WorkflowEditActionCodeFields.tsx — arrays went into the nested-fields
branch; Object.entries([]) is empty → empty container, no placeholder.
2. mergeDefaultFunctionInputAndFunctionInput.ts — recursed into arrays
during merge, turning [] into {}. Triggered on every debounced save, so
the bug surfaced after any edit.
3. get-function-input-schema.ts — only handled T[]
(SyntaxKind.ArrayType); Array<T> (SyntaxKind.TypeReference) was
unrecognised, so the form lost any item-type info.
2026-06-01 13:14:46 +00:00
da5e1152bb i18n - translations (#21103)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-01 14:24:47 +02:00
Félix MalfaitandGitHub b338a7a1d2 feat(settings): discovery hero rollout + ephemeral playground token (#21072)
## Summary

Two intertwined streams of work:

### UI — discovery hero pattern, settings shell, AI/API redesign
- **Generalize `SettingsDiscoveryHeroCard`** and use it on Layout, Data
Model, Apps, AI, API/Webhooks, Members. Drops 4 per-page wrapper files
(`SettingsObjectCoverImage`, `SettingsLayoutCoverImage`,
`SettingsLayoutCustomizeVideoModal`,
`SettingsDataModelVisualizeVideoModal`). Each page now supplies cover
src, modal id, and tab list.
- **Modal**: swap `<video>` placeholder for the Vimeo iframe pattern
from `twenty-docs`, per-tab `vimeoId`. Drop the parallel border-bottom
on the header (TabList draws its own baseline) and the grey background
behind the video. Note: Vimeo's embed allowlist applies — the iframes
load with the correct URL on `localhost` but the player itself requires
the video owner to allow the dev/staging domains in Vimeo settings.
- **AI page** rebuilt into a Cockpit pattern (Overview / Models / Skills
/ Tools / Usage). New `SettingsAiOverviewTab` with default Smart/Fast
pickers, at-a-glance stats, and an MCP signpost that deep-links to
`/settings/api-webhooks#mcp`. System Prompt link moved under Models.
Advanced tab removed.
- **API & Webhooks** now has 4 tabs (Playground / MCP / API Keys /
Webhooks). Hero card above tabs. Playground tab inverted to "Core API" /
"Metadata API" sections, each containing REST + GraphQL cards — schema
is the meaningful axis, protocol is secondary. Hash deep-link sync
delegated to the shared `TabListFromUrlOptionalEffect`.
- **Settings shell**: unified drawer outer padding (kill `isSettings`
branch), extract `CollapsibleNavigationDrawerSection`, add `iconColor`
on settings nav items, fix Exit Settings button alignment, 880px content
cap.

### Backend — strategy C: ephemeral playground token
The legacy paste-your-API-key flow is replaced by an on-demand
short-lived token scoped to the calling user's permissions. No shared
"Playground" API key to manage or revoke.

- New `JwtTokenTypeEnum.PLAYGROUND`. `PlaygroundTokenJwtPayload =
Omit<AccessTokenJwtPayload, 'type' | impersonation fields>` so any
future ACCESS claim flows through automatically.
- `AccessTokenService.generatePlaygroundToken` signs an access-shaped
JWT with `type: PLAYGROUND` and a configurable short TTL. A shared
private `resolveTokenSubject` helper parallelizes the user / workspace /
userWorkspace lookups for both generators.
- `JwtAuthStrategy.validateAccessToken` widened to accept
`AccessTokenJwtPayload | PlaygroundTokenJwtPayload`; impersonation gated
on `payload.type === ACCESS` so the union narrows without `as unknown
as` casts. The two branches in `validate()` collapse into one.
- New `PLAYGROUND_TOKEN_EXPIRES_IN` config var (default `2h`).
- New `generatePlaygroundToken` mutation (`WorkspaceAuthGuard`, no args,
returns `AuthToken`).
- Frontend `useOpenPlayground` hook centralizes mint → atom write →
navigate, with Apollo `onError` snackbar and a "use cached PLAYGROUND
token if still fresh" short-circuit (decodes via `jwt-decode`, checks
both `type` AND `exp`). Old API_KEY tokens left in localStorage from the
prior paste-form flow are rejected on `type` alone and force a re-mint —
this is what was causing the "This API Key is revoked" symptom on stale
browsers.

### Drive-by cleanups
- `PlaygroundToken` DTO removed (identical shape to `AuthToken` already
in use).
- 5 `customize-sidebar.webm` imports and the dead placeholder pipeline
removed.

## Test plan

### Discovery hero
- [ ] `/settings/layout`, `/settings/data-model`,
`/settings/applications`, `/settings/ai`, `/settings/api-webhooks`,
`/settings/members` each render the discovery hero card with its
illustration + play button + tabbed modal
- [ ] Modal tabs show the correct Vimeo embed URL per tab; aspect ratio
stays at 1440/900; no parallel border-bottom jog at the tab baseline
- [ ] AI Overview tab shows Smart/Fast model pickers + stats grid + MCP
signpost card; the MCP card lands on `/settings/api-webhooks#mcp` with
the MCP tab active

### API playground (ephemeral token)
- [ ] With an empty `playgroundApiKeyState` in localStorage, clicking
REST or GraphQL playground card opens the playground and the cached
token has `type: "PLAYGROUND"` with ~2h exp
- [ ] Clicking the card again within the freshness window does **not**
re-mint (`iat` / fingerprint stable across visits)
- [ ] Planting a fake API_KEY-shaped JWT in localStorage and clicking
the card forces a fresh mint (old token rejected on `type`)
- [ ] `GET /rest/companies?limit=1` with the cached token returns 200 +
real data
- [ ] `POST /graphql { __typename }` returns 200

### Settings shell
- [ ] Settings nav matches main app drawer padding; sections collapse;
Exit Settings button aligns with the workspace links above
- [ ] Active nav items have a right-gap (cleaner active state)
- [ ] Content area capped at 880px

### Verify
- [ ] `npx nx typecheck twenty-front` passes
- [ ] `npx nx typecheck twenty-server` passes
- [ ] `npx nx lint:diff-with-main twenty-front` passes
- [ ] `npx nx lint:diff-with-main twenty-server` passes
2026-06-01 14:16:02 +02:00
nitinandGitHub 6e00a122c6 fix(kanban): contain checkbox hover reveal within card bounds (#21100)
follow up to https://github.com/twentyhq/twenty/pull/20455

before - 


https://github.com/user-attachments/assets/e5a8a328-81ec-4dc4-8e54-1a54cf252135


after - 



https://github.com/user-attachments/assets/61cbb856-564c-487f-81e5-e27adc4a0d2d
2026-06-01 12:20:40 +02:00
4dff30f676 Twenty server:Fix REST pagination issues (#20980)
Fixes #20109 

The entry was repeating because in the database we store DateTime fields
with microsecond precision (timestamptz), but when JS parses timestamptz
into a Date object it only keeps millisecond precision.

### Example
If previous cursor was:
```
{
    name: "Quick Lead",
    createdAt: "2026-05-21T15:33:00.708Z",
}
```
The resulting query look something like:
```
...
WHERE (
  "workflow"."name" > "Quick Lead"
  OR (
    "workflow"."name" = "Quick Lead"
    AND "workflow"."createdAt" > "2026-05-21T15:33:00.708Z"
  )
  OR (
    "workflow"."name" = "Quick Lead"
    AND "workflow"."createdAt" = "2026-05-21T15:33:00.708Z"
    AND "workflow"."id" > "8b213cac-a68b-4ffe-817a-3ec994e9932d"
  )
)
```
So, when comparing the 2nd condition the `"workflow"."createdAt" >
"2026-05-21T15:33:00.708Z"` would always result to true because in db
the data for createdAt is `2026-05-21 21:03:00.708 +0530` which will
always be greater than `2026-05-21T15:33:00.708Z`
The second condition `"workflow"."createdAt" >
"2026-05-21T15:33:00.708Z"` always evaluates to true, because the value
actually stored in the DB for createdAt is something like `2026-05-21
21:03:00.708264 +0530`, which is always greater than
`2026-05-21T15:33:00.708Z` in the cursor. The row used to generate the
cursor therefore reappears on the next page.

### My solution
Truncate the column to milliseconds in the comparison so both sides have
the same precision: `date_trunc('milliseconds', ${fieldReference})`.

For the issue of nested sorting filters, when ordering by a composite
field (e.g. `createdBy.name`), `encodeCursor` stored the entire
composite object (`source`, `workspaceMemberId`, `name`, `context`). The
where-condition builder later iterated those sub-keys and threw "Invalid
cursor" because only name had an orderBy direction.

P.S: Duplicate of #20867 because last fork got polluted.

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-01 08:50:14 +00:00
71c377484e fix(front): keep app variable cache in sync after update (#20861)
Updating an application variable in Workspace / Applications / <App> /
Settings persisted server-side but the Apollo cache kept the old value.
Switching tabs unmounted the settings tab, and remounting reseeded the
input from the stale cache — only a full refresh showed the new value.

Mutation now writes the new value into the ApplicationVariable entity
via cache.modify, so FindOneApplication reflects the change immediately.
Hook + table updated to pass the variable id through.

Adds a hook test that pre-seeds the cache and asserts the cached value
after the mutation.

NOTE: saving the plain value in Apollo's cache might not be the best
approach here

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-01 08:14:28 +00:00
26906951b3 fix(twenty-sdk): minify front-component bundles & set NODE_ENV=production in deploy build (#20937)
## Summary

`twenty deploy` (and `twenty build`) currently ship front-component
`.mjs` bundles **unminified**, with `process.env.NODE_ENV` undefined at
build time. Two missing options in
`get-base-front-component-build-options.ts` — fix is two lines.

## Why this matters

Each front-component bundle includes React + ReactDOM + the design
system AOT (only `twenty-client-sdk/{core,metadata}` are listed in
`FRONT_COMPONENT_EXTERNAL_MODULES`). A trivial widget measures **~2 MB**
unminified. Because every widget mount spawns a fresh Web Worker that
re-fetches and re-parses the bundle (`FrontComponentWorkerEffect.tsx`),
that 2× size translates directly into 2× cold-start latency on **every
record-page navigation and every browser refresh**. On a CRM with even a
handful of custom widgets this dominates perceived UI latency.

## Why it bites every app, not one user

Reference apps in this repo
(`packages/twenty-apps/fixtures/{minimal,rich}-app`,
`community/github-connector`, `internal/twenty-for-twenty`) ship the
same way — verified by inspecting the released `twenty-sdk@2.5.0`
bundle. There's no CLI flag, env var, or config option to opt into a
production build. A search of issues/PRs for "minify", "bundle size",
"production build" surfaces nothing tracking this.

## Fix

Enable `minify: true` and `define: { 'process.env.NODE_ENV':
'"production"' }` in the base front-component build options. These flow
through `build-application.ts` (the orchestrator for `twenty deploy` and
`twenty build`).

**Watch mode (`twenty dev`) is intentionally untouched.**
`esbuild-watcher.ts` has its own configuration path that doesn't consume
`getBaseFrontComponentBuildOptions()`; it stays unminified so local
rebuilds remain fast and stack traces remain readable during
development.

## Measured impact

Two production extension apps using `twenty-sdk@2.5.0`:

| File | Before | After | Δ |
|---|---:|---:|---:|
| `oapps-deal-items` (single widget) | 2,114,953 B | 863,444 B |
**−59%** |
| `oapps-document-hub/documents-panel` | 2,120,341 B | 872,478 B |
**−59%** |
| `oapps-document-hub/hub-document-record` | 2,077,013 B | 852,544 B |
**−59%** |
| `oapps-document-hub/field-mapping-editor` | 1,168,941 B | 255,787 B |
**−78%** |

End-user effect: opening an Opportunity record with two custom-widget
tabs went from ~3-4s widget paint to under 1s on the same machine, same
browser, same record.

## Risk / scope

- **No behavior change.** Minification is a transparent transform;
`NODE_ENV=production` is the standard signal libraries already gate on.
No app code changes needed.
- **No effect on `twenty dev`** — separate code path.
- **No effect on logic functions** — they use their own build-options
object.
- One file touched.

## Test plan

- [ ] `yarn twenty deploy` on
`packages/twenty-apps/fixtures/minimal-app` → output `.mjs` is mangled
and `process.env.NODE_ENV` no longer appears literally inside the
bundle.
- [ ] `yarn twenty dev` on the same app → output `.mjs` remains
readable.
- [ ] Existing CI green.

Happy to add a feature-flag (`TWENTY_BUILD_MODE` env var or `twenty
deploy --no-minify` escape hatch) if maintainers prefer that over
unconditional minification.

---------

Co-authored-by: 8Maverik8 <8maverik8@users.noreply.github.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-01 08:13:55 +00:00
51202d5a32 fix(front): scroll long content in rich text editor (#20319)
## Summary

Fixes scroll behavior in the rich text editor.

Long content was unbounded — the popup grew off-screen, the
expand-to-side-panel button became unreachable, and the side panel
itself didn't scroll either.

Closes #20309

## Changes

- `RichTextFieldInput`: cap the popup at `min(60vh, 500px)` and wrap the
editor in a scrollable region. Collapse button stays at the top via
`align-items: flex-start`.

- `RecordInlineCellEditMode`: add `shift()` middleware to keep the popup
inside the viewport after `flip()` triggers (previously the popup could
extend above the viewport top with the collapse button out of reach).

- `SidePanelContainer` / `SidePanelRouter`: add `min-height: 0` to the
flex-column chain so the existing `overflow-y: auto` on the content area
can actually clip and scroll long children.

The previous attempt in #20310 added the same `min-height: 0` plus a
nested overflow wrapper inside the rich-text page; the nested wrapper
turned out to be the reason scrolling didn't work.

## Test plan

- [x] Open a record with a long `RICH_TEXT` field
- [x] Click the field — popup opens bounded; long content scrolls inside
- [x] Click the expand button (top-right) — side panel opens
- [x] Side panel: long content scrolls vertically
- [x] Popup near the bottom of the viewport flips upward and stays fully
inside the viewport (collapse button remains visible)
- [x] `lint:diff-with-main`, `typecheck`, prettier — green



https://github.com/user-attachments/assets/827be881-aadd-49ed-9ddc-7566c00cf4be

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 08:13:11 +00:00
Thomas TrompetteandGitHub f4380f89a8 fix: SSE event stream reconnection after idle connection death (#21061)
The SSE event stream could silently die from network partitions, NAT
table flushes, browser tab throttling, or server restarts. When this
happened:
1. The `error` callback only called `captureException` — no reconnection
was triggered
2. The `complete` callback was `() => {}` — a cleanly terminated stream
left the client permanently broken
3. No mechanism existed to detect a silently dead connection where no
FIN/RST was received

## Summary

- **Fix `error`/`complete` callbacks**: The `graphql-sse` subscription's
`error` callback only reported to Sentry, and `complete` was a no-op.
Both now set `shouldDestroyEventStreamState = true` to trigger the
destroy-recreate lifecycle, ensuring detected transport failures and
clean stream terminations lead to automatic reconnection.
- **Add server-side keepalive**: The existing heartbeat timer now runs
every 30s (instead of 6min) and publishes empty events through the Redis
pub/sub channel in addition to refreshing the Redis TTL (throttled to
~6min). Unlike GraphQL Yoga's opaque SSE comment pings, these are real
subscription events that flow through the client's `next`/`message`
handlers.
- **Add client-side keepalive monitor (`SSEKeepAliveEffect`)**: Tracks
the timestamp of the last received event. If no event arrives within 90
seconds (3x the keepalive interval), it clears query listeners and
triggers a stream destroy-recreate cycle.

## Test plan

- [x] Start the app, verify SSE events flow normally (workflow runs
update in real-time)
- [x] Leave the app idle for >90 seconds, then trigger a workflow run —
verify the stream auto-reconnects and events are delivered
- [x] Kill the server, restart it, verify the frontend recovers its
event stream
- [x] Verify keepalive events (empty
`objectRecordEventsWithQueryIds`/`metadataEvents`) appear in browser
network tab every ~30s
- [x] Verify no regressions in SSE-dependent features (record updates,
metadata changes, workflow run visualization)
2026-06-01 08:09:22 +00:00
nitinGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Félix Malfait
e430e4ea0a fix(ai): route xAI search through Responses API as native tools (#21037)
xAI deprecated Live Search, so the `searchParameters` provider option
now returns 410. This routes all xAI models through the Responses API
and binds web/X search as native agent tools, matching how
Anthropic/OpenAI expose search.

- xAI provider now uses `provider.responses()` — its
`webSearch()`/`xSearch()` tools only run against the Responses endpoint,
not chat completions
- web/X search migrated from the `provider-option` variant to `sdk-tool`
(`web_search`/`x_search`); deleted the dead `searchParameters` path, the
`provider-option` variant, and `providerOptions` on `NativeModelBinding`
- dropped a dead `rolePermissionConfig` param on `getAgentRoleId`, left
over from #20331

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
2026-05-31 15:11:04 +02:00
Abdullah.andGitHub b027e4bdb1 [Website] i18n module, page-local sections, translatable copy (#21082)
**i18n** — collapsed the ~22 scattered i18n files into a single module
and turned on Spanish alongside French.

**Sections** — dropped the old compound pattern (`Section.Root`,
`Section.Heading`, …). Reusable layout shells moved to `src/templates/`,
atomic bits stay in `design-system/`, and each page now owns its copy in
local `_components` blocks instead of pulling it out of shared sections.
Data files hold arrays only, no prose.

**Copy → `<Trans>`** — A lot of headings were split across several
`<HeadingPart>`s just for font styling, which meant each piece was a
separate translation string. A translator got "Build your Enterprise
CRM" and "at AI Speed" as two unrelated strings and had no way to
reorder them for their language. Those are now single `<Trans>` units
with placeholders. Same idea for the old `\n` + `white-space: pre-line`
line-break trick: replaced with a small `ResponsiveLineBreak` element so
the break is doesn't quietly rot, and did a dead-code pass.

The de-fragmentation changes the message IDs, so around 60 strings will
fall back to English in fr/es until Crowdin re-syncs.
2026-05-31 12:39:35 +00:00
fc90b4ba8b i18n - docs translations (#21064)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 19:30:26 +02:00
88b77cb699 feat(server): opt-in FRONT_AUTO_BASE_URL for hostname-relative API URL (#20504)
## Problem

`generateFrontConfig()` writes `window._env_.REACT_APP_SERVER_BASE_URL =
process.env.SERVER_URL` unconditionally. The frontend then pins to that
absolute URL. For self-hosted deployments reachable from multiple
hostnames (Tailscale IP, LAN IP, internal DNS, SSH tunnel to localhost,
public DNS), only the one matching `SERVER_URL` works — others hit CORS
errors or unreachable hosts because the frontend tries to call the API
at the configured URL, not the one the user came in via.

The frontend already supports the right fallback:
`packages/twenty-front/src/config/index.ts:20-21` reads
`window._env_?.REACT_APP_SERVER_BASE_URL` and falls back to
`getDefaultUrl()` (which uses `window.location`) when the env var is
absent. But the server-side `generateFrontConfig` always populates
`_env_`, so the fallback never runs.

## Fix

One file: `packages/twenty-server/src/utils/generate-front-config.ts`.
Add a `FRONT_AUTO_BASE_URL=true` opt-in (also triggered when
`SERVER_URL` is unset entirely). When the toggle is on, inject
`window._env_ = {}` so the frontend's existing `getDefaultUrl()`
fallback resolves the origin from `window.location` at runtime.

## Backwards compatibility

When `SERVER_URL` is set AND `FRONT_AUTO_BASE_URL` is unset (or anything
other than `'true'`): unchanged — `REACT_APP_SERVER_BASE_URL:
process.env.SERVER_URL` is injected exactly as before.

The toggle is strictly additive. Existing single-hostname deployments
are not affected.

## Use case

Self-hosted Twenty reachable via:
- `http://100.115.12.29` over Tailscale
- `http://localhost:4440` over SSH tunnel
- `http://twenty.internal` over LAN DNS
- `http://crm.example.com` public

With `FRONT_AUTO_BASE_URL=true`, all four paths work without rebuilds or
per-hostname server processes.

## Test plan

- [ ] `SERVER_URL=http://x.com` (toggle unset) → `<script>window._env_ =
{"REACT_APP_SERVER_BASE_URL":"http://x.com"};</script>` (unchanged from
main)
- [ ] `SERVER_URL` unset → `<script>window._env_ = {};</script>` (new
fallback path)
- [ ] `SERVER_URL=http://x.com FRONT_AUTO_BASE_URL=true` →
`<script>window._env_ = {};</script>` (toggle wins)
- [ ] `FRONT_AUTO_BASE_URL=false SERVER_URL=http://x.com` → unchanged
(only `'true'` triggers the toggle)

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-29 16:26:43 +00:00
Raphaël BosiandGitHub 0ed2e9d82d Docs: clarify numberOfSelectedRecords usage for RECORD_SELECTION items (#21059)
Add a note to the command menu items docs explaining that
RECORD_SELECTION already guarantees a non-empty selection, so
numberOfSelectedRecords > 0 is redundant in
conditionalAvailabilityExpression.
2026-05-29 17:59:14 +02:00
643cfe9b13 i18n - docs translations (#21062)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 17:29:40 +02:00
Thomas TrompetteandGitHub bc1b7f6fdf fix: resolve workflow form step auto-open race condition (#21053)
## Summary
- Fix intermittent failure where the Quick Lead workflow form step did
not auto-open
- Root cause: race conditions between SSE events, Apollo cache writes,
and the `runWorkflowVersion` mutation timing
- Add generic monotonicity guard in the SSE handler that drops stale
updates for all records (not just WorkflowRun)

## Changes
- **`useTriggerOptimisticEffectFromSseUpdateEvents.ts`**: Compare
incoming `updatedAt` with cached record before writing — skip if stale.
Moved `upsertRecordsInStore` after the guard so neither Apollo cache nor
Jotai store receive stale data.
- **`useRunWorkflowVersion.tsx`**: Await mutation before opening side
panel; register SSE listener eagerly before mutation
- **`useWorkflowRun.ts`**: Simplified back to plain `useFindOneRecord` +
schema parse (no extra state needed)
- **`generateWorkflowRunDiagram.ts`**: `shouldOpenStep` matches both
PENDING and RUNNING for form steps (backend RUNNING means "waiting for
user input")
- **`WorkflowRunVisualizerEffect.tsx`**: Pass `runStatus` directly
without status mapping
- **`WorkflowRunStepNodeDetail.tsx`**: Form is interactive when step is
PENDING or RUNNING
- **Deleted `latestWorkflowRunFamilyState.ts`**: No longer needed — the
generic SSE guard replaces it

## Test plan
- [x] Hard refresh, run Quick Lead workflow 10+ times — form should
always auto-open
- [x] Complete the form and verify all subsequent steps execute without
getting stuck
- [x] Verify the workflow diagram is always visible (never disappears)
- [x] Verify other record types still update correctly via SSE (e.g.
edit a person in another tab)
2026-05-29 14:18:24 +00:00
Raphaël BosiandGitHub 57118a868f Docs update: Calling a logic function from a front component (#21057)
Documents how a headless front component calls a server-side logic
function over HTTP via the /s/ route, so AI agents have a clear
reference for implementing this pattern.
2026-05-29 14:04:19 +00:00
93f848fd2f i18n - translations (#21055)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 15:30:31 +02:00
Félix MalfaitandGitHub 667cb95730 fix(sso): accept HTTP-POST binding and surface descriptive parser errors (#21051)
Fixes https://github.com/twentyhq/twenty/issues/21044

## Summary
- Closes [#490](https://github.com/twentyhq/private-issues/issues/490) —
JumpCloud customers (and anyone else whose IdP only advertises
`HTTP-POST` for `SingleSignOnService`) could not upload their SAML
metadata; the parser silently rejected them with a generic `Invalid
file` toast.
- The SAML IdP metadata parser now falls back to `HTTP-POST` when
`HTTP-Redirect` is not advertised. Both are valid SAML 2.0 bindings.
- The parser now returns a descriptive `reason` string (Zod issues +
custom errors) instead of an opaque `error: unknown`, and the upload
snack bar surfaces it so the customer can self-diagnose (e.g. `entityID:
entityID is not a valid URL` if they forgot to fill in their IdP Entity
ID).
- Added unit tests for HTTP-POST-only metadata, HTTP-Redirect
preference, and each descriptive-error path.

## Test plan
- [x] `npx jest parseSAMLMetadataFromXMLFile
--config=packages/twenty-front/jest.config.mjs` — 8/8 pass
- [x] `npx oxlint -c packages/twenty-front/.oxlintrc.json` on changed
files — clean
- [x] `npx oxfmt --check` on changed files — clean
- [ ] Manual: upload the customer's JumpCloud metadata (HTTP-POST only,
placeholder `entityID`) and confirm the error now says `Invalid file:
entityID: entityID is not a valid URL` instead of `Invalid file`
- [ ] Manual: upload metadata with a real `entityID` and HTTP-POST-only
binding, confirm the form populates correctly
2026-05-29 15:22:50 +02:00
martmullandGitHub 4e5d47168c 2439 improve command menu item display in right panel (#21020)
## Before

<img width="1512" height="389" alt="image"
src="https://github.com/user-attachments/assets/33274356-fb99-4a02-baa7-c324e6d151c6"
/>


## After

<img width="1512" height="357" alt="image"
src="https://github.com/user-attachments/assets/c0affb71-e920-4d64-b2f0-1bed53209ea5"
/>
2026-05-29 11:37:27 +00:00
neo773andGitHub 10c0bed462 fix: harden email-group SES provisioning, cleanup, and inbound replay (#21046)
## Changes

**Provisioning idempotent** (`aws-ses-register-domain.service.ts`)
- Each SES create call (`CreateConfigurationSet`, event destination,
contact list, tenant association) now swallow `AlreadyExistsException`
via `.send().catch()`.
- Retry after partial failure re-run every step, no blow up on "already
exists". Before: one existing resource kill whole provision.

**Workspace delete clean up cloud** (`workspace.service.ts`,
`emailing-domain-workspace-cleanup.job.ts`,
`emailing-domain.service.ts`)
- On workspace delete, fetch domain list first, pass domains to cleanup
job.
- Cleanup now loop `driver.cleanupDomain(domain)` per domain +
`deprovisionWorkspace`. Tear down SES identity/tenant/config-set, not
just delete DB rows.
- Before: DB rows gone, SES resources orphaned forever. Now: cloud match
DB.

**Inbound replay dedupe** (`ses-inbound-mail-handler.service.ts`)
- Use `snsMessageId` as job id. SNS deliver same message twice → second
is no-op. No duplicate inbound email import.
2026-05-29 10:31:03 +00:00
08b1c5738d i18n - translations (#21050)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 12:12:35 +02:00
martmullandGitHub c2df39405c Fix admin pannel server variable config tab (#21017)
## Before

<img width="1046" height="490" alt="image"
src="https://github.com/user-attachments/assets/450557de-fcf5-4b51-afdb-36c0c36e43d8"
/>


## After

<img width="1040" height="414" alt="image"
src="https://github.com/user-attachments/assets/4a5fe2ab-85d6-4431-9397-6f81ae24055d"
/>
2026-05-29 09:55:11 +00:00
3e2c50c6cf i18n - translations (#21048)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 11:21:23 +02:00
f67db8d8b2 i18n - translations (#21047)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 11:17:19 +02:00
3cd2458fdf i18n - website translations (#21045)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 11:12:31 +02:00
nitinandGitHub 13f09d8946 [Dashboards] Remove gauge chart types and code (#20410)
Follow-up cleanup to #20172.
2026-05-29 11:08:50 +02:00
martmullandGitHub 961745e9ba Fix latency spike on application lookup (#21042)
Fixes
https://discord.com/channels/1130383047699738754/1509645089062781058

Caching application entities to improve authentication latency
2026-05-29 11:06:36 +02:00
MarieandGitHub 41832c8d82 Fix workflow creation on view filtered by status (#21027)
Creating a workflow on a table with with a filter on status (eg: status
is "active") failed because it added the status to createOneWorkflow (in
order to have the record belonging to the view) - while
createOneWorkflow throwed a 400 exception when attempting to create a
workflow with a status (does not correpsond to a valid behaviour).

Silently stripping status rom create workflow endpoints.
2026-05-29 08:36:59 +00:00
3041ed3b6e chore: sync AI model catalog from models.dev (#21041)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-29 09:12:25 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Félix MalfaitWeikoclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Félix Malfait
6d550611d2 chore(deps): bump typescript from 5.9.2 to 5.9.3 (#20991)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.2
to 5.9.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/microsoft/TypeScript/releases">typescript's
releases</a>.</em></p>
<blockquote>
<h2>TypeScript 5.9.3</h2>
<p>Note: this tag was recreated to point at the correct commit. The npm
package contained the correct content.</p>
<p>For release notes, check out the <a
href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-9/">release
announcement</a></p>
<ul>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.9.0%22+is%3Aclosed+">fixed
issues query for Typescript 5.9.0 (Beta)</a>.</li>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.9.1%22+is%3Aclosed+">fixed
issues query for Typescript 5.9.1 (RC)</a>.</li>
<li><em>No specific changes for TypeScript 5.9.2 (Stable)</em></li>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.9.3%22+is%3Aclosed+">fixed
issues query for Typescript 5.9.3 (Stable)</a>.</li>
</ul>
<p>Downloads are available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/typescript">npm</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/microsoft/TypeScript/commit/c63de15a992d37f0d6cec03ac7631872838602cb"><code>c63de15</code></a>
Bump version to 5.9.3 and LKG</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/8428ca4cc8a7ecc9ac18dd0258016228814f5eaf"><code>8428ca4</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/62438">#62438</a>
(Fix incorrectly ignored dts file fr...) into release-5.9 (#...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/a131cac6831aa6532ea963d0cb3131b957cad980"><code>a131cac</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/62351">#62351</a>
(Add missing Float16Array constructo...) into release-5.9 (#...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/04243333584a5bfaeb3434c0982c6280fe87b8d5"><code>0424333</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/62423">#62423</a>
(Revert PR 61928) into release-5.9 (<a
href="https://redirect.github.com/microsoft/TypeScript/issues/62425">#62425</a>)</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/bdb641a4347af822916fb8cdb9894c9c2d2421dd"><code>bdb641a</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/62311">#62311</a>
(Fix parenthesizer rules for manuall...) into release-5.9 (#...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/0d9b9b92e2aca2f75c979a801abbc21bff473748"><code>0d9b9b9</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/61978">#61978</a>
(Restructure CI to prepare for requi...) into release-5.9 (#...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/2dce0c58af51cf9a9068365dc2f756c61b82b597"><code>2dce0c5</code></a>
Intentionally regress one buggy declaration output to an older version
(<a
href="https://redirect.github.com/microsoft/TypeScript/issues/62163">#62163</a>)</li>
<li>See full diff in <a
href="https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typescript&package-manager=npm_and_yarn&previous-version=5.9.2&new-version=5.9.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
2026-05-29 08:39:35 +02:00
Félix MalfaitandGitHub 3c97d9648b fix(address): show saved address in record detail when street1 is null (#21033)
## Fixes #20084

### Problem
A saved address is visible in the **table view** but shows **"Empty"**
in the **record detail page** when `addressStreet1` is `null`.

This reproduces with the default seed data out of the box — e.g.
**Google** (city "Mountain View", no street), **Microsoft** (Redmond),
**Meta** (Menlo Park) — which is why several users reported hitting it
immediately.

### Root cause
The frontend zod schema required `addressStreet1` to be a **non-null**
string:

```ts
// isFieldAddressValue.ts
export const addressSchema = z.object({
  addressStreet1: z.string(),          // ← required non-null
  addressStreet2: z.string().nullable(),
  ...
});
```

…but the backend composite type marks it `isRequired: false`
(`address.composite-type.ts`), and the DB column is nullable. So the API
legitimately returns `addressStreet1: null` when only other subfields
are filled.

The two views diverge on how they render:

- **Record detail** gates the value behind `useIsFieldEmpty()` →
`isFieldValueEmpty()`, which for addresses calls
`isFieldAddressValue()`. With `addressStreet1: null` the `safeParse`
**fails**, so `isFieldValueEmpty` returns `true` and the `"Empty"`
placeholder is shown (`RecordInlineCellDisplayMode`).
- **Table view** (`RecordTableCellDisplayMode`) renders
`AddressFieldDisplay` directly with **no** empty check, so the address
stays visible.

This was a latent mismatch since the address guard was introduced.

### Fix
Make `addressStreet1` nullable to match the backend and the other
subfields:

- `addressSchema` → `addressStreet1: z.string().nullable()`
- `FieldAddressValue.addressStreet1` → `string | null`
- `FieldAddressDraftValue.addressStreet1` → `string | null` (keeps the
input/draft type consistent; the text input already renders `?? ''`)

The change is strictly more permissive — persisting and the settings
default-value form still accept string values; they now also accept
`null`.

### Tests
- `isFieldAddressValue.test.ts` — guard returns `true` for
`addressStreet1: null` with other subfields filled.
- `isFieldValueEmpty.test.ts` — new address coverage: empty address is
empty; **`street1: null` + city filled is NOT empty**; normal address is
not empty. (Added an `addressFieldDefinition` mock.)

Both new assertions were confirmed to **fail before the fix** and pass
after.

### Verification
- `npx jest isFieldValueEmpty isFieldAddressValue
normalize-address-field-value-for-persist` → 17 passed
- `npx nx typecheck twenty-front` → pass
- `npx nx lint:diff-with-main twenty-front` → 0 warnings, 0 errors
2026-05-29 08:38:47 +02:00
Félix MalfaitandGitHub 3afdabb93e fix(dashboards): isolate pie chart slice labels per widget (#21034)
## Summary

Fixes [#21014](https://github.com/twentyhq/twenty/issues/21014).

When two pie chart widgets shared the same group-by field (and therefore
the same slice ids) but used different aggregation operators (e.g.
`count` vs `sum`), the arc-link labels would mirror between the two
charts — both ending up showing either the count or the sum values,
depending on render order. Center metrics stayed correct.

**Root cause.** Nivo's `ArcLinkLabelsLayer` and `ArcsLayer` (from
`@nivo/arcs`) wire `react-spring`'s `useTransition` with `keys: e =>
e.id`. When two `<ResponsivePie>` instances render with overlapping ids,
the transitioned data bleeds across charts. The center metric is
unaffected because it's computed by a separate hook
(`usePieChartCenterMetricData`).

**Fix.** Namespace the Nivo-computed slice id per widget by passing an
`id` accessor to `<ResponsivePie>`:
```tsx
id={(datum) => `${id}:${String(datum.id)}`}
```
Lookups inside the widget switch to `datum.data.id` (the original,
un-namespaced id stored on the raw datum), so value/percentage
formatting, the custom tooltip, and the legend hover-dim behavior all
keep working.

Touched files:
- `GraphWidgetPieChart.tsx` — add `id` accessor
- `CustomArcsLayer.tsx` — compare legend highlight against
`datum.data.id`
- `getPieChartFormattedValue.ts`, `getPieChartTooltipData.ts` — match on
`datum.data.id`
- Tests for both utils get a regression case covering the namespaced
computed id

## Test plan

- [ ] `npx jest getPieChartFormattedValue` 
- [ ] `npx jest getPieChartTooltipData` 
- [ ] `npx tsc --noEmit` 
- [ ] Manual: dashboard with two pies on the same group-by field, one
`count` and one `sum`, "Display data label" on for both — confirm each
chart shows its own metric on the slices, and the central total is
unchanged.
- [ ] Manual: hover a legend item — the matching slice in that chart
stays solid while the others dim, and the sibling chart is not affected.
- [ ] Manual: clicking a slice still drills into the correctly filtered
view.
2026-05-29 05:51:10 +00:00
25b0e0d091 fix: correct typo occurence -> occurrence in metadata-event-emitter.ts (#21036)
## Summary

Fixes a spelling typo in
`packages/twenty-server/src/engine/subscriptions/metadata-event/metadata-event-emitter.ts`:

- Variable name `occurence` → `occurrence` (4 references on lines 101,
103, 114, 115)

## Changes

-
`packages/twenty-server/src/engine/subscriptions/metadata-event/metadata-event-emitter.ts`
— rename misspelled variable

Co-authored-by: james <li@jamesdeMacBook-Pro.local>
2026-05-29 05:48:57 +00:00
a43e5c3fb3 i18n - translations (#21032)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-28 22:15:29 +02:00
996cdaf3ff refactor(agents): split tool resolution into native and action rails (#20331)
## Summary

Splits AI agent tool resolution into two independent rails:

- **Native tools** — capabilities baked into the model SDK
(Anthropic/OpenAI `web_search`, xAI `web`/`x` provider options). Bound
by `NativeToolBinderService`, controlled by per-agent
`modelConfiguration` toggles. Opaque to Twenty — executed on the model
provider's servers.
- **Action tools** — registry-scoped tools from `ToolRegistryService`
(code interpreter, send email, record CRUD, etc.). Permission-gated via
the agent's role. Executed on Twenty's server.

Both rails merge into a single `ToolSet` at call time. When both
surfaces expose a search tool the model picks at runtime — coexistence
is intentional (relevant once Exa returns as an action, see below).

## Notable changes worth calling out

**Contract change: `AgentAsyncExecutorService.executeAgent` no longer
accepts `rolePermissionConfig`.** Workflow agents now scope exclusively
by the agent's own permission-tab role (`unionOf: [agentRoleId]`). The
previous role-merging path (caller role intersected with agent role) is
removed. No agent role → no registry tools (fail-closed by design).

**`NativeToolBinderService` relocated** from
`core-modules/tool-provider/native/` →
`metadata-modules/ai/ai-models/services/`. The binder needs SDK-package
knowledge, which lives in `ai-models`. Old location created a backwards
module dependency.

**`NATIVE_MODEL_TOOLS_BY_SDK_PACKAGE` is exhaustive over
`AiSdkPackage`** (`Record<>`, not `Partial<Record<>>`). Adding a new SDK
without thinking about native tools now fails the build. SDKs without
native tools (Bedrock, Google, Mistral, Azure, OpenAI-compatible) get
explicit `{}` entries.

**Discriminated union `kind: 'sdk-tool' | 'provider-option'`** lets one
registry describe both function tools (Anthropic/OpenAI) and runtime
sources (xAI). Follows the local `tool-provider` convention from #19321.

## Deferred to follow-ups

- **Exa web search is dropped from this PR** (along with its
`WEB_SEARCH_TOOL` permission flag and the Exa-specific gating). Exa
comes back as an **action/app tool** once apps can define permission
flags through the SDK — ongoing work in #20481.
- **xAI native search currently errors.** xAI deprecated its Live Search
API (the `web`/`x` provider-option sources this rail maps to), so xAI
returns `410` when native search is actually exercised. The code path
itself is clear — it's only hit if you test xAI native tools. Fixed
separately alongside the broader xAI model fixes.

## Conscious non-decisions

- **No "twenty-native" category.** `native` is reserved for
model/provider SDK features; everything Twenty-owned is just a
tool/action.
- **Coexistence over precedence.** No rule forcing an action search tool
to override native search (or vice-versa) — when both exist, it's the
user's choice in workflow agents and the model's choice in chat.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
2026-05-28 22:08:05 +02:00
Raphaël BosiandGitHub 1d84695fb0 Fix: focus stack overwritten when auto-opening title cell on new record (#21029)
Fixes https://github.com/twentyhq/twenty/issues/20894

In `PageChangeEffect` `resetFocusStackToFocusItem` ran right after
`openNewRecordTitleCell`, wiping the title cell entry. Typing in the
auto-opened breadcrumb input (e.g. new workflow) triggered global
shortcuts and ignored Enter / Escape / Tab.

Reordered so the page reset runs first, then the title cell push lands
on top.

## Before


https://github.com/user-attachments/assets/d3c0c266-a493-46b8-b99b-32f4381b8664


## After


https://github.com/user-attachments/assets/3a886386-f438-46d2-a6ea-9ee6908d6df2
2026-05-28 19:23:40 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Weiko
64e0b76d00 chore(deps): bump js-cookie from 3.0.5 to 3.0.7 (#20992)
Bumps [js-cookie](https://github.com/js-cookie/js-cookie) from 3.0.5 to
3.0.7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/js-cookie/js-cookie/releases">js-cookie's
releases</a>.</em></p>
<blockquote>
<h2>v3.0.7</h2>
<ul>
<li>Prevent cookie attribute injection: CVE-2026-46625 (eb3c40e)</li>
<li>Add <code>Partitioned</code> attribute to readme (b994768)</li>
<li>Publish to npm registry via trusted publisher exclusively
(4dc71be)</li>
<li>Ensure consistent behaviour for <code>get('name')</code> +
<code>get()</code> (1953d30)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/17bacba0171dd022728d8fdeba3203c60791bf58"><code>17bacba</code></a>
Craft v3.0.7 release</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/adb823cb7e95ead47f3af4d4951e589acbde2077"><code>adb823c</code></a>
Fix release workflow halting at <code>git tag</code></li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/5f9e759b07d2752e8407a3a43fb5f879bf384c5e"><code>5f9e759</code></a>
May remove Git user config from release workflow</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/6ac921184c7b3b7d9431c88707f56521acd72ab4"><code>6ac9211</code></a>
Fix release workflow not able to push commit + tag</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/2278bc55e1804c4c2d9bd2110a9b449949a52751"><code>2278bc5</code></a>
Fix missing package version bump</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/eb3c40e89731e99b8970faaf35ddad249c6c0020"><code>eb3c40e</code></a>
Prevent cookie attribute injection</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/f6f157f430d707d2ffd0c9c9138227a6cea564e5"><code>f6f157f</code></a>
Bump globals from 17.5.0 to 17.6.0</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/f409d022da50a0c6fa8724f087fbc50fab9a9533"><code>f409d02</code></a>
Bump eslint from 10.2.0 to 10.3.0</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/a686883c03a754c04546cfc1653911a70a640b40"><code>a686883</code></a>
Bump protobufjs in the npm_and_yarn group across 1 directory</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/c6112d2d4f2881a12aaf89d9e2996ef6870eb6d0"><code>c6112d2</code></a>
Bump <code>@​protobufjs/utf8</code> in the npm_and_yarn group across 1
directory</li>
<li>Additional commits viewable in <a
href="https://github.com/js-cookie/js-cookie/compare/v3.0.5...v3.0.7">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for js-cookie since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=js-cookie&package-manager=npm_and_yarn&previous-version=3.0.5&new-version=3.0.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Weiko <corentin@twenty.com>
2026-05-28 21:25:35 +02:00
Félix MalfaitandGitHub 8a74ea8829 fix(contact-creation): enrich missing names on auto-created contacts (#21018)
## Summary

Three related fixes to the auto-creation of People records from calendar
events and email messages, all centred on the data-quality problem of
contacts being created with missing or malformed names.

### 1. Enrich names on existing contacts (commit 1)

Previously: when an email or calendar import matched an existing Person
by email, the existing record was left untouched — even if the new
source carried a better name.

This is the root cause of contacts like `"Félix"` (no last name)
sticking around forever: the `To:`/`Cc:` headers of outbound emails
rarely include a display name, and Google Calendar only returns
`displayName` for attendees already in the organizer's address book. So
the first sighting often creates a Person as `{firstName: "felix",
lastName: ""}`, and a later inbound `From: "Félix Malfait"
<felix@twenty.com>` — which would have produced the right name — gets
silently dropped because the Person already exists.

The new `computePeopleToEnrichNames` bucket and
`CreatePersonService.enrichPeopleNames` method fill in missing
`firstName`/`lastName` fields from the new parsed name, with
conservative rules:
- Only enrich when the existing Person's `createdBy.source` is
`CALENDAR` or `EMAIL` — `MANUAL`, `IMPORT`, `API`, `WORKFLOW`, etc. are
never touched.
- Only fill empty fields. Non-empty `firstName`/`lastName` are never
overwritten.
- Soft-deleted contacts continue to be handled by the existing restore
path.

### 2. Handle multi-comma "Last, First, Suffix" display names (commit 2)

The comma-inverted swap in the parser previously required *exactly* one
comma. Names like `"Smith, Jane, Jr."`, `"O'Brien, Mary, MD"` or `"Doe,
John, Patrick"` fell through to the space-split fallback, which stored
the comma in `firstName` (e.g. `"Smith,"`) and produced garbled records
(the avatar shows a single "B" and the name reads `"Barbey, Julien"`
because the entire string lives in `firstName`).

The regex now splits on the first comma and treats the remainder as the
first name, collapsing any further commas to spaces. Single-comma
behaviour is unchanged.

### 3. Perf: skip the parser when an existing record is already
populated (commit 3)

`computePeopleToEnrichNames` runs on every cron-driven email/calendar
import batch. The first version called the display-name parser for every
matched existing person, even when both `firstName` and `lastName` were
already set — i.e. the steady-state case after the initial enrichment
pass.

Reordered so the cheap "both fields populated" check short-circuits
before any parsing happens. Same behaviour, fewer parser calls on the
hot path.

## Test plan

- [x] 8 new unit tests for the enrichment bucket: empty `lastName`
enrichment, both `EMAIL` and `CALENDAR` sources, non-overwrite of
non-empty fields, skip on `MANUAL`/`IMPORT`, skip when the new source
also has no last name, skip for soft-deleted, fill `firstName` while
preserving `lastName`, handle null `name` field
- [x] 5 new parser tests for multi-comma forms: `"Last, First, Suffix"`,
credential suffixes (`MD`), three-token forms, whitespace around inner
commas, `:GROUP` tag interaction
- [x] 1 new parser test on the single-comma path covering multi-word
first names (`"Smith, Mary Jane"`)
- [x] All 15 existing parser tests still pass
- [x] All 116 tests in `contact-creation-manager` pass
- [x] `npx nx typecheck twenty-server`
- [x] `npx oxlint --type-aware` + `npx oxfmt --check` on changed files
- [ ] Manual: trigger a fresh contact creation from an outbound email
with no display name, then a subsequent inbound email from the same
address with a full display name, and confirm the Person's last name
gets populated
2026-05-28 20:49:56 +02:00
Félix MalfaitandGitHub f4ead89956 refactor(twenty-orm): migrate 23 grandfathered entities to WorkspaceScopedRepository (#20987)
## Summary

Follow-up to #20953. Migrates 23 of the 30 entities that were left in
`WORKSPACE_SCOPED_EXEMPTIONS` last time, so the lint rule's
workspaceId-enforcement default now covers most of the core/metadata
schema.

### Migrated (23 entities, 88 files, 22 commits)

| Family | Entities |
|---|---|
| Trivial caches | `NavigationMenuItem`, `Skill`, `DataSource`,
`Webhook`, `CommandMenuItem`, `IndexMetadata` |
| Views | `View`, `ViewField`, `ViewFieldGroup`, `ViewFilter`,
`ViewFilterGroup`, `ViewGroup`, `ViewSort` |
| Layouts | `PageLayout`, `PageLayoutTab`, `PageLayoutWidget` |
| Roles & permissions | `Role`, `RoleTarget`, `PermissionFlag`,
`ObjectPermission`, `FieldPermission`, `RowLevelPermissionPredicate`,
`RowLevelPermissionPredicateGroup` |

For each entity: swap `@InjectRepository(X)` →
`@InjectWorkspaceScopedRepository(X)` (and the field type →
`WorkspaceScopedRepository<X>`); rewrite every call site to pass
`workspaceId` as the first arg (stripped from `where`/criteria — the
wrapper throws if you include it now); register
`provideWorkspaceScopedRepository(X)` in every owning NestJS module;
update affected spec providers to
`getWorkspaceScopedRepositoryToken(X)`.

### Rule update

- `ApplicationRegistrationVariableEntity` was misclassified — moved to
`STRUCTURAL_EXEMPTIONS` (no `workspaceId` column; it's keyed on
`applicationRegistrationId` at the instance level).
- 22 of the 23 migrated entities removed from
`WORKSPACE_SCOPED_EXEMPTIONS` entirely (zero remaining raw
`@InjectRepository` sites).
- `RoleTargetEntity` also removed; one call site in
`user-workspace.service.ts` keeps a raw injection with an
`eslint-disable` + reason because `softRemove(...)` is not on the
wrapper API yet (the migration would require threading `workspaceId`
through `deleteUserWorkspace`'s three callers).

### Still exempted (7 entities, follow-up PRs)

| Entity | Why deferred |
|---|---|
| `ApplicationEntity` | ~50 sites with several cross-workspace lookups
by id (auth, OAuth, file-storage, cleanup) |
| `CalendarChannelEntity` / `MessageChannelEntity` | Use
`.increment(...)` (not on wrapper) and
`repository.manager.transaction(...)` — wrapper needs to grow
`.increment` + the transaction sites need `withManager` or dual-inject |
| `FieldMetadataEntity` / `ObjectMetadataEntity` | The metadata services
`extends TypeOrmQueryService<X>` and `super(rawRepo)` — requires
dual-inject or reworking the inheritance |
| `KeyValuePairEntity` | Allows `workspaceId: IsNull()` for
instance-level config; wrapper rejects null |
| `UpgradeMigrationEntity` | Same — instance-level + cross-workspace
ledger |

## Test plan

- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx nx lint twenty-server` — clean (0/0)
- [x] All 10 affected unit specs pass (115 tests) — api-key, agent-role,
permissions, workspace-roles-permissions-cache, view-filter-group,
workflow-version-step-operations, two-factor-authentication (service +
resolver), user-workspace, file
- [ ] Server integration tests in CI
2026-05-28 20:46:21 +02:00
Félix MalfaitandGitHub 865ca697ca Fix AI permission gating: use Ask AI for chat UI, AI Settings for admin endpoints (#21030)
## Summary

Closes #20662.

Two AI permission flags exist:
- **`AI`** (label "Ask AI") — user-facing: chat with AI agents, use AI
features
- **`AI_SETTINGS`** (label "AI") — admin: create and configure AI agents

After auditing every use of these flags I found:

### Frontend — chat UI gated by the admin permission (user-facing bug
from the issue)
A user granted only `Ask AI` could not see chat tabs, the "new chat"
button (desktop & mobile), or the chat content pane; thread
initialization was also skipped, leaving the chat in a half-initialized
state and producing intermittent `THREAD_NOT_FOUND` errors. Switched
these to `AI`:
- `MainNavigationDrawerTabsRow.tsx`
- `MainNavigationDrawer.tsx`
- `MobileNavigationBar.tsx`
- `AgentChatThreadInitializationEffect.tsx`

### Backend — admin-only resolvers gated by the user permission
(privilege escalation)
Two resolvers had a class-level guard of `AI`, letting any user with the
user-facing flag reach admin endpoints (skill CRUD, eval runs). Switched
the class-level guards to `AI_SETTINGS`:
- `SkillResolver` — create/update/delete/activate/deactivate skills
- `AgentTurnResolver` — read turns, run/grade evaluations

### Left as-is (already correct)
- `AgentResolver` — class-level `AI` for reads (workflow editors and
admin pages both need them), mutation-level `AI_SETTINGS` overrides for
writes
- `AgentChatResolver` & `AgentChatSubscriptionResolver` — already `AI`
- `AiGenerateTextController` — already `AI`
- Workspace AI config fields in `workspace.service.ts` — already
`AI_SETTINGS`

## Test plan

- [ ] As a user with `Ask AI` only (no `AI_SETTINGS`): chat tabs, "new
chat" button, and chat history pane are visible on desktop + mobile;
sending a message works; no `THREAD_NOT_FOUND` errors
- [ ] As a user with `AI_SETTINGS` but no `Ask AI`: chat UI is hidden
- [ ] As a user with `Ask AI` only: calling `skills` / `createSkill` /
`agentTurns` / `runEvaluationInput` via GraphQL returns permission
denied
- [ ] As an admin (`AI_SETTINGS`): skill settings and agent eval pages
still work
2026-05-28 20:21:58 +02:00
Paul RastoinandGitHub ebfaca5b3d EncryptedString PlaintextString branded string types (#21001)
## Summary

closes https://github.com/twentyhq/core-team-issues/issues/2464

Introduces compile-time branded types to distinguish encrypted
ciphertext from plaintext strings, preventing mix-ups like the one fixed
in #20819 — but at the type level rather in addition to the one existing
at runtime.

### Branded string primitives

- Created `EncryptedString` and `PlaintextString` as hard nominal brands
using `z.string().brand(...)`, making them non-assignable to each other
or to raw `string`
- Created `isEncryptedString` type predicate to narrow `string` to
`EncryptedString` based on the `enc:v2:` envelope prefix
- Retyped `SecretEncryptionService`: `encryptVersioned` accepts
`PlaintextString`, `decryptVersioned` returns `PlaintextString`

### Entity typing

- Typed encrypted columns across entities:
`SigningKeyEntity.privateKey`,
`TwoFactorAuthenticationMethodEntity.secret`,
`ApplicationRegistrationVariableEntity.encryptedValue`,
`ApplicationVariableEntity.value`
- Parameterized JSONB types for connected account connection parameters
(`ImapSmtpCaldavParams<Pwd>`) with reusable aliases
`EncryptedImapSmtpCaldavParams` / `DecryptedImapSmtpCaldavParams`
- Typed DTOs (`CreateApplicationRegistrationVariableInput`,
`UpdateApplicationRegistrationVariablePayload`,
`UpdateApplicationVariableEntityInput`) with `PlaintextString`

### ApplicationVariable always-encrypt uniformization

- Retyped `ApplicationVariableEntity.value` to `EncryptedString | ''` —
all values are now encrypted regardless of `isSecret`
- Updated `ApplicationVariableEntityService` to always encrypt on write
and always decrypt on read
- Simplified `UpdateApplicationVariableActionHandlerService` by removing
conditional encrypt/decrypt-on-isSecret-toggle logic
- Added slow instance command (`2.9.0`) to backfill-encrypt existing
`isSecret=false` plaintext rows and tighten the `CHECK` constraint

### ConfigStorageService refactor

- Split `convertAndSecureValue` (which used `any`) into two well-typed
methods: `convertAndDecrypt` and `convertAndEncrypt`
- Introduced `isSensitiveStringValue` type predicate to narrow values
before encryption/decryption

### What's next
- Typeorm entity derivation to strictly type sitemap configuration as
code + handler logic for encryption rotation
- https://github.com/twentyhq/core-team-issues/issues/2465
2026-05-28 17:41:16 +02:00
WeikoandGitHub 9b54200d8c Fix playwright CI (#21024)
## Context
The Install Playwright step ran npx playwright install with no
arguments, which downloads all browsers (Chromium + Firefox + WebKit +
ffmpeg, ~500MB+) on every run with no caching.

Fix:
- Install Chromium only — npx playwright install chromium instead of all
browsers.
- Cache the browser binaries — actions/cache on ~/.cache/ms-playwright,
keyed on the resolved Playwright version (v4-playwright-browsers-${{
runner.os }}-<version>). On a cache hit the install step is skipped
entirely; the cache invalidates automatically when the Playwright
version bumps.
2026-05-28 14:19:58 +00:00
bd6811d060 i18n - translations (#21022)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-28 15:59:42 +02:00
b32249877a i18n - translations (#21021)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-28 15:55:32 +02:00
martmullandGitHub 6fb6ef4e7a Add darkmode for oAuth screen (#21005)
## after

<img width="1512" height="828" alt="image"
src="https://github.com/user-attachments/assets/71664eab-c921-45da-ac67-7a660c976d5c"
/>
2026-05-28 15:48:57 +02:00
Raphaël BosiandGitHub bb4e28904f Support the "Me" filter for workspace members in dashboard widgets and add multi select (#20971)
Fixes https://github.com/twentyhq/twenty/issues/20225

The "Me" filter (current workspace member) worked in view filters but
not in dashboard widget filters — the server never resolved the
placeholder, and the widget side-panel UI had no "Me" option and only
allowed single selection.

Backend: `ChartDataQueryService` now forwards the current workspace
member id (from authContext) into filterValueDependencies, so the shared
filter logic resolves "Me" the same way it does for view filters. Added
unit tests for the converter.

Frontend: new multi-select picker for workspace member filters in the
widget side panel, mirroring the view filter's actor select: search
input, "Me" pinned item, and a multi-select workspace member list.

## Before
<img width="3024" height="1488" alt="CleanShot 2026-05-27 at 17 16
36@2x"
src="https://github.com/user-attachments/assets/b2cff46c-53e5-4e8a-a463-b106daf96c8c"
/>

## After
<img width="3024" height="1488" alt="CleanShot 2026-05-27 at 17 14
05@2x"
src="https://github.com/user-attachments/assets/8b3b5f11-44b9-4ae5-a2f3-9c7a689f4bb2"
/>
2026-05-28 15:43:47 +02:00
c3d1af89ae i18n - docs translations (#21019)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-28 15:43:18 +02:00
Raphaël BosiandGitHub e6221c5f0e Fix twenty sdk billing exports (#21016)
Fix two omissions from #19973 that prevented `twenty-sdk/billing` from
being a fully exported subpath:

- `package.json`: add `billing` to `typesVersions` (every other subpath
was listed; billing was the only one missing, breaking type resolution
for consumers using classic TS moduleResolution).
- `project.json`: add the billing vite build and `dist/billing` output
to the `build:sdk` target
2026-05-28 15:41:24 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6566a918af chore(deps): bump @apollo/client from 4.1.6 to 4.2.0 (#20993)
Bumps [@apollo/client](https://github.com/apollographql/apollo-client)
from 4.1.6 to 4.2.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/apollographql/apollo-client/releases">@​apollo/client's
releases</a>.</em></p>
<blockquote>
<h2><code>@​apollo/client</code><a
href="https://github.com/4"><code>@​4</code></a>.2.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/apollographql/apollo-client/pull/13132">#13132</a>
<a
href="https://github.com/apollographql/apollo-client/commit/f3ce805425d10a9666218a8e109288a2d46dcab1"><code>f3ce805</code></a>
Thanks <a
href="https://github.com/phryneas"><code>@​phryneas</code></a>! -
Introduce &quot;classic&quot; and &quot;modern&quot; method and hook
signatures.</p>
<p>Apollo Client 4.2 introduces two signature styles for methods and
hooks. All signatures previously present are now &quot;classic&quot;
signatures, and a new set of &quot;modern&quot; signatures are added
alongside them.</p>
<p><strong>Classic signatures</strong> are the default and are identical
to the signatures before Apollo Client 4.2, preserving backward
compatibility. Classic signatures still work with manually specified
TypeScript generics (e.g.,
<code>useSuspenseQuery&lt;MyData&gt;(...)</code>). However, manually
specifying generics has been discouraged for a long time—instead, we
recommend using <code>TypedDocumentNode</code> to automatically infer
types, which provides more accurate results without any manual
annotations.</p>
<p><strong>Modern signatures</strong> automatically incorporate your
declared <code>defaultOptions</code> into return types, providing more
accurate types. Modern signatures infer types from the document node and
do not support manually passing generic type arguments; TypeScript will
produce a type error if you attempt to do so.</p>
<p>Methods and hooks automatically switch to modern signatures the
moment any non-optional property is declared in
<code>DeclareDefaultOptions</code>. The switch happens across all
methods and hooks globally:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  namespace ApolloClient {
    namespace DeclareDefaultOptions {
      interface WatchQuery {
errorPolicy: &quot;all&quot;; // non-optional → modern signatures
activated automatically
      }
    }
  }
}
</code></pre>
<p>Users can also manually switch to modern signatures without declaring
any <code>defaultOptions</code>, for example when wanting accurate type
inference without relying on global <code>defaultOptions</code>:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  export interface TypeOverrides {
    signatureStyle: &quot;modern&quot;;
  }
}
</code></pre>
<p>Users can do a global <code>DeclareDefaultOptions</code> type
augmentation and then manually switch back to &quot;classic&quot; for
migration purposes:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  export interface TypeOverrides {
    signatureStyle: &quot;classic&quot;;
  }
}
</code></pre>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md">@​apollo/client's
changelog</a>.</em></p>
<blockquote>
<h2>4.2.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/apollographql/apollo-client/pull/13132">#13132</a>
<a
href="https://github.com/apollographql/apollo-client/commit/f3ce805425d10a9666218a8e109288a2d46dcab1"><code>f3ce805</code></a>
Thanks <a
href="https://github.com/phryneas"><code>@​phryneas</code></a>! -
Introduce &quot;classic&quot; and &quot;modern&quot; method and hook
signatures.</p>
<p>Apollo Client 4.2 introduces two signature styles for methods and
hooks. All signatures previously present are now &quot;classic&quot;
signatures, and a new set of &quot;modern&quot; signatures are added
alongside them.</p>
<p><strong>Classic signatures</strong> are the default and are identical
to the signatures before Apollo Client 4.2, preserving backward
compatibility. Classic signatures still work with manually specified
TypeScript generics (e.g.,
<code>useSuspenseQuery&lt;MyData&gt;(...)</code>). However, manually
specifying generics has been discouraged for a long time—instead, we
recommend using <code>TypedDocumentNode</code> to automatically infer
types, which provides more accurate results without any manual
annotations.</p>
<p><strong>Modern signatures</strong> automatically incorporate your
declared <code>defaultOptions</code> into return types, providing more
accurate types. Modern signatures infer types from the document node and
do not support manually passing generic type arguments; TypeScript will
produce a type error if you attempt to do so.</p>
<p>Methods and hooks automatically switch to modern signatures the
moment any non-optional property is declared in
<code>DeclareDefaultOptions</code>. The switch happens across all
methods and hooks globally:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  namespace ApolloClient {
    namespace DeclareDefaultOptions {
      interface WatchQuery {
errorPolicy: &quot;all&quot;; // non-optional → modern signatures
activated automatically
      }
    }
  }
}
</code></pre>
<p>Users can also manually switch to modern signatures without declaring
any <code>defaultOptions</code>, for example when wanting accurate type
inference without relying on global <code>defaultOptions</code>:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  export interface TypeOverrides {
    signatureStyle: &quot;modern&quot;;
  }
}
</code></pre>
<p>Users can do a global <code>DeclareDefaultOptions</code> type
augmentation and then manually switch back to &quot;classic&quot; for
migration purposes:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  export interface TypeOverrides {
    signatureStyle: &quot;classic&quot;;
  }
}
</code></pre>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apollographql/apollo-client/commit/e010bdd239b5c10415d4b70ca791467cde12fc88"><code>e010bdd</code></a>
Version Packages (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13241">#13241</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/9c4c01a640b43bfb47bd52b25d5881c4ad7bec71"><code>9c4c01a</code></a>
Release 4.2 (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13129">#13129</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/222838e99bc6054120cc1f881bb225b1ef049de9"><code>222838e</code></a>
Exit prerelease mode</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/7d3a533c811a8423536ceeebccc06413ded5b6a3"><code>7d3a533</code></a>
Merge branch 'main' into release-4.2</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/f20d591bbf74cb4f0d87ec9a14b93a59fe46b039"><code>f20d591</code></a>
chore(deps): update actions/create-github-app-token digest to d72941d
(<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13239">#13239</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/d4a28b6142e47164c8a24bd8c05a8aa3f1ce4eee"><code>d4a28b6</code></a>
chore(deps): pin dependencies (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13237">#13237</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/c1f39cf5402b052ab92886a1857840a745aee02b"><code>c1f39cf</code></a>
ci: pin Actions@SHA and disable cache on workflows with elevated OIDC
permiss...</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/511048b7bd6253a38a6b7ebe58e9674a39c74273"><code>511048b</code></a>
Event-based refetching docs (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13228">#13228</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/d1f68f1a5fdb7c6915a72b2426cad373a0526c06"><code>d1f68f1</code></a>
Version Packages (rc) (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13234">#13234</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/f1b541fed4111028b6842727178288156582e669"><code>f1b541f</code></a>
Prepare for rc release (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13232">#13232</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/apollographql/apollo-client/compare/@apollo/client@4.1.6...@apollo/client@4.2.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@apollo/client&package-manager=npm_and_yarn&previous-version=4.1.6&new-version=4.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-28 15:12:52 +02:00
martmullandGitHub 6ea637d6c5 Export STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS (#21010)
fix https://discord.com/channels/1130383047699738754/1509086323464474705
2026-05-28 12:51:01 +00:00
Félix MalfaitandGitHub 531410f64a fix(ai): expose MORPH_RELATION join columns in AI/MCP tool schemas (#21012)
## Summary

- Fixes a bug where `noteTarget` (and any other morph-relation join
object) created via AI/MCP would land with `targetCompanyId` /
`targetPersonId` / `targetOpportunityId` left null, even though the tool
reported success.
- Root cause: the Zod schema generators for the AI tools only branched
on `FieldMetadataType.RELATION`. MORPH_RELATION fields fell through to
the default case — for `create_*` they were exposed as `targetCompany:
string` instead of `targetCompanyId: uuid`, and for `group_by_*` they
were silently skipped entirely. Downstream
(`data-arg-processor.service.ts` and the group-by arg processor) already
accept the join-column form for both kinds of relations via
`computeMorphOrRelationFieldJoinColumnName` and
`isMorphOrRelationFlatFieldMetadata`, so the fix is purely in the schema
generators.

## Changes

- `record-properties.zod-schema.ts` — extend the existing RELATION
MANY_TO_ONE / ONE_TO_MANY branches to also match MORPH_RELATION.
- `group-by-tool.zod-schema.ts` — replace the silent MORPH_RELATION skip
with the same treatment as RELATION MANY_TO_ONE (exposes `${name}Id` as
a groupBy option).
- `test/integration/ai/suites/mcp-tool-execution.integration-spec.ts` —
new file. First integration test for tool execution end-to-end. Drives
the real MCP JSON-RPC endpoint with the seeded API key (`learn_tools`
for schema introspection, `execute_tool` for invocation):
- asserts `create_note_target`'s schema exposes `targetCompanyId` /
`targetPersonId` / `targetOpportunityId` as UUIDs and does **not**
expose `targetCompany` / `targetPerson` / `targetOpportunity`.
- creates a company + note + noteTarget via MCP, then queries the
workspace schema to confirm `targetCompanyId` is actually persisted in
the FK column.
- asserts `group_by_note_targets` schema accepts `targetCompanyId` as a
groupBy key.
- sets up 3 noteTargets (2 → company A, 1 → company B), calls
`group_by_note_targets` by `targetCompanyId`, and asserts the counts.

Out of scope: `record-filter.zod-schema.ts` has the same pattern (only
RELATION) — left for a follow-up so this PR stays focused on what was
reported.

## Test plan

- [x] `npx nx typecheck twenty-server`
- [x] `npx oxlint --type-aware` on changed files — clean
- [x] `npx oxfmt --check` on changed files — clean
- [x] Integration tests pass (4/4) after `database:reset`:
- `should expose the morph-relation join columns as \`${name}Id\` UUID
parameters`
- `should persist targetCompanyId when create_note_target is invoked via
MCP`
  - `should expose targetCompanyId as a valid groupBy option`
  - `should group noteTargets by targetCompanyId via MCP`
2026-05-28 14:17:10 +02:00
182960051f i18n - translations (#21013)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-28 13:30:39 +02:00
Félix MalfaitandGitHub 7d9f9605a2 feat(settings): move email handles and emailing domains to dedicated Email page (#21008)
## Summary

Both **Email Handles** and **Emailing Domains** were rendered on the
General workspace settings page, but they're workspace-level *email
infrastructure* (inbound shared addresses + outbound sender
authentication) and don't belong with the workspace name, picture, and
domain config.

- New `SettingsWorkspaceEmail` page at `/settings/email`
- Nav item under **Workspace**, hidden when `IS_EMAIL_GROUP_ENABLED` is
off (and gated by `WORKSPACE` permission)
- Related sub-routes (`email-group/:messageChannelId`,
`emailing-domain/:domainId`, etc.) moved from `general/` to `email/` so
the URL space stays consistent with the page
- General page now only contains name, picture, workspace domain, and
the delete-workspace section

No behavior changes to the underlying section components — they're
imported as-is into the new page.

## Test plan

- [ ] With `IS_EMAIL_GROUP_ENABLED` enabled: **Email** appears in the
Workspace nav and the page renders both sections
- [ ] With the flag disabled: **Email** is hidden from nav; navigating
to `/settings/email` directly renders nothing
- [ ] General page no longer shows Email Handles / Emailing Domains
- [ ] Clicking a shared inbox row navigates to
`/settings/email/email-group/:id` (was `general/...`)
- [ ] "Add emailing domain" navigates to
`/settings/email/emailing-domain/new`

## Notes

- Pre-existing `twenty-front` typecheck error in
`FrontComponentRendererProvider.tsx` (React types mismatch between
sibling packages) reproduces on `main` and is unrelated to this PR.
2026-05-28 13:22:55 +02:00
Félix MalfaitandGitHub 7065441972 fix: refresh admin panel feature flags after toggling (#21007)
## Summary

- Add `refetchQueries` to the `updateWorkspaceFeatureFlag` mutation in
the admin workspace detail page so the toggle reflects the new value
after toggling.
- Rename `useFeatureFlagState` → `useAdminUpdateFeatureFlag` since the
hook lives under `admin-panel` and is only consumed by the admin
workspace detail page.

## Bug

In the admin panel, toggling a feature flag for a workspace other than
the admin's own workspace sent the backend mutation successfully, but
the toggle in the UI remained unchanged.

The displayed value is derived from:
```tsx
const currentWorkspaceValue =
  currentWorkspace?.id === workspaceId
    ? currentWorkspace?.featureFlags?.find((f) => f.key === flag.key)?.value
    : undefined;
const displayedValue = currentWorkspaceValue ?? flag.value;
```

When viewing a different workspace, `currentWorkspaceValue` is
`undefined` so the toggle reads `flag.value` from the
`WORKSPACE_LOOKUP_ADMIN_PANEL` query. That query was never refetched
after the mutation, so the displayed value stayed stale.

The existing optimistic Jotai update on `currentWorkspaceState` still
runs — it is needed so the rest of the app (anything consuming
`useIsFeatureEnabled`) reacts immediately when an admin toggles a flag
on their own workspace.

## Test plan

- [ ] Open the admin panel → pick a workspace that is not your own →
Feature Flags tab → toggle a flag → toggle visually flips after the
mutation completes.
- [ ] Same flow on your own workspace → toggle flips, and any UI gated
on that flag also reacts.
- [ ] If the mutation fails, the toggle reverts (existing `onError`
rollback path).
2026-05-28 12:23:26 +02:00
6e2cf4cbc0 Fix lambda timeout diagnostics (#21002)
Trying to fix
https://twenty-v7.sentry.io/issues/7420384466/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20%21issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=issue-stream&sort=date

## Summary
- Prevent invoking Lambda functions stuck in `Pending` state by checking
`Configuration.State === 'Active'` in `checkLambdaExecutorBuildStatus` —
a Pending function now goes through `ensureLambdaExecutor` which waits
for Active.
- Track execution phase (`build`/`fetch-code`/`invoke`) via a
`LambdaExecutionPhase` enum and include phase timing + function state in
all error messages for faster debugging.

Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
2026-05-28 09:06:33 +00:00
de7daaa81a fix: exclude system objects and workflow/dashboard from AI/MCP write tool descriptors (#20973)
## Summary

fix: exclude system join objects from AI/MCP create/update/delete tool
descriptors

Closes #20403

---
AI was used for assistance.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-05-27 20:01:11 +02:00
c0cbe67bcd i18n - translations (#20982)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 19:47:07 +02:00
neo773GitHubFélix Malfaitclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Félix Malfait
c5606212f2 Ses outbound followup (#20610)
This pull request unifies outbound with inbound under the new feature
and the new email groups feature.

These are workspace level shared inboxes that are shared between all
workspace members.

outbound sending with SES works, we only listen for tenant status
events, rest is managed by AWS

PR refactors old code and webhook to be split for outbound and inbound
for proper separation


| Area | Change |
|---|---|
| AWS SES driver | Split into `AwsSesRegisterDomainService` (tenant +
identity + DKIM + MAIL FROM + configuration-set + EventBridge dest +
contact list) and `AwsSesSendEmailService` (SendEmail). |
| Reputation webhook | New `/webhooks/messaging/ses/outbound` route. SES
→ EventBridge (`Sending Status Enabled/Disabled` on default bus) → SNS →
router → `SesOutboundSendingStateHandlerService` updates
`emailing_domain.tenantStatus`. |
| Inbound webhook | Refactored into `SesInboundWebhookRouterService` +
`SesInboundMailHandlerService`. Shared `SnsSignatureVerifierService` +
`SnsSubscriptionConfirmerService` across both routes. |
| Global uniqueness | New migration + instance command:
`emailing_domain.domain` is now globally unique (one tenant per domain
across workspaces). |
| Tenant status | New `emailing_domain.tenantStatus` column (`ACTIVE` /
`PAUSED`) + `EmailingDomainTenantStatusService`. |
| Send-email mutation | New `sendEmailViaDomain` GraphQL mutation +
DTOs. |
| Cleanup | `EmailingDomainWorkspaceCleanupJob` wired into
`WorkspaceService.deleteWorkspace` — tears down SES tenant association +
identity on workspace delete. |
| Settings UI | Rewritten around reusable `SettingsTableListSection`.
"Email Group" → "Email Handle" rename. New cells for
status/source/forwarding. Outbound domains surfaced on workspace
settings page. |

### Env vars (new)

All in `config-variables.ts`, group `AWS_SES_SETTINGS`, all optional:

- `AWS_SES_REGION` — `@IsAWSRegion`, consumed by `AwsSesClientProvider`
+ driver factory
- `AWS_SES_ACCOUNT_ID` — used for ARN construction in driver factory
- `SES_SNS_TOPIC_ARN_ALLOWLIST` — **shared** by inbound + outbound
webhook routers, comma-separated list of accepted SNS topic ARNs
(verified via `sns-payload-validator`)

### Migrations

- `1778862608620-add-emailing-domain-tenant-status` (fast) — adds
`tenantStatus` column.
- `1778865501791-unique-emailing-domain-globally` (slow, idempotent) —
enforces global uniqueness on `domain`.
- Instance commands bumped to `2.5`.

### Infra dependency

Two coupled twenty-infra PRs:

- `ses-inbound-email` — receipt-rule + inbound SNS topic + S3 bucket
policy + KMS grant + `email_group_*` outputs.
- `ses-outbound-tf` — EventBridge rule + outbound SNS topic + SES IAM
policy + outbound `webhook_url` subscription. **Based on
`ses-inbound-email`.**

Merge order: inbound first, then outbound. Outbound PR's chart edit owns
the comma-joined `SES_SNS_TOPIC_ARN_ALLOWLIST` value (both ARNs).


Features lives under `/settings/general`

<img width="1496" height="845" alt="SCR-20260519-ofhi-2"
src="https://github.com/user-attachments/assets/a025485a-09f7-4131-91cd-0067690ff18d"
/>

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
2026-05-27 19:38:44 +02:00
0702e72e3f i18n - translations (#20979)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 19:18:37 +02:00
martmullandGitHub 0503aa7982 Display application info in workflow side panel (#20976)
To comply this design
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=99421-167676&t=GXw0bOgp2P51YVKi-0


## Before

<img width="569" height="470" alt="image"
src="https://github.com/user-attachments/assets/c52c8775-2934-4620-8885-dff5a8230a89"
/>


## After

<img width="314" height="258" alt="image"
src="https://github.com/user-attachments/assets/df9cbd15-cf7b-4a47-ac8d-1292c71a7c94"
/>
2026-05-27 16:57:48 +00:00
f98c5d0609 i18n - translations (#20978)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 19:01:41 +02:00
Félix MalfaitandGitHub 4797d2f270 feat(twenty-orm): introduce WorkspaceScopedRepository for core/metadata workspace-scoped entities (#20953)
## Summary

Adds a third tenancy enforcement layer for entities that live in shared
schemas (`core`, `metadata`) and carry a `workspaceId` column —
previously the only safeguard at this layer was developer discipline
(remembering to put `workspaceId` in every WHERE clause).

### The three layers, after this PR

| Layer | Scope | How it's enforced |
|---|---|---|
| 1. Workspace data | per-workspace schema (companies, people, custom
objects) | `twentyORMManager.getRepository(workspace, E)` — physical
isolation (own data source) |
| 2. Metadata | shared `metadata` schema (objectMetadata, fieldMetadata,
views, roles…) | Flat-entity-maps cache — workspace-scoped in-memory
map, lookups by id within it |
| 3. Core (new) | shared `core` schema (agent threads/turns/messages,
app tokens, etc.) | `WorkspaceScopedRepository<T>` — `workspaceId` is a
required positional argument on every read/write |

## What's in the PR

### The wrapper
(`packages/twenty-server/src/engine/twenty-orm/workspace-scoped-repository/`)
- `WorkspaceScopedRepository<T extends WorkspaceScopedEntity>` — wraps a
TypeORM `Repository<T>`, requires `workspaceId` on every
`find`/`findOne`/`findOneOrFail`/`update`/`delete`/`softDelete`/`insert`/`save`/`count`
call, merging it into the WHERE or stamping it on the entity.
`createQueryBuilder` is an explicit escape hatch (caller scopes
manually).
- Provided via Nest DI with
`@InjectWorkspaceScopedRepository(EntityClass)` and the
`provideWorkspaceScopedRepository(EntityClass)` provider factory.
- 19 unit tests cover the merge behavior, override-on-conflict, and the
array-where (OR) case.

### Lint enforcement
(`packages/twenty-oxlint-rules/rules/prefer-workspace-scoped-repository.ts`)
- New `twenty/prefer-workspace-scoped-repository` rule (level:
**error**).
- Blacklist of entity names: raw `@InjectRepository(E)` is rejected if
`E` is on the list.
- Initial list: `AgentTurnEntity`, `AgentMessageEntity`,
`AgentMessagePartEntity`, `AgentChatThreadEntity`,
`AgentTurnEvaluationEntity`, `AgentEntity`.
- Designed to grow over time as more consumers are migrated.
- 5 rule tests.

### Migration in this PR
All consumers of the six blacklisted entities, including:
- AI agent / chat / monitor resolvers, services, and jobs
- `AgentService`, `AiAgentRoleService`, `AiAgentWorkflowAction`,
`ApplicationService`, `WorkspaceFlatAgentMapCacheService`
- Admin-panel chat (migrated where the lookup is workspace-known; one
documented `eslint-disable` on the threadId-discovery lookup that
necessarily precedes the `allowImpersonation` permission check)
- `AiAgentRoleService` unit spec updated to mock the scoped wrapper

## Future work (deliberately not in this PR)

A standalone audit identified ~14 additional `core`/`metadata` entities
with `workspaceId` that currently use raw `@InjectRepository` and could
be added to the blacklist. Notable candidates: `UserWorkspaceEntity` (42
sites), `AppTokenEntity` (10), `FileEntity` (7),
`BillingCustomerEntity`/`BillingSubscriptionEntity` (~22 combined). Each
should be its own PR — the migration is mechanical but the surface is
wide.

## Test plan
- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx nx lint twenty-server` — 0 warnings, 0 errors
- [x] `npx jest workspace-scoped-repository` — 19/19 pass
- [x] `npx nx test twenty-oxlint-rules` — 215/215 pass
- [x] `npx jest src/engine/metadata-modules/ai` — 44/44 pass
- [ ] Manual smoke: end-to-end AI agent chat send/receive (reviewer)
- [ ] Manual smoke: AI agent monitor — list turns, run evaluation
(reviewer)
- [ ] Manual smoke: admin-panel chat thread inspection (reviewer)
2026-05-27 18:52:53 +02:00
Raphaël BosiandGitHub c8b9dace72 Fix focus in front components inputs (#20961)
Fixes https://github.com/twentyhq/twenty/issues/20714

Fixes keyboard hotkey conflicts when typing inside `<input>` /
`<textarea>` elements rendered by Front Components. Editable fields
rendered through the component renderer now properly push/pop a focus
item onto Twenty's focus stack, disabling global keyboard hotkeys while
the user is typing.

## Before


https://github.com/user-attachments/assets/2003c2cb-2698-480f-aedf-bb2f30396572


## After


https://github.com/user-attachments/assets/2c7c6cb0-ecd7-4557-a77b-4d1f264345f0
2026-05-27 16:30:49 +00:00
martmullandGitHub fbae66de8a Fix error when token invalid (#20972)
## Before

<img width="914" height="519" alt="image"
src="https://github.com/user-attachments/assets/8933bf9e-d8db-4670-ad08-69e900083fc1"
/>

## After

<img width="1105" height="523" alt="image"
src="https://github.com/user-attachments/assets/d8638dbb-adee-4b7d-9d29-1a2a0185ab4f"
/>
2026-05-27 16:30:14 +00:00
46e7f23df1 fix(contact-creation): handle common email display-name shapes when auto-creating People (#20639)
## Summary

When messages are imported, Twenty auto-creates a Person record for any
recipient that doesn't exist yet. The display-name parser used at that
point is `displayName.split(' ')[0] / [1]`, which silently mangles
several common header shapes:

| Header | Old result |

|-------------------------------------------------|-----------------------------------------|
| `"Doe, John" <...>` | `firstName="Doe,"`, `lastName="John"` |
| `"John.Doe Doe" <...>` | `firstName="John.Doe"`, `lastName="Doe"`|
| `"Mary Jane Watson" <...>` | `lastName="Jane"` ("Watson" dropped) |
| `"john.doe@x.com" <john.doe@x.com>` (forwarder) | full address in
`firstName` |
| `"Doe, John:GROUP" <...>` (group-tag servers) |
`firstName="John:GROUP"` |

This PR rewrites `getFirstNameAndLastNameFromHandleAndDisplayName` to
handle each pattern. Behaviour in order:

1. Trim + strip wrapping quotes
2. Swap `"Last, First"` comma form
3. Fall back to handle parsing when display name contains `@` (real
names don't)
4. Split single dotted tokens (`"john.doe"` → `"John"`, `"Doe"`)
5. Preserve multi-word last names (`tokens.slice(1).join(' ')`)
6. De-synthesize dot-glued first names (`"John.Doe Doe"` → `"John"`,
`"Doe"`)
7. Strip `:XXX` trailing tag suffix from each parsed field

## Test plan

- [x] 16 new unit test cases covering each shape
(`__tests__/get-first-name-and-last-name-from-handle-and-display-name.util.spec.ts`)
- [x] Lint + typecheck clean
- [ ] No regression in the messaging import flow

---------

Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: neo773 <neo773@protonmail.com>
2026-05-27 15:49:46 +00:00
6f3541fd7c i18n - translations (#20975)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 17:47:07 +02:00
nitinandGitHub 863f3f29a2 Fix page layout widget tab moves (#20915)
Fixes widget moves between page layout tabs by making pageLayoutTabId
part of the flat-entity diff, so the save mutation no longer silently
drops the new tab assignment.

https://discord.com/channels/1130383047699738754/1508737039128985680
2026-05-27 15:29:12 +00:00
martmullandGitHub 9264480824 Display which remote is used when twenty-sdk runs command (#20969)
## After
<img width="669" height="186" alt="image"
src="https://github.com/user-attachments/assets/c6ce25c9-35a9-4b2f-a473-59803b8931cc"
/>
2026-05-27 15:23:17 +00:00
nitinandGitHub 5ff8c3b219 revert(navigation-drawer): unwanted desktop design changes from #20634 (#20955)
Follow-up to #20634. Removes three desktop design changes that bled in
unintentionally:

- `font-weight: regular` → restored to `medium` on nav item labels
- `MenuItemIconBoxContainer` wrap around bare icons → removed
- Section title `padding-right/top` tweak → restored to original values

Mobile-specific fixes from #20634 (slide-over drawer width, min-width
overflow fixes, breadcrumb cleanup, etc.) are preserved.
2026-05-27 15:06:45 +00:00
Thomas TrompetteandGitHub ff28547a36 tt-fix-logic-function-error-handling (#20928)
## Summary
- **Fix `callWithTimeout` timer leak**: the `setTimeout` was never
cleared when the callback resolved first, leaving orphaned timers (up to
15 minutes) in the Node.js event loop. Now uses `try/finally` with
`clearTimeout`.
- **Properly classify timeout errors**: introduced
`ExecutionTimedOutError` so the Lambda driver can throw
`LOGIC_FUNCTION_EXECUTION_TIMEOUT` instead of
`LOGIC_FUNCTION_EXECUTION_FAILED` — users now see "Function execution
timed out" instead of the generic "Function execution failed."

## Test plan
- [x] Execute a logic function normally and verify it still works
- [x] Trigger a logic function timeout and verify the error message says
"Function execution timed out" (not "Function execution failed")
- [x] Verify that a deleted logic function invocation logs a specific
"was deleted" warning
- [x] Verify no orphaned timers remain after logic function execution
completes
2026-05-27 14:32:12 +00:00
Félix MalfaitandGitHub dde6df7a26 refactor(website): replace axios with native fetch (#20967)
## Summary

Follow-up to #20966 (Stripe fetch client). axios's default Node http
adapter on workerd has the same TLS hang the Stripe SDK had — it just
doesn't surface today because all three call sites are wrapped in
\`unstable_cache(revalidate: 3600)\` and the cache is populated at build
time, so misses are rare and the failure mode is a silent \`null\` to
the layout.

This swaps the three remaining axios calls in \`twenty-website\` for
native \`fetch\` and removes axios from
\`packages/twenty-website/package.json\`. The package is still used by
other workspaces, so yarn.lock keeps the other resolution.

Touched call sites:
- \`src/lib/releases/fetch-latest-release-tag.ts\` (GitHub releases —
runs at build time, cosmetic)
- \`src/lib/community/fetch-github-star-count.ts\` (GitHub star count in
menu)
- \`src/lib/community/fetch-discord-member-count.ts\` (Discord member
count in menu)

## Test plan

- [ ] After merge + deploy: confirm GitHub star + Discord member counts
render in the site menu (non-zero, formatted)
- [ ] Confirm \`/releases\` shows the latest tag-gated visible release
notes
- [ ] No \`axios\` in worker bundle (\`grep axios .open-next/worker.js\`
should be empty)
2026-05-27 16:31:39 +02:00
DeviSriSaiCharanandGitHub 92b0e07617 fix: separate initial timeline loading from fetchMore loading state (#20896)
Fixes: #20742 

# Issue:
In the timeline activity inside the side panel, when we scroll down it
fetches more data and it displays a skeleton and after fetching finishes
the scroll position always jumps back to the top. Because of this, we
have to scroll to the bottom again to load more data.



https://github.com/user-attachments/assets/40d99df7-bdfb-4351-bc4f-baec2a035f13

## Root Cause
`useTimelineActivities` exposed a single `loading` state from
`useFindManyRecords`, which became true for both:
- Initial timeline fetch
- Pagination / fetchMore requests

`loadingTimelineActivities` becomes true whenever a network request is
triggered, including pagination requests where timeline records are
already available.

Because of this, the UI could not distinguish between the first query
loading state and subsequent fetchMore loading states.

## Fix
Added a separate firstQueryLoading state to detect only the first
timeline request.

The first query is identified by checking:

- the request is still loading
- and no timeline activities have been loaded yet

Once activities are already available, any future loading state is
treated as pagination/loading more records instead of initial loading.

This allows the UI to correctly handle:

- Skeleton loaders for first load
- Infinite scroll loaders for pagination
- Empty states after loading finishes



https://github.com/user-attachments/assets/48e8e078-82e2-43d8-823f-2f71e4f4f6e1
2026-05-27 14:20:59 +00:00
951ead5a1e fix(front): ignore IME composition Enter in input hotkeys (#20958)
## What

Pressing Enter to confirm an IME (CJK) composition no longer submits the
input. The Enter / Escape / Tab handlers now ignore key events fired
while a composition is in progress (`isComposing`, or the legacy
`keyCode === 229`).

Fixes #20954

## Why

`isComposing` was not checked anywhere in `twenty-front`, so the Enter
that confirms a Japanese / Chinese / Korean conversion was also consumed
as a submit / escape / tab hotkey — making it very hard to type CJK text
into any input that submits on Enter.

## Changes

- `useHotkeysOnFocusedElement` — central guard; covers every input wired
through `useRegisterInputEvents` (~13 components) and all hotkeys routed
through this hook.
- Direct `onKeyDown` Enter handlers: `CreateWorkspace`,
`SettingsDevelopersApiKeysNew`, `SettingsAccountsBlocklistInput`.

## Notes

- No effect on non-IME (Latin) typing — `isComposing` is only true
during an active composition. It also improves accented / dead-key input
on Latin layouts.
- `react-hotkeys-hook@4` does not handle IME composition on its own, so
the guard is explicit.

## Testing

Manually verified with a Japanese IME on Chrome (macOS) against the
v2.8.3 self-hosted image: romaji + Enter now only confirms the
conversion; a second Enter on committed text submits as expected. The
GIF in #20954 shows the original buggy behavior.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:09:10 +00:00
Félix MalfaitandGitHub e79f43813b fix(website): use Stripe fetch client on Cloudflare Workers (#20966)
## Summary

Every `/api/enterprise/*` route on `twenty-website-prod` is currently
timing out at exactly 80s with `Request aborted due to timeout being
reached (80000ms)` — that's `Stripe.DEFAULT_TIMEOUT` aborting itself,
not Cloudflare.

The Stripe SDK's default transport uses Node's `http`/`https` module.
Under workerd, even with `nodejs_compat`, the outbound TLS connection to
`api.stripe.com` hangs and the SDK eventually times out at its built-in
80s ceiling. Worked on EKS (real Node), breaks on Cloudflare Workers.

Fix is one line: pass `httpClient: Stripe.createFetchHttpClient()` so
the SDK uses workerd's native `fetch` instead of the polyfilled Node
transport.

## Blast radius

`getStripeClient()` is shared across every enterprise route. All of
these are silently broken on prod right now:

- `POST /api/enterprise/checkout` (confirmed in logs)
- `POST /api/enterprise/portal`
- `POST /api/enterprise/seats`
- `GET  /api/enterprise/status`
- `POST /api/enterprise/activate`
- `POST /api/enterprise/validate`

Single change in `stripe-client.ts` unblocks all of them.

## Test plan

- [ ] After merge + deploy to prod: `wrangler tail twenty-website-prod`
while hitting the enterprise checkout flow; same call should complete in
<2s instead of 80s
- [ ] Verify a real Stripe checkout session is created (Stripe dashboard
→ Payments → Checkout sessions)
- [ ] Spot-check `/api/enterprise/status` and `/api/enterprise/portal`
are no longer timing out
2026-05-27 16:03:48 +02:00
73ab5de46c i18n - translations (#20963)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 14:59:02 +02:00
MarieandGitHub 986b9dcb3d Deprecate dummy enterprise key 1/2 (#20890)
Remove usage of hasValidEnterpriseKey in FE (replaced by
hasValidSignedEnterpriseKey)

To avoid breaking change at deploy time, we will wait until after this
has been deployed in prod, to remove hasValidEnterpriseKey in the BE.
2026-05-27 12:34:48 +00:00
ae7db23bdd i18n - translations (#20959)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 14:33:57 +02:00
WeikoandGitHub 2f358a1775 Add a Table display mode to relation field widgets (#20929)
## Context

Adds a new Table layout to the FIELD widget for to-many relation fields.
On a record page, a relation can now be displayed as a full record table
(the same component used for record indexes and dashboard table widgets)
scoped to the records related to the current record.



https://github.com/user-attachments/assets/320b24dc-f019-4d0e-bc71-3e64d032d75a



https://github.com/user-attachments/assets/2f6d4f8e-de26-4fc1-ae12-c9b9c19654dc



https://github.com/user-attachments/assets/3fb6d512-f83c-4818-823e-46ad2644fbc2
2026-05-27 12:13:58 +00:00
EtienneandGitHub fc3004b2f6 fix(website) - fix getPartners (#20947)
**Context**
Fix empty marketplace at /partners/list: page was being statically
prerendered with getPartners() returning [] (build-time fetch failure),
then served from OpenNext's R2 cache forever — so the partners API was
never actually called in production.

**Change**
Wrap getPartners in unstable_cache with revalidate: 300, matching the
existing fetchGithubStarCount / fetchDiscordMemberCount ISR pattern.
After deploy, the cached empty result expires within 5 minutes and the
worker refetches from the partners API at runtime (where the env vars
actually exist), populating the page. Also drops the now-redundant
cache: 'no-store' from partnersApiFetch.
2026-05-27 10:53:17 +00:00
086b79e5b9 i18n - translations (#20952)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 11:02:42 +02:00
Félix MalfaitandGitHub 8cb88cabee fix(role): rebind API keys + agents before deleting their role (#20935)
## Customer-reported bug

A customer hit this when using the AI chat:

```json
{
  "message": "API key 760d4822-da40-4b3f-9031-40563d7ed6c9 has no role assigned",
  "extensions": {
    "code": "INTERNAL_SERVER_ERROR",
    "userFriendlyMessage": "This API key has no role assigned."
  }
}
```

Their integration authenticates via API key. Somewhere along the way,
the role bound to that API key was deleted, leaving the API key
authenticated but role-less. Any request that hits a permission check
(`getRoleIdForApiKeyId`) blows up.

## Root cause

In `RoleService.deleteManyRoles`, the pre-deletion cleanup
(`assignDefaultRoleToMembersWithRoleToDelete`) only rebinds **user
workspaces** to the workspace default role. API keys and agents pointing
at the role are ignored. Because `RoleTargetEntity.role` declares
`onDelete: 'CASCADE'`, the FK then drops the role_target rows for those
API keys / agents — but the API keys themselves stay in `api_key`, now
orphaned in `apiKeyRoleMap`.

A previous read-side workaround
([2767ddac44](https://github.com/twentyhq/twenty/commit/2767ddac44) —
make the `role` ResolveField nullable) handled the API-key-details page,
but did not address the write paths (`getRoleIdForApiKeyId`).

## Fix

- Rename `assignDefaultRoleToMembersWithRoleToDelete` →
`rebindTargetsOfRoleToDeleteToDefaultRole` and extend it to rebind API
keys (via `ApiKeyRoleService.assignRoleToApiKey`) and agents (via
`AiAgentRoleService.assignRoleToAgent`) in the same step, before the
role is deleted.
- If the workspace default role doesn't satisfy `canBeAssignedToApiKeys`
/ `canBeAssignedToAgents`, the inner `assignRoleTo*` validation throws.
We catch that and rethrow as a `PermissionsException` with a
role-deletion-context message and two new codes —
`ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS` /
`ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS` — so the admin sees a clear
"reassign these first" prompt rather than a confusing inner error.

## Scope / non-goals

- **Already-orphaned API keys are not auto-healed.** The customer still
needs to reassign a role to their existing orphan API key via the UI
(Settings > API Keys > [the key] > role). A separate cleanup command for
existing orphans is a follow-up.
- I did not investigate *why* the customer's session was authenticated
via API key in the AI chat — that may be their integration setup. Worth
confirming with them separately.

## Test plan

- [ ] Workspace with default role `Admin` (which has
`canBeAssignedToApiKeys: true`): create an API key with a custom role,
delete the custom role → API key is rebound to Admin, requests keep
working.
- [ ] Workspace with default role `Member` (default, has
`canBeAssignedToApiKeys: false`): create an API key with a custom role,
delete the custom role → role deletion fails with the new
`ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS` error explaining the admin must
reassign first. API key + custom role are both unchanged.
- [ ] Same two scenarios for agents (`canBeAssignedToAgents`).
- [ ] Existing user-workspace rebind behavior is unchanged.
- [ ] Role deletion with no dependent API keys / agents still works.
2026-05-27 10:54:02 +02:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
ac89d2ff56 feat: raise FILES field max number of values from 10 to 60 (#20950)
## Summary

Raises the artificial hardcoded ceiling on `maxNumberOfValues` for
custom FILES fields from `10` to `60` so users can attach more files per
record.

- Bumped `FILES_FIELD_MAX_NUMBER_OF_VALUES` constant in `twenty-shared`
from `10` to `60`
- Updated validator unit test (inline snapshots + "exceeds max" case)
- Updated create/update files-field metadata integration tests and Jest
snapshots

The frontend Zod schema only enforces a `min`, so no frontend changes
are required — the backend constant is the single source of truth for
the upper bound.

Refs #20942

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

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-05-27 10:53:09 +02:00
423faa6153 i18n - translations (#20951)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 10:40:15 +02:00
martmullandGitHub a051490ec9 Basic app logo fixes (#20919)
as title, took the quick win fixes from
https://github.com/twentyhq/twenty/pull/20909/changes#diff-3367344412b2f44f0273d8019c1bc36396198244b9558d02921b135f62522baaR180
and leave the main fix for later as it requires an architectural update
2026-05-27 08:21:35 +00:00
34db7ac8b4 chore: sync AI model catalog from models.dev (#20948)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-27 09:19:32 +02:00
EtienneandGitHub 4a82cddad6 remove ai-model-preferences var env and config (#20859)
Split the single AI_MODEL_PREFERENCES JSON config into 4 array configs
and migrates existing workspace data.
2026-05-27 06:47:47 +00:00
Abdullah.andGitHub 9d6c5b7d58 [Website] Restore shared build dependency for typecheck (#20936)
Should fix the CI failure on `typecheck` by building twenty-shared
first.
2026-05-26 19:33:04 +02:00
Paul RastoinandGitHub a2db0b6932 Fix index cache computation (#20933)
## Introduction
Should rely on custom typeorm entity loader layer that inspects the
upgradeMigration that has bene run to dynamically request existing col
only
2026-05-26 18:55:24 +02:00
EtienneandGitHub 5eb79e7797 fix(billing) - fix orphaned stripe subs 2/2 (#20916)
Fixes https://sonarly.com/issue/40688

Should have been included in
https://github.com/twentyhq/twenty/commit/0edd8d400c646cd6a40ff0fea5342a0f61645d5e
2026-05-26 16:28:21 +00:00
Paul RastoinandGitHub f19647617a Fix cross version upgrade for 2.8 (#20927) 2026-05-26 15:46:06 +00:00
Thomas TrompetteandGitHub 424c6737a1 Fix workflow step output schema not reflecting user-renamed step name (#20922)
## Summary
- Fixes https://github.com/twentyhq/twenty/issues/20906
- Moves `markStepForRecomputation` from `useUpdateStep` into the
lower-level `useUpdateWorkflowVersionStep` hook, so **every** caller
that updates a step also triggers output schema recomputation.
- Previously, renaming a step via the side panel title input called
`useUpdateWorkflowVersionStep` directly (bypassing `useUpdateStep`), so
the variable picker kept showing the initial default name (e.g. "Create
Record") instead of the user's custom name.

## Test plan
- [x] Rename a workflow step via the side panel title input
- [x] Verify the variable picker dropdown shows the updated name
- [x] Verify variable tags/chips in subsequent steps reflect the updated
name
- [x] Verify that updating step settings (e.g. changing object type)
still refreshes the output schema correctly
2026-05-26 15:38:36 +00:00
be8c25dfc2 i18n - translations (#20930)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-26 17:46:04 +02:00
Paul RastoinandGitHub b8de469f37 Refactor and centralize file mimeType integrity check and sanitization (#20889)
# Introduction
closes https://github.com/twentyhq/private-issues/issues/484

This PR refactors the writeFile API to never expect to be passed a
mimetype, its extract is done programmatically low level so any callers
will pass through
Same for the file sanitization

## IANA override
Disclaimer for consistency we existing behavior we wanted to always have
`application/typescript`
- should we rather consider fallbacking to octect-steam instead ?
- Any pulbic assets that has .ts will now also fallback to
`application/typescript` instead of the official IANA

## Integration
Added coverage
2026-05-26 15:10:52 +00:00
c74337978b i18n - docs translations (#20926)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-26 17:25:43 +02:00
059e75e532 chore: bump version to 2.9.0 (#20925)
## Summary

- Moves current version to previous versions array
- Sets TWENTY_CURRENT_VERSION to the new version
- Updates TWENTY_NEXT_VERSIONS with the next minor version
- Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same
version

## Checklist

- [ ] Verify version constants are correct
- [ ] Verify npm package versions match

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-05-26 17:14:22 +02:00
bb5c2bd00c Fix/restore channel association scalar field metadata (#20920)
[#20836](https://github.com/twentyhq/twenty/pull/20836) dropped the
channel objects but even though
calendarChannelId/messageChannelId/messageFolderId already existed in
compute standard flat field, there was never an upgrade command to readd
them on the surviving association objects

so existing workspaces lost the field metadata (columns survived) and
import workers throw
```Error: Unknown error importing calendar events for calendar channel <REDACTED> in workspace <REDACTED>: Query runner already released. Cannot run queries anymore.```

This PR adds that command

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-05-26 14:58:33 +00:00
a2673da164 i18n - translations (#20924)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-26 17:03:31 +02:00
WeikoandGitHub 08682fb3f5 Various fixes - some AI findings (#20921)
App permissions tab:
- The fallback uuidv4() for a marketplace field was generated twice, so
id and universalIdentifier could diverge; it's now computed once and
reused as it seemed to be the intention (even though I don't really
think it's a good idea)
- Renamed buildobjectMetadataItemsFromMarketplaceApp →
buildObjectMetadataItemsFromMarketplaceApp to follow camelCase.

Morph relation validation:
- Fixed the user-facing message "At least one relation is require" →
"...is required"
- Typos in the related test descriptions (Morh → Morph, samefield → same
field) and their snapshots.

Docs
- The UUID field-type row in views.mdx only listed IS; updated to the
full set supported by FILTER_OPERANDS_MAP (IS, IS_NOT, IS_EMPTY,
IS_NOT_EMPTY).
2026-05-26 14:43:50 +00:00
53d4e92dda [Website] Generate release notes manifest at build time (#20913)
Removed the releases page’s runtime dependency on `fs` and
`process.cwd()` by introducing a build-time manifest generator: release
notes still live as markdown under `src/content/releases`, but a new
script now parses their frontmatter/content, validates that each note
has a release, title, and preview image (and that the image actually
exists), sorts the notes, and emits a typed `generated-release-notes.ts`
file that the app imports at runtime.

Updated the releases loader to return that generated data, changed the
menu releases preview and release JSON-LD to use explicit typed fields
(`title`, `previewImage`) instead of scraping markdown with regex at
runtime, wired the generator into Nx so it runs automatically before
`dev`, `build`, and `typecheck`, and fixed two stale image references in
the release MDX files that the new validation exposed.

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-05-26 16:46:11 +02:00
Rashad KaranouhandGitHub 53392f9a16 feat(twenty-partners): partnerContent catalog + TFT import improvements (#20904)
## Summary

Two related threads for the internal `twenty-partners` app:

1. **Redesign `partnerQuote` → `partnerContent`.** The object was
mis-modeled as a sales/pre-invoice doc (`amount`, opportunity link). In
TFT it's actually a marketing-content catalog — customer quotes, case
studies, partner quotes, logos — moving through a production lifecycle.
This renames it in place and reshapes it to mirror TFT's
`CustomerContent`.
2. **Import tooling improvements** to the TFT importer + multi-env
workflow.

## Changes

**Schema (`partnerContent`)**
- Rename `partnerQuote` → `partnerContent` (object, view, nav, relation
fields, identifiers).
- Add `contentType` MULTI_SELECT `[CUSTOMER_QUOTE, CASE_STUDY,
PARTNER_QUOTE, LOGO]` and `interview` LINKS.
- Add `customerCompany` / `customerPerson` relations; keep `partner`;
drop the `opportunity` link (TFT has none).
- Drop `amount`; rename the FILES field `quoteFile` → `documents`
(`attachments` is a reserved morph-relation name).

**Importer (`import-from-tft.ts`)**
- Import the full content catalog (all types), not just `PARTNER_QUOTE`.
- Map TFT `partnerTimezone` → `region`, default
`languagesSpoken=[ENGLISH]`, and set `deploymentExpertise=[SELF_HOST]`
when scope includes `HOSTING_ENVIRONMENT`.
- Filter to partner-relevant records only: opportunities linked to a
partner (20 of 164), content linked to a partner (10 of 22). Drops
general sales-pipeline / customer-only noise.
- Dedupe companies by **normalized domain** (Twenty's unique key), not
just name — fixes duplicate-entry crashes when the same company arrives
under different names.
- Progress logging throughout.

**Tooling**
- `purge-soft-deleted` script (soft-deleted rows block re-imports via
unique constraints).
- Multi-env script variants (`*:prod`) selected via `ENV_FILE`.

## Testing

Verified on a local Twenty instance and on `partner.twenty.com`:
- 122 partners, 20 partner-linked opportunities, 10 partner-linked
content (all types), 229 domain-deduped companies.
- Schema confirmed via metadata introspection; `yarn twenty typecheck`
clean.

## Notes

- Renaming an installed object isn't a pure in-place migration on a
server that already had `partnerQuote` — the working path is `uninstall
→ deploy → install` (safe here: prod had no data).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-26 14:09:57 +00:00
7e7bb81586 i18n - translations (#20911)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-26 12:13:49 +02:00
3703b3e2f6 i18n - translations (#20910)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-26 12:06:57 +02:00
Charles BochetandGitHub e72d10f550 chore(security): bump esbuild to ^0.28.0 to clear CVE-2025-68121 (CVSS 10.0) (#20902)
## Summary

Follow-up to #20876. That PR bumped `esbuild` to `^0.27.3` to address
the Go-stdlib CVEs the self-hoster reported, but only one of the two Go
CVEs is actually fixed at that level. This PR closes the remaining gap.

### Why 0.27.3 wasn't enough

`esbuild` ships a Go-built binary inside the `@esbuild/<platform>`
packages. The vulnerability lives in the bundled Go toolchain, not in
any JavaScript. Verified by reading the Go `buildinfo` section from
`node_modules/@esbuild/<platform>/bin/esbuild`:

- `esbuild@0.27.7` → built with **Go 1.23.8**
- `esbuild@0.28.0` → built with **Go 1.26.1**

CVE-2024-24790 (IPv6 zone parsing) is fixed in Go 1.21.11 / 1.22.4, so
0.27.x covers it.

**CVE-2025-68121** (crypto/tls cert validation bypass via TLS session
resumption, **CVSS 10.0 / Critical** per
[NVD](https://nvd.nist.gov/vuln/detail/cve-2025-68121)) is fixed only in
Go 1.24.13, 1.25.7, and 1.26.0-rc.3+. Go 1.23.x is past Go's support
window and will not receive this fix. So `esbuild@0.27.x` still ships a
Go binary that Trivy correctly flags as vulnerable.

### Reachable risk in Twenty

Low. `esbuild` does not use `crypto/tls` at runtime — it reads files,
parses, transforms, and writes. The vulnerable code path is dead code
inside the binary, present but never executed. The scan finding is what
we are clearing, not an exploitation risk.

### Fix

Bump `twenty-client-sdk`'s `esbuild` from `^0.27.3` to `^0.28.0`
(resolves to 0.28.0, built with Go 1.26.1).

### Verification

Ran `yarn workspaces focus --production twenty twenty-server
twenty-emails twenty-shared twenty-client-sdk` (the same install the
Dockerfile uses) and confirmed:

- `node_modules/esbuild/` resolves to `esbuild@0.28.0` (single copy)
- The bundled `node_modules/@esbuild/<platform>/bin/esbuild` binary
reports `go1.26.1` in its `buildinfo`

## Test plan

- [x] `nx typecheck twenty-server` passes
- [x] `nx build twenty-client-sdk` passes (esbuild's `build()` API is
stable across 0.27 → 0.28)
- [x] Production focus install shows Go 1.26.1 in the shipped binary
- [ ] CI green
- [ ] Re-run Trivy against the resulting image; confirm CVE-2025-68121
no longer appears
2026-05-26 09:49:25 +00:00
martmullandGitHub 7ca9081efa Add application installation validation modale (#20907)
## After

<img width="1512" height="829" alt="image"
src="https://github.com/user-attachments/assets/231d4f0d-6052-4c4e-9a2a-0244d2b3832e"
/>
2026-05-26 09:48:31 +00:00
015dca95fc Edit actor chip icon style & read-only behavior (#18552)
https://github.com/user-attachments/assets/925f4380-e3e2-430d-a8e3-7e1242298900

Removed background color from icons for consistency
Removed chip hover state as chips are not navigable
Updated read-only design (text/secondary on chip hover)

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2026-05-26 09:38:39 +00:00
Paul RastoinandGitHub 076c05cbd0 File service uniformize not found behavior and stream management (#20891)
# Introduction

closes https://github.com/twentyhq/private-issues/issues/485
2026-05-26 09:23:48 +00:00
Félix MalfaitandGitHub 82c565f7dc perf(website): cache prerendered pages in CF Cache API (withRegionalCache) (#20905)
## Summary
Recovers most of the TTFB the EKS→Cloudflare migration lost on
`twenty.com`. OpenStatus's P50 chart shows the regression clearly: TTFB
went from ~50–80ms (pre-migration, CF edge cache HIT) to ~250–350ms
(post-migration, every request hits Worker → R2 → respond).

## Why the existing Cache Rule stopped working
The zone-level `Twenty Website - Aggressive cache` Cache Rule was
correctly configured and was the reason pre-migration TTFB was low. It
still exists, still has `cache: true`, Edge TTL 1d. But it doesn't apply
to Worker responses on a Worker custom domain:

- **Pre-migration** request flow: `edge → Cache Rule lookup → HIT
(~20ms) / MISS → origin → cache the response`
- **Post-migration**: `edge → Worker runs first (custom domain) → Worker
generates synthetic response from R2 → return`

Cache Rules cache responses obtained via `fetch()` from the Worker, not
synthetic responses constructed inside the Worker. OpenNext for SSG
pages reads prerendered HTML from R2 and returns it — that's synthetic.
So the rule has no insertion point.

This is structural to how CF Workers handle custom domains; not a
misconfiguration on your side.

## The fix
`open-next.config.ts`:
```ts
const incrementalCache = withRegionalCache(r2IncrementalCache, {
  mode: 'long-lived',
});
const baseConfig = defineCloudflareConfig({ incrementalCache });
```

OpenNext-native wrapper. The Worker still runs per request (~5–20ms
execution), but the ISR cache lookup goes through CF's per-region Cache
API (~5–20ms) instead of R2 (~50–150ms). For pages whose prerender
doesn't change between requests, that's the bulk of the TTFB recovered.

## Measured impact (live before/after on twenty.com today)
| URL | Before (avg of 3) | After cold (first 2 hits/region) | After
warm |
|---|---|---|---|
| `/` | 322ms | 600–640ms | **110–125ms** |
| `/pricing` | 267ms | 630–690ms | **104–110ms** |
| `/why-twenty` | 250ms | 175–270ms | **100–175ms** |

First 1–2 hits per CF region after this deploys will be slower than
baseline (regional Cache API populating from R2), then it sustains.
Steady state is significantly better than pre-fix.

## What this doesn't recover
Pre-migration `cf-cache-status: HIT` was ~20–30ms because the Worker
wasn't invoked at all. We can't get there without either:
- Moving SSG hosting off the Worker (back to a static origin Cache Rules
would cover)
- OpenNext gaining a "publish responses to caches.default" mode (doesn't
exist today)

Realistic-best on CF Workers + OpenNext is around the ~80–130ms range
we're now seeing.

## Live state
Already deployed to both prod (Version `40dfaa1a-...`) and dev (Version
`b45cc2de-...`) ahead of opening this PR, so the OpenStatus chart should
start improving immediately. This PR makes `main` reflect the change.
2026-05-26 09:20:55 +02:00
martmullandGitHub 44bae39538 Publish new cli tool version (#20903)
publish 2.8.0
2026-05-26 07:06:04 +00:00
WeikoandGitHub 1188ea9cd5 chore(server): remove unused REST→GraphQL HTTP bridge (#20897)
## Summary

Removes the unused `RestApiService` HTTP bridge that posted to
`/graphql` and `/metadata` from inside the server.
2026-05-25 19:56:32 +00:00
f721420705 i18n - docs translations (#20898)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-25 20:50:59 +02:00
96d247e94d i18n - translations (#20895)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-25 19:20:56 +02:00
WeikoandGitHub 90f711361c Add definePermissionFlag for app-defined permission flags (#20887)
## Context
Adds the SDK plumbing for apps to declare custom permission flags and
the server-side manifest pipeline to persist them.

```typescript
import { definePermissionFlag } from 'twenty-sdk/define';

export const MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER = '…';

export default definePermissionFlag({
  universalIdentifier: MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER,
  key: 'MANAGE_INVOICES',
  label: 'Manage Invoices',
  description: 'Create, edit, and delete invoices',
  icon: 'IconReceipt',
});
```

```typescript
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
import { MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER } from './permission-flags/manage-invoices';

export default defineApplicationRole({
  universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  label: `${APP_DISPLAY_NAME} default function role`,
  // ...
  permissionFlagUniversalIdentifiers: [
    SystemPermissionFlag.UPLOAD_FILE,
    MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER,
  ],
});
```

The flag can then be referenced by UUID in a role's
permissionFlagUniversalIdentifiers. On sync, the catalog row lands in
core.permissionFlag and the link in core.rolePermissionFlag.

## Not in this PR
- Runtime permission checks.
PermissionsService.getUserWorkspacePermissions still builds its result
from Object.values(PermissionFlagType), so custom flags are stored but
not yet enforced, code asking "does this role have MANAGE_INVOICES?"
won't get a meaningful answer. Widening PermissionsService and
UserWorkspacePermissions.permissionFlags to support arbitrary flag keys
is the next PR.
- PermissionFlag from apps can only define "tool" permissions and not
"settings" as a permissionType, this parameter is not mutable. This is
because "settings" are for settings page (until we might decide to
separate both type of permissions into 2 different entities) and apps
can't declare settings page or interact with them so this parameter
would be unnecessary.
2026-05-25 16:53:37 +00:00
98d47d0dd0 i18n - docs translations (#20893)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-25 19:02:12 +02:00
f613886511 fix(localization): parse date-only ISO strings as local midnight in relative date formatter (#20630)
## Summary

Fixes #19634

### Root Cause

The ECMAScript spec treats date-only strings (`YYYY-MM-DD`) as **UTC
midnight** when passed to `new Date()`. But `date-fns` comparison
functions (`isToday`, `isYesterday`, `isTomorrow`) operate in **local
time**. For users in UTC-negative timezones, UTC midnight April 14 is
April 13 evening locally — so the label shows "Yesterday" instead of
"Today".

### Fix

In `formatDateISOStringToRelativeDate.ts`, detect date-only strings
(length === 10) and append `T00:00:00` (no `Z`) to force local-time
parsing:

```ts
// Before
const targetDate = new Date(isoDate);

// After
const targetDate =
  isoDate.length === 10 ? new Date(isoDate + 'T00:00:00') : new Date(isoDate);
```

Full datetime strings (with time component) are left unchanged — they
already carry timezone information.

### Tests

Added `formatDateISOStringToRelativeDate.test.ts` covering:
- `Today` / `Yesterday` / `Tomorrow` labels for date-only strings
- Regression case: date-only string parsed at local midnight (not UTC
midnight)
- Full datetime strings continue to work as before

## Before / After

| Scenario | Before | After |
|---|---|---|
| `"2026-04-14"` viewed at UTC-5 on April 14 | Yesterday  | Today ✓ |
| `"2026-04-14"` viewed at UTC+0 on April 14 | Today ✓ | Today ✓ |
| `"2026-04-14T12:00:00Z"` | Today ✓ | Today ✓ |

---------

Co-authored-by: Marie Stoppa <marie@twenty.com>
2026-05-25 15:54:36 +00:00
a3e41ec267 i18n - translations (#20892)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-25 17:54:07 +02:00
Félix MalfaitandGitHub d602f35cbd feat(data-model): custom-indexes management UI and mutations (#20846)
## Summary

Brings indexes management into the per-object Settings tab as a section
under Search (no feature flag, advanced mode only). Admins can create /
delete non-unique indexes with the UI; apps can declare indexes in code
with `defineIndex`. Composite-typed fields are now indexable by picking
a specific sub-column (e.g. `Address > City`).

A few related polish items also land here (invite-user dropdown lands on
the Invite tab; standard warning callout above the new-index form).

## What ships

### UI — custom indexes on per-object Settings
- New section directly under Search, wrapped in
`AdvancedSettingsWrapper`.
- Filter dropdown on the search bar toggles system-index visibility
(shown by default since advanced mode).
- **+ Add Index** button (disabled with tooltip once the per-object cap
is reached) navigates to a dedicated `SettingsObjectNewIndex` page
(matches the field-creation pattern, not a modal):
- Field picker mirrors the webhook event-form layout (rows of dropdowns,
implicit trailing empty row).
- Composite fields surface their sub-properties (`Address > City`,
`Currency > Amount`, …).
  - BTREE / GIN type selector.
- Standard warning Callout: "Use indexes sparingly — each one speeds
reads but slows writes."
- Trash icon on `isCustom: true` rows → confirmation modal →
`deleteOneIndex`.

### Server — `createOneIndex` / `deleteOneIndex` mutations
- Gated by `SettingsPermissionGuard(DATA_MODEL)`.
- `IndexMetadataService` wraps the existing migration runner via
`WorkspaceMigrationValidateBuildAndRunService` so the metadata row and
the SQL index land atomically.
- Validation: rejects empty fields, duplicate `(fieldMetadataId,
subFieldName)` pairs, fields not on the object, requires `subFieldName`
for composite parents, forbids `subFieldName` on scalar/relation,
enforces `MAX_CUSTOM_INDEXES_PER_OBJECT = 10`.
- Delete refuses on `isCustom: false` rows so system indexes can't be
removed via this API.
- Dedicated GraphQL exception handler maps each typed error to the right
transport error class.

### Composite sub-field indexing
- Adds `subFieldName: string | null` column to
`IndexFieldMetadataEntity` (fast instance command).
- The flat-entity flow (`UniversalFlatIndexFieldMetadata`,
`FlatIndexFieldMetadata`, `from-universal-flat-index-to-flat-index`,
runner column resolution) all carry `subFieldName` through.
- For composite parents, the runner uses
`computeCompositeColumnName({...}, property)` for the picked sub-column;
for non-composite parents, behavior is unchanged.
- The `'::'` separator encodes `(fieldMetadataId, subFieldName)` for
dedup on the wire; the frontend uses the same separator inside the
Select component's string value.

### Apps can declare indexes in code (`defineIndex`)
- New `IndexManifest` + `IndexFieldManifest` types in
`twenty-shared/application` wired into the `Manifest` type.
- `defineIndex` SDK helper + `IndexConfig`. CLI manifest builder +
extractor recognize `defineIndex` / `ManifestEntityKey.Indexes`.
- Server: `from-index-manifest-to-universal-flat-index` converter
resolves field IDs, validates composite/scalar `subFieldName` rules, and
delegates to `generateFlatIndexMetadataWithNameOrThrow` for the
deterministic name.
- Orchestrator wires the loop after the field-resolution pass;
per-object cap enforced inline against the manifest.
- Cascade on uninstall is automatic — when an app disappears its indexes
drop with it (universal-flat-entity diff handles it).
- Rich-app fixture ships a real `defineIndex` on `PostCard.status`,
exercising the full manifest → install path in CI.

### Closed for now (open later if needed)
- Apps cannot declare `isUnique` indexes — unique constraints stay with
the field-creation flow.
- Apps cannot use a partial-`indexWhereClause` — the UI surface keeps
the framework's hardcoded allowlist.
- UI cannot create unique or partial indexes either; same reasons.

### Cleanups along the way
- Reused the existing `getCompositeSubFieldLabel` +
`COMPOSITE_FIELD_SUB_FIELD_LABELS` (deleted the duplicates I'd created
early in the PR).
- Moved `MAX_CUSTOM_INDEXES_PER_OBJECT` to `twenty-shared/constants`
(single source for FE + BE).
- Replaced inline `isDefined(x) && x !== ''` with `isNonEmptyString`
(from `@sniptt/guards`).
- Hoisted the per-object fields Map + inlined the cap counter into the
indexes orchestrator loop (drops the install scan from O(indexes ×
totalFields) to O(totalFields + indexes)).
- Per design-feedback: page-based create flow (not a modal), filter
dropdown on the SearchInput (not a separate toggle), webhook-style
picker, field icons.

### Unrelated polish that lands here
- "Invite user" link in the multi-workspace dropdown now lands on the
Invite tab directly (`#invite`) instead of the first tab of the members
page.

## Test plan
- [ ] `npx nx typecheck twenty-server / twenty-front / twenty-sdk /
twenty-shared` — passes
- [ ] `npx nx lint:diff-with-main twenty-server / twenty-front` — clean
- [ ] `npx jest index-metadata.service.spec` — green
- [ ] `npx jest from-index-manifest-to-universal-flat-index` — green
(new converter spec, 8 cases)
- [ ] `npx vitest run
src/sdk/define/indexes/__tests__/define-index.spec.ts` (twenty-sdk) —
green (6 cases)
- [ ] `npx vitest run --config vitest.integration.config.ts -t
"rich-app"` — green (rich-app app-dev integration exercises the new
manifest path with the PostCard.status index)
- [ ] Advanced mode → Settings → any object → Settings tab → Indexes
section is visible under Search
- [ ] Create a single-field BTREE index, confirm SQL index exists
(verify via `pg_indexes`)
- [ ] Create a composite-field index (`Address > City`) and confirm the
column is `addressAddressCity`
- [ ] Create an index spanning two columns; column order matches the
picker order
- [ ] Attempt to create an 11th custom index → button is disabled with
tooltip
- [ ] Delete a custom index → confirmation modal → row disappears, PG
index dropped
- [ ] System indexes have no trash icon and are hidden by default
2026-05-25 17:47:09 +02:00
Paul RastoinandGitHub 69d89f8cfc Early return in public assets (#20881)
# Introduction
Related https://github.com/twentyhq/twenty/issues/20879

More abstracted response error and cleaner integrity check before
performing any in database search
Nothing critical patched here

Also added integration coverage to the related endpoint

Fixed the stream on error throw that would have been bubbling up into
node process

## Next
Once this has been approved will re-apply to all the existing prone
file.getBy* methods and controllers endpoints
2026-05-25 14:17:32 +00:00
Charles BochetandGitHub be39702fd2 chore(security): bump protobufjs and esbuild to clear CVEs (#20876)
## Summary

A self-hoster reported that Trivy blocks the `twentycrm/twenty:v2.7.x`
image on three fixed-critical CVEs. The reachable risk is low (none of
the vulnerable code paths are exposed to attacker-controlled input in
our deployment), but the findings are real and easy to clear by bumping
the affected dependencies in their owning workspaces.

### CVE-2026-41242 — `protobufjs` < 7.5.5

Pulled transitively into the production image via
`@opentelemetry/sdk-node`, `@opentelemetry/auto-instrumentations-node`,
and `@grpc/grpc-js` → `@grpc/proto-loader`. Lockfile was on 7.5.3; this
matches dismissed dependabot alert #1009 (Critical 9.4).

**Fix:** add `protobufjs: ^7.5.5` as a direct dep of `twenty-server`
(the workspace that exercises it via the OpenTelemetry gRPC exporters)
and run `yarn dedupe protobufjs` to collapse the residual transitive
7.5.3 copy. Resolves to 7.6.0.

### CVE-2024-24790 and CVE-2025-68121 — Go stdlib in bundled binaries

Present in the Go-built `bin/esbuild` shipped by `@esbuild/<platform>`
packages. Two paths put esbuild into the production image:

1. `twenty-client-sdk` declares `esbuild` as a runtime dep (used by its
`./generate` entry point).
2. `twenty-server` had `@lingui/vite-plugin` in `dependencies`, which
pulls `@lingui/cli` as a runtime sub-dep, which bundles `esbuild@0.21.5`
nested under `node_modules/@lingui/cli/node_modules/esbuild/`.

**Fix:**
- Bump `twenty-client-sdk`'s `esbuild` from `^0.25.0` to `^0.27.3`
(resolves to 0.27.7, built with patched Go).
- Move `@lingui/vite-plugin` from `dependencies` to `devDependencies` in
`twenty-server`. The plugin is not imported by any source file — it was
misclassified.

### Verification

Ran `yarn workspaces focus --production twenty twenty-server
twenty-emails twenty-shared twenty-client-sdk` (the same command the
Dockerfile uses) and inventoried the resulting `node_modules`. After all
three changes:

- `node_modules/esbuild/` → **0.27.7 only** (Go-patched)
- `node_modules/protobufjs/` → **7.6.0 only** (CVE-patched)

No nested copies of either package remain in the production install.

### Follow-up worth tracking separately

`esbuild` should arguably not be in `twenty-client-sdk`'s `dependencies`
at all — only the `./generate` entry point uses it, and the server never
imports that entry. Moving it to optional `peerDependencies` would stop
shipping a Go binary into the production image entirely. Out of scope
for this PR.

## Test plan

- [x] `yarn install` succeeds; `protobufjs` and `esbuild` each resolve
to a single version in production focus
- [x] `nx build twenty-client-sdk` passes
- [x] `nx typecheck twenty-server` passes
- [x] `nx build twenty-server` passes
- [x] Production focus install confirmed clean (`node_modules/esbuild`
and `node_modules/protobufjs` both single-version, both patched)
- [ ] CI green
- [ ] Re-run Trivy against the resulting image; confirm the three CVEs
no longer appear
2026-05-25 12:36:53 +00:00
EtienneandGitHub 50c8834e21 fix(billing) - skip redundant cap writes (#20882)
fixes https://sonarly.com/issue/40207
2026-05-25 12:03:18 +00:00
d54caf10c1 i18n - translations (#20885)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-25 14:10:10 +02:00
nitinandGitHub 0b38b4ffc4 fix(page-layout): respect tab layoutMode on standalone pages (#20856)
Standalone-page tabs with `layoutMode: CANVAS` were silently rendering
as GRID (border, padding, scroll). Now they render full-bleed, matching
the CANVAS contract elsewhere.

Three layered fixes:
- `getTabLayoutMode`: respect `tab.layoutMode` for `STANDALONE_PAGE`
(was hardcoded to GRID for any non-`RECORD_PAGE`)
- `getWidgetCardVariant`: CANVAS now wins regardless of page type —
refactored to early-return + exhaustive switch on `pageLayoutType`
- `FrontComponentWidgetRenderer`: removed hardcoded `overflow: auto`
(workflow/tasks/timeline widgets don't have it either)

New `getTabLayoutMode.test.ts`. Variant tests refactored to declarative
+ parameterized.

QA:

<img width="3024" height="1654" alt="CleanShot 2026-05-22 at 20 46
50@2x"
src="https://github.com/user-attachments/assets/cc61d459-6bc6-48de-ac79-d63a2ccd8957"
/>


https://github.com/user-attachments/assets/a3374e18-ad1b-4888-ab2b-d07730edccac
2026-05-25 11:59:31 +00:00
Paul RastoinandGitHub b8b115f4e3 FileStorageService Dedicated file and folder code flow + integrity check (#20831)
# Introduction

Next handling mimetype integrity check and checksum integrity check for
s3 storage type

Always expecting a trailing end slash when deleting a folder etc

## Application
Uninstalling an application now deletes all its related files

## File storage service
Making a distincton between folder path and file path

## Validation Pipeline

Every file operation in `FileStorageService.buildOnStoragePath` runs
through `validateResourcePath`, which chains three validators in order:

**1. `validateSafeRelativePath`** -- rejects path traversal attacks

| Input | Result | Error |
|---|---|---|
| `../../../etc/passwd` | Rejected | `Resource path must not contain
path traversal (..)` |
| `/etc/passwd` | Rejected | `Resource path must be relative, not
absolute` |
| `file\0.txt` | Rejected | `Resource path contains null bytes` |
| `..\\..\\etc\\passwd` | Rejected | `Resource path must not contain
backslashes` |
| _(empty)_ | Rejected | `Resource path must not be empty` |

**2. `validateFilenameIntegrity`** -- enforces safe characters, length
limits, extension required

| Input | Result | Error |
|---|---|---|
| `my folder/file.mjs` | Rejected | `A path segment contains invalid
characters...` |
| `Makefile` | Rejected | `Filename must have an extension` |
| `aaa...(256 chars).mjs` | Rejected | `A path segment exceeds the
maximum length of 255 characters` |
| `a/b/.../file.mjs` (1025+ chars) | Rejected | `Resource path exceeds
maximum length of 1024 characters` |
| `src/handlers/index.mjs` | Accepted | -- |
| `my-app/my_file.tsx` | Accepted | -- |
| `v1.0/module.config.mjs` | Accepted | -- |

Allowed characters per segment: `a-z`, `A-Z`, `0-9`, `.`, `-`, `_`

**3. `validateResourceExtension`** -- checks extension against the
`FileFolder` allowlist

| Input | FileFolder | Result | Error |
|---|---|---|---|
| `handler.js` | `BuiltLogicFunction` | Rejected | `Invalid file
extension. Allowed extensions: .mjs` |
| `card.tsx` | `BuiltFrontComponent` | Rejected | `Invalid file
extension. Allowed extensions: .mjs` |
| `script.js` | `PublicAsset` | Rejected | `Invalid file extension.
Allowed extensions: .png, .jpg, ...` |
| `index.mjs` | `BuiltLogicFunction` | Accepted | -- |
| `app.tsx` | `Source` | Accepted | -- |
| `photo.png` | `CorePicture` | Accepted | -- (unconfigured folder,
passes through) |

## Consumers

- **`FileStorageService`** -- calls `validateResourcePath`, throws
`FileStorageException` on failure (last-resort defense)
- **Resolver (`uploadApplicationFile`)** -- calls
`validateResourcePath`, throws `ApplicationException` on failure
(user-facing)
- **Flat validators** -- call `validateResourcePath`, push the error to
`validationResult.errors` (non-throwing, collects all errors)

All error messages are translated via Lingui `t` and returned in a
discriminated union `{ isValid: true } | { isValid: false, error: string
}`, letting each consumer decide how to handle failures.
2026-05-25 11:52:54 +00:00
85d649e831 [Fix] Backfill missing command menu items conditional availability expression (#20852)
## Description

Following [report in
discord](https://discord.com/channels/1130383047699738754/1498690477044793386/1506602927412744242)

Some command menu items were showing to all users because they had no
conditional availability expression, whereas users did not actually have
access to the page or feature behind. For instance: "Go to Admin panel",
"Go to AI settings", "Send email" etc.

<img width="833" height="1245" alt="image"
src="https://github.com/user-attachments/assets/8d2a9404-9b81-4d58-9522-558e9924c457"
/>


## Fix

- Add conditional availability expressions
- Backfill expressions for existing workspaces as they are stored in db
(commandMenuItems table)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-23 13:22:25 +00:00
Charles BochetandGitHub 2f4ebf8160 Move preview environment workflow to ci-privileged (#20872)
## Summary

Slims `preview-env-dispatch.yaml` to a single dispatch and deletes
`preview-env-keepalive.yaml`. The actual preview-env work moves to
**twentyhq/ci-privileged#22** (must merge as a pair).

## Why

Context: PR #20867 was a credential-exfil attempt against our workflows.
GitHub's default fork-PR-no-secrets policy + our existing gates
(`author_association` checks, `pull_request_target` checking out base,
`enableScripts: false`) neutralized the actual attack — but the audit
surfaced one workflow that *would* have given a malicious external PR
access to a real secret if a maintainer had applied the `preview-app`
label: `preview-env-keepalive.yaml`.

That workflow checked out the PR head SHA, did `docker login` with
`DOCKERHUB_PASSWORD`, then ran the PR's `docker-compose.yml`. A
malicious compose could have mounted `~/.docker/config.json` and
exfiltrated the Dockerhub credential.

After this PR, that workflow lives in `twentyhq/ci-privileged` instead,
paired with a rename of the credential to `DOCKERHUB_RO_TOKEN`
(Dockerhub PAT with `Public Repo Read-only` scope). A read-only PAT has
no exfiltration value — it's equivalent to anonymous Dockerhub access
plus rate-limit headroom — so the credential lives safely on the runner
without further hygiene tricks.

## What this PR does

- **Modifies** `.github/workflows/preview-env-dispatch.yaml`:
- Single dispatch to `twentyhq/ci-privileged` (was: self-dispatch to
twenty for the env + a separate dispatch to ci-privileged for the PR
comment).
  - `permissions: {}` (was: `contents: write`).
  - Drops `preview-env-keepalive.yaml` from the path-trigger list.
- **Deletes** `.github/workflows/preview-env-keepalive.yaml`. The
207-line workflow now lives in
`twentyhq/ci-privileged/.github/workflows/preview-env.yaml`.

Net `twenty` repo change: **-204 lines / +3 lines**.

## Companion PR

twentyhq/ci-privileged#22 — adds the new `preview-env.yaml`, deletes the
now-redundant `post-preview-comment.yaml`.

## Secrets fallout in this repo

After this PR, `DOCKERHUB_PASSWORD` in `twentyhq/twenty` secrets is only
used by `ci-test-docker-compose.yaml`, where:
- It evaluates to empty for fork PRs (GitHub default — secrets aren't
passed to fork-PR workflows).
- It's only needed for internal / merge_queue runs, for Dockerhub
rate-limit headroom on base-image pulls.

Recommend (separate change): also convert the twenty-side
`DOCKERHUB_PASSWORD` to a `Public Repo Read-only` Dockerhub PAT, and
rename it to `DOCKERHUB_RO_TOKEN` for consistency with ci-privileged.
The workflow change for `ci-test-docker-compose.yaml` would just be a
rename — login flow is identical for password vs. PAT.

## Test plan

- [ ] Merge twentyhq/ci-privileged#22 first (so the dispatched event has
a handler)
- [ ] Open an internal PR touching `packages/twenty-docker/**`, confirm
`Preview Environment Dispatch` runs and ci-privileged's `Preview
Environment` workflow runs the docker compose + posts the URL
- [ ] On an external contributor PR, apply the `preview-app` label,
confirm the same flow
- [ ] Confirm closing the PR doesn't break (no cleanup workflow was
changed)
2026-05-23 11:37:37 +00:00
77603a4102 i18n - docs translations (#20869)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-23 12:47:00 +02:00
2927 changed files with 142006 additions and 45893 deletions
+28 -3
View File
@@ -6,18 +6,43 @@ permissions:
on:
push:
tags:
- 'v*'
- 'twenty/v*'
- 'sdk/v*'
defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}
jobs:
deploy-tag:
dispatch-tag:
timeout-minutes: 3
runs-on: ubuntu-latest
steps:
- name: Resolve dispatch event from tag family
id: target
env:
REF_NAME: ${{ github.ref_name }}
run: |
case "$REF_NAME" in
twenty/v*)
event_type=auto-deploy-twenty
;;
sdk/v*)
event_type=auto-publish-npm
;;
*)
echo "Unsupported tag '$REF_NAME', expected 'twenty/v*' or 'sdk/v*'." >&2
exit 1
;;
esac
printf 'event_type=%s\n' "$event_type" >> "$GITHUB_OUTPUT"
- name: Repository Dispatch
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
EVENT_TYPE: ${{ steps.target.outputs.event_type }}
REF_NAME: ${{ github.ref_name }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches \
-f event_type=auto-deploy-tag \
-f "event_type=$EVENT_TYPE" \
-f "client_payload[github][ref_name]=$REF_NAME"
+44
View File
@@ -0,0 +1,44 @@
name: CI Codex Plugin
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-codex-plugin/**
.github/workflows/ci-codex-plugin.yaml
codex-plugin-validate:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Codex Plugin / Validate
run: npx nx run twenty-codex-plugin:validate
- name: Codex Plugin / Test
run: npx nx run twenty-codex-plugin:test
@@ -120,7 +120,7 @@ jobs:
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.dependencies['twenty-sdk'];
const sdkVersion = pkg.devDependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
@@ -80,10 +80,20 @@ jobs:
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
- name: Resolve Playwright version
id: playwright-version
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: v4-playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: |
cd packages/twenty-front-component-renderer
npx playwright install
npx playwright install chromium
- name: Serve storybook & run tests
run: |
npx http-server packages/twenty-front-component-renderer/storybook-static --port 6008 --silent &
+11 -1
View File
@@ -96,10 +96,20 @@ jobs:
with:
name: storybook-static
path: packages/twenty-front/storybook-static
- name: Resolve Playwright version
id: playwright-version
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: v4-playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: |
cd packages/twenty-front
npx playwright install
npx playwright install chromium
- name: Front / Write .env
run: npx nx reset:env twenty-front
- name: Serve storybook & run tests
-63
View File
@@ -1,63 +0,0 @@
name: "Release: on merge"
permissions:
contents: write
on:
pull_request:
types:
- closed
defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}
jobs:
tag_and_release:
timeout-minutes: 10
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
steps:
- name: Check PR Author
id: check_author
env:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
set -euo pipefail
if [[ "$PR_AUTHOR" != "github-actions[bot]" ]]; then
echo "PR author ($PR_AUTHOR) is not trusted. Exiting."
exit 1
fi
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: main
- name: Get version from PR title
id: extract_version
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail
VERSION=$(printf '%s' "$PR_TITLE" | sed -n 's/.*Release v\([0-9][0-9.]*\).*/\1/p')
if [ -z "$VERSION" ]; then
echo "No valid version found in PR title. Exiting."
exit 1
fi
printf 'VERSION=%s\n' "$VERSION" >> "$GITHUB_ENV"
- name: Push new tag
run: |
set -euo pipefail
git config --global user.name 'Github Action Deploy'
git config --global user.email 'github-action-deploy@twenty.com'
git tag "v${{ env.VERSION }}"
git push origin "v${{ env.VERSION }}"
- uses: release-drafter/release-drafter@09c613e259eb8d4e7c81c2cb00618eb5fc4575a7 # v5
if: contains(github.event.pull_request.labels.*.name, 'create_release')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: v${{ env.VERSION }}
+12 -10
View File
@@ -27,11 +27,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
# Pull base images through Google's Docker Hub mirror — avoids Docker Hub
# rate limits and needs no credentials (this repo is public).
- name: Configure Docker Hub mirror
run: |
echo '{"registry-mirrors":["https://mirror.gcr.io"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
- name: Run compose
run: |
echo "Patching docker-compose.yml..."
@@ -102,11 +103,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
# Pull base images through Google's Docker Hub mirror — avoids Docker Hub
# rate limits and needs no credentials (this repo is public).
- name: Configure Docker Hub mirror
run: |
echo '{"registry-mirrors":["https://mirror.gcr.io"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
- name: Create frontend placeholder
run: |
mkdir -p packages/twenty-front/build
+18 -16
View File
@@ -3,6 +3,8 @@ name: CI UI
on:
pull_request:
merge_group:
push:
branches: [main]
permissions:
contents: read
@@ -13,7 +15,7 @@ concurrency:
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
if: github.event_name == 'pull_request'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
@@ -40,7 +42,10 @@ jobs:
run: npx nx ${{ matrix.task }} twenty-ui
ui-sb-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
if: >-
always() &&
(github.event_name == 'push' ||
needs.changed-files-check.outputs.any_changed == 'true')
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
@@ -62,8 +67,7 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: ui-sb-build
env:
STORYBOOK_URL: http://localhost:6007
if: always() && needs.ui-sb-build.result == 'success'
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
@@ -82,22 +86,20 @@ jobs:
run: |
cd packages/twenty-ui
npx playwright install
- name: Serve storybook & run tests
run: |
npx http-server packages/twenty-ui/storybook-static --port 6007 --silent &
timeout 30 bash -c 'until curl -sf http://localhost:6007 > /dev/null 2>&1; do sleep 1; done'
npx nx storybook:test twenty-ui
- name: Run storybook tests
run: npx nx storybook:test twenty-ui
- name: Upload screenshots for visual regression
if: always() && !cancelled()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: argos-screenshots-twenty-ui
path: packages/twenty-ui/screenshots
retention-days: 1
ci-ui-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs:
[
changed-files-check,
ui-task,
ui-sb-build,
ui-sb-test,
]
needs: [changed-files-check, ui-task, ui-sb-build, ui-sb-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
+3 -18
View File
@@ -1,7 +1,6 @@
name: 'Preview Environment Dispatch'
permissions:
contents: write
permissions: {}
on:
pull_request_target:
@@ -11,7 +10,6 @@ on:
- packages/twenty-server/**
- packages/twenty-front/**
- .github/workflows/preview-env-dispatch.yaml
- .github/workflows/preview-env-keepalive.yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -35,28 +33,15 @@ jobs:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Trigger preview environment workflow
- name: Dispatch preview-env to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPOSITORY: ${{ github.repository }}
run: |
gh api repos/"$REPOSITORY"/dispatches \
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=preview-environment \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \
-f "client_payload[repo_full_name]=$REPOSITORY"
- name: Dispatch to ci-privileged for PR comment
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
KEEPALIVE_DISPATCH_TIME: ${{ github.event.pull_request.updated_at }}
REPOSITORY: ${{ github.repository }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=preview-env-url \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[keepalive_dispatch_time]=$KEEPALIVE_DISPATCH_TIME" \
-f "client_payload[repo]=$REPOSITORY"
@@ -1,186 +0,0 @@
name: 'Preview Environment Keep Alive'
permissions:
contents: read
on:
repository_dispatch:
types: [preview-environment]
jobs:
preview-environment:
timeout-minutes: 310
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.event.client_payload.pr_head_sha }}
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Run compose setup
run: |
echo "Patching docker-compose.yml..."
# change image to localbuild using yq
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
echo "Setting up .env file..."
cp packages/twenty-docker/.env.example packages/twenty-docker/.env
echo "Generating secrets..."
echo "" >> packages/twenty-docker/.env
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
echo "APP_SECRET=$(openssl rand -base64 32)" >> packages/twenty-docker/.env
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> packages/twenty-docker/.env
echo "SIGN_IN_PREFILLED=true" >> packages/twenty-docker/.env
echo "Docker compose build..."
cd packages/twenty-docker/
docker compose build
working-directory: ./
- name: Create Tunnel
id: expose-tunnel
env:
CLOUDFLARED_VERSION: '2026.3.0'
run: |
set -euo pipefail
# Install cloudflared (pinned for reproducibility)
sudo curl -fsSL -o /usr/local/bin/cloudflared \
"https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64"
sudo chmod +x /usr/local/bin/cloudflared
cloudflared --version
# Start an account-less "quick tunnel" pointing at the server container.
# Cloudflare prints the assigned https://*.trycloudflare.com URL into the log.
log_file="$RUNNER_TEMP/cloudflared.log"
: > "$log_file"
cloudflared tunnel \
--url http://localhost:3000 \
--no-autoupdate \
--logfile "$log_file" \
--loglevel info \
> "$RUNNER_TEMP/cloudflared.stdout" 2>&1 &
pid=$!
echo "$pid" > "$RUNNER_TEMP/cloudflared.pid"
echo "cloudflared PID: $pid"
# Wait up to 2 minutes for the URL to appear; fail fast if cloudflared exits.
url=''
for _ in $(seq 1 60); do
url=$(grep -oE 'https://[a-zA-Z0-9-]+\.trycloudflare\.com' "$log_file" 2>/dev/null | head -n1 || true)
[ -n "$url" ] && break
if ! kill -0 "$pid" 2>/dev/null; then
echo "cloudflared exited before producing a URL"
cat "$log_file" || true
exit 1
fi
sleep 2
done
if [ -z "$url" ]; then
echo "Timed out waiting for tunnel URL"
cat "$log_file" || true
exit 1
fi
echo "Tunnel URL: $url"
echo "tunnel-url=$url" >> "$GITHUB_OUTPUT"
- name: Start services with correct SERVER_URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
cd packages/twenty-docker/
echo "Setting SERVER_URL to $TUNNEL_URL"
sed -i '/SERVER_URL=/d' .env
echo "" >> .env
echo "SERVER_URL=$TUNNEL_URL" >> .env
# Start the services
echo "Docker compose up..."
docker compose up -d || {
echo "Docker compose failed to start"
docker compose logs
exit 1
}
echo "Waiting for services to be ready..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ] || [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
sleep 5
count=$((count+1))
if [ $count -gt 60 ]; then
echo "Timeout waiting for services to be ready"
docker compose logs
exit 1
fi
echo "Still waiting for services... ($count/60)"
done
echo "All services are up and running!"
working-directory: ./
- name: Seed Dev Workspace
run: |
cd packages/twenty-docker/
echo "Seeding light dev workspace (Apple only)..."
if ! docker compose exec -T server yarn command:prod workspace:seed:dev --light; then
echo "❌ Seeding light dev workspace failed. Dumping server logs..."
docker compose logs server
exit 1
fi
working-directory: ./
- name: Output tunnel URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
echo "✅ Preview Environment Ready!"
echo "🔗 Preview URL: $TUNNEL_URL"
echo "⏱️ This environment will be available for 5 hours"
echo "## 🚀 Preview Environment Ready!" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Preview URL: $TUNNEL_URL" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "This environment will automatically shut down after 5 hours." >> "$GITHUB_STEP_SUMMARY"
echo "$TUNNEL_URL" > tunnel-url.txt
- name: Upload tunnel URL artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: tunnel-url
path: tunnel-url.txt
retention-days: 1
- name: Keep tunnel alive for 5 hours
run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)
- name: Cleanup
if: always()
run: |
if [ -f "$RUNNER_TEMP/cloudflared.pid" ]; then
kill "$(cat "$RUNNER_TEMP/cloudflared.pid")" 2>/dev/null || true
fi
cd packages/twenty-docker/
docker compose down -v
working-directory: ./
@@ -1,52 +1,33 @@
name: Visual Regression Dispatch
# Uses workflow_run to dispatch visual regression to ci-privileged.
# This runs in the context of the base repo (not the fork), so it has
# access to secrets — making it work for external contributor PRs.
# Dispatches visual regression processing to ci-privileged after CI completes.
# Runs in the context of the base repo (not the fork) so it has access to secrets,
# making it work for external contributor PRs.
on:
workflow_run:
workflows: ['CI Front', 'CI UI']
workflows: ['CI UI']
types: [completed]
permissions:
actions: read
contents: read
pull-requests: read
jobs:
dispatch:
dispatch-pr:
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Determine project and artifact name
id: project
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const workflowName = context.payload.workflow_run.name;
if (workflowName === 'CI Front') {
core.setOutput('project', 'twenty-front');
core.setOutput('artifact_name', 'storybook-static');
core.setOutput('tarball_name', 'storybook-twenty-front-tarball');
core.setOutput('tarball_file', 'storybook-twenty-front.tar.gz');
} else if (workflowName === 'CI UI') {
core.setOutput('project', 'twenty-ui');
core.setOutput('artifact_name', 'storybook-twenty-ui');
core.setOutput('tarball_name', 'storybook-twenty-ui-tarball');
core.setOutput('tarball_file', 'storybook-twenty-ui.tar.gz');
} else {
core.setFailed(`Unexpected workflow: ${workflowName}`);
}
- name: Check if storybook artifact exists
- name: Check if screenshots artifact exists
id: check-artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const artifactName = '${{ steps.project.outputs.artifact_name }}';
const artifactName = 'argos-screenshots-twenty-ui';
const runId = context.payload.workflow_run.id;
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
@@ -59,7 +40,7 @@ jobs:
core.setOutput('exists', found ? 'true' : 'false');
if (!found) {
core.info(`Artifact "${artifactName}" not found in run ${runId} — storybook build was likely skipped`);
core.info(`Artifact "${artifactName}" not found in run ${runId} — skipping`);
}
- name: Get PR number
@@ -71,8 +52,6 @@ jobs:
const headBranch = context.payload.workflow_run.head_branch;
const headRepo = context.payload.workflow_run.head_repository;
// workflow_run.pull_requests is empty for fork PRs,
// so fall back to searching by head label (owner:branch)
let pullRequests = context.payload.workflow_run.pull_requests;
let prNumber;
@@ -80,7 +59,7 @@ jobs:
prNumber = pullRequests[0].number;
} else {
const headLabel = `${headRepo.owner.login}:${headBranch}`;
core.info(`pull_requests is empty (likely a fork PR), searching by head label: ${headLabel}`);
core.info(`Searching for PR by head label: ${headLabel}`);
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
@@ -96,7 +75,7 @@ jobs:
}
if (!prNumber) {
core.info('No pull request found for this workflow run — skipping');
core.info('No pull request found — skipping');
core.setOutput('has_pr', 'false');
return;
}
@@ -105,43 +84,100 @@ jobs:
core.setOutput('has_pr', 'true');
core.info(`PR #${prNumber}`);
- name: Download storybook artifact from triggering run
- name: Compute merge-base for Argos reference
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
id: merge-base
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
name: ${{ steps.project.outputs.artifact_name }}
path: storybook-static
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
script: |
const headSha = context.payload.workflow_run.head_sha;
- name: Package storybook
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
run: tar -czf /tmp/${{ steps.project.outputs.tarball_file }} -C storybook-static .
try {
const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
owner: context.repo.owner,
repo: context.repo.repo,
basehead: `main...${headSha}`,
});
- name: Upload storybook tarball
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ steps.project.outputs.tarball_name }}
path: /tmp/${{ steps.project.outputs.tarball_file }}
retention-days: 1
if (comparison.merge_base_commit?.sha) {
core.setOutput('sha', comparison.merge_base_commit.sha);
core.info(`Merge base: ${comparison.merge_base_commit.sha}`);
} else {
core.info('Could not determine merge base — will skip reference_commit');
core.setOutput('sha', '');
}
} catch (error) {
core.warning(`Failed to compute merge base: ${error instanceof Error ? error.message : String(error)}`);
core.setOutput('sha', '');
}
- name: Dispatch to ci-privileged
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
RUN_ID: ${{ github.run_id }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
REPOSITORY: ${{ github.repository }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
REFERENCE_COMMIT: ${{ steps.merge-base.outputs.sha }}
run: |
ARGS=(
--method POST
-f event_type=visual-regression
-f "client_payload[pr_number]=$PR_NUMBER"
-f "client_payload[run_id]=$WORKFLOW_RUN_ID"
-f "client_payload[repo]=$REPOSITORY"
-f "client_payload[branch]=$BRANCH"
-f "client_payload[commit]=$COMMIT"
)
if [ -n "$REFERENCE_COMMIT" ]; then
ARGS+=(-f "client_payload[reference_commit]=$REFERENCE_COMMIT")
fi
gh api repos/twentyhq/ci-privileged/dispatches "${ARGS[@]}"
dispatch-main:
if: >-
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check if screenshots artifact exists
id: check-artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const runId = context.payload.workflow_run.id;
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId,
});
const found = artifacts.artifacts.some(a => a.name === 'argos-screenshots-twenty-ui');
core.setOutput('exists', found ? 'true' : 'false');
if (!found) {
core.info(`Artifact not found in run ${runId} — skipping`);
}
- name: Dispatch to ci-privileged
if: steps.check-artifact.outputs.exists == 'true'
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
REPOSITORY: ${{ github.repository }}
PROJECT: ${{ steps.project.outputs.project }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
--method POST \
-f event_type=visual-regression \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[run_id]=$RUN_ID" \
-f "client_payload[run_id]=$WORKFLOW_RUN_ID" \
-f "client_payload[repo]=$REPOSITORY" \
-f "client_payload[project]=$PROJECT" \
-f "client_payload[branch]=$BRANCH" \
-f "client_payload[commit]=$COMMIT"
+2
View File
@@ -51,7 +51,9 @@ dump.rdb
mcp.json
/.junie/
/.agents/plugins/marketplace.json
TRANSLATION_QA_REPORT.md
.playwright-mcp/
.playwright-cli/
output/playwright/
screenshots/
+159
View File
@@ -0,0 +1,159 @@
# Twenty Website — DESIGN.md
> Visual system for the Twenty marketing site. Distilled from `packages/twenty-website/src/theme/`. Loaded by every `impeccable` invocation alongside PRODUCT.md.
## Theme
**Light by default.** A founder browsing a partner profile in daylight on a 1427 inch monitor is the default scene. The site does ship a `data-scheme="dark"` override (see `css-variables.ts`), but no current public page opts into it. Treat dark as a deferred surface.
## Color
Palette is OKLCH-equivalent neutrals at the surface level. The brand accents (blue, pink, yellow, green) are present in the token system but used sparingly — none of them appear on the partner pages.
### Strategy: Restrained
Tinted neutrals + one accent ≤10%. The accent for partner pages is the deep ink black (`var(--color-black-100)`) used in CTAs and hover states. Anything beyond a hairline border, an icon glyph, or a primary CTA should question whether it needs color at all.
### Tokens (from `src/theme/colors.ts` + `css-variables.ts`)
Neutrals (the workhorses):
| Token | Hex (computed) | Role |
| --- | --- | --- |
| `colors.primary.background[100]` | `#ffffff` | Page + card surface |
| `colors.primary.text[100]` | `#1c1c1c` | Headlines, primary text |
| `colors.primary.text[80]` | `#1c1c1ccc` | Body text |
| `colors.primary.text[60]` | `#1c1c1c99` | Eyebrows, meta, captions |
| `colors.primary.text[40]` | `#1c1c1c66` | Disabled / placeholder |
| `colors.primary.text[20]` | `#1c1c1c33` | Subtle separators |
| `colors.primary.text[10]` | `#1c1c1c1a` | Hairline borders |
| `colors.primary.text[5]` | `#1c1c1c0d` | Subtle fills (rates panel, skill chips) |
| `colors.primary.border[10]` | `#1c1c1c1a` | Default border |
| `colors.primary.border[20]` | `#1c1c1c33` | Hover border |
Reverse palette (for dark CTAs):
| Token | Role |
| --- | --- |
| `colors.secondary.background[100]` | Filled CTA background (deep ink) |
| `colors.secondary.text[100]` | Filled CTA text (white) |
Brand accents (currently absent from partner pages; available if needed):
- `colors.accent.blue``#4a38f5` / `#8174f8`
- `colors.accent.pink``#ed87fc` / `#f3abfd`
- `colors.accent.yellow``#feffb7` / `#feffd9`
- `colors.accent.green``#89fc9a` / `#b0fdbe`
- `colors.highlight` — same hue as blue accent
**Do not introduce gradients, glass blurs, or saturated fills on partner pages.** Color is conviction here, not decoration.
## Typography
Three families, each load-balanced via CSS variables:
| Family | Var | Use |
| --- | --- | --- |
| `theme.font.family.serif` | `--font-serif` | Headlines, partner names, headline values |
| `theme.font.family.sans` | `--font-sans` | Body, prose, interactive labels |
| `theme.font.family.mono` | `--font-mono` | Eyebrows, meta, currency labels, tabular numerics |
| `theme.font.family.retro` | `--font-retro` | Reserved (not used on partner pages) |
### Weight + Size Contrast
Weights: `light: 300`, `regular: 400`, `medium: 500`. No bold. Hierarchy is driven by scale and family contrast, never by weight alone.
Scale (`theme.font.size(n)``calc(var(--font-base) * n)`, where `--font-base: 0.25rem` ≈ 4px):
- Display / h1: size 912 (3648px)
- h2 / section heads: size 78 (2832px)
- h3 / card heads: size 56 (2024px)
- Body / prose: size 45 (1620px)
- Eyebrow / meta: size 3 (12px) with `letter-spacing: 0.060.08em` and `text-transform: uppercase`
Body line length: cap at 6575ch (the existing `PartnerProfileIntro` uses `max-width: 62ch` — keep that order of magnitude).
### Hierarchy contract
- A serif `<h1>` at size 9 light reads as a partner's name on the detail page.
- A mono eyebrow above or below it locates the partner (region · city · country).
- A serif size 6 light reads as a section head.
- Body prose is sans regular.
- Currency values are serif (they read as headline numbers, not stats).
- Currency labels and meta are mono.
## Spacing & Layout
Base unit `4px`. Spacing helper `theme.spacing(n)` returns `n * 4px`. Common rhythms on the partner pages:
- Inter-section gap on the detail page: `theme.spacing(1014)` — generous, editorial breathing room.
- Inter-element gap inside a section: `theme.spacing(35)`.
- Card padding: `theme.spacing(6)`.
- Page horizontal padding: `theme.spacing(4)` mobile, `theme.spacing(10)` ≥ md breakpoint.
### Radius
`theme.radius(n)` returns `n * 2px`. The default card radius is `theme.radius(2)` = 4px. Pills use `999px`. No softer rounding than that.
### Borders
Borders are hairline (`1px solid theme.colors.primary.border[10]`). They define edges quietly. On hover they step to `border[20]`. Never use a chunky border as decoration.
## Components
### Card (PartnerCard, RatesPanel)
White surface, hairline border, 4px radius, 24px padding, soft shadow on hover only:
```css
background-color: ${theme.colors.primary.background[100]};
border: 1px solid ${theme.colors.primary.border[10]};
border-radius: ${theme.radius(2)};
padding: ${theme.spacing(6)};
&:hover {
border-color: ${theme.colors.primary.border[20]};
box-shadow: 0 12px 32px -16px rgba(0, 0, 0, 0.18);
transform: translateY(-2px);
}
```
### Chip / Pill
Rounded `999px`, 1px border, subtle background fill (`primary.text[5]` for filter pills, transparent for chip rows), `text[80]` color, mono or sans font.
### Button / LinkButton
Lives in `@/design-system/components`. Two color modes: `primary` (deep ink fill, white text) and `secondary` (transparent fill, ink text + 1px border). `variant="contained"` is what partner pages use.
### Avatar
`PartnerAvatar` is a deterministic generated mark from name + slug. Used as fallback when `profilePictureUrl` is missing. The real photo overlays it at 120px circle on the detail page, 56px on the list card.
## Motion
- Hover transitions: 250ms, ease-out (cubic-bezier curve in `PartnerCard`: `0.25s ease`).
- Card entrance: 700ms cubic-bezier `0.22, 1, 0.36, 1` (ease-out-quart), 90ms stagger per index.
- All motion respects `@media (prefers-reduced-motion: reduce)` — animations stop, hover translate disabled.
- **No bounce, no elastic, no parallax.** Editorial restraint.
## Iconography
`@tabler/icons-react`, 1416px on body-level chips, 1824px on buttons. Always `aria-hidden="true"` when decorative. Stroke width `2` (default).
## Accessibility Defaults
- Focus ring: `outline: 2px solid theme.colors.primary.text[100]; outline-offset: 4px` (already used on the card link).
- Touch target ≥ 40×40px on mobile.
- `aria-label` on icon-only buttons, `aria-labelledby` on sectioned regions.
- All `<a target="_blank">` includes `rel="noopener noreferrer"`.
- Color is never the sole carrier of meaning. The money pills carry both an icon and a text label.
## Anti-patterns (project-specific)
In addition to the impeccable shared absolute bans:
- **Do not use the brand accent colors (blue/pink/yellow/green) on partner pages** unless we have a stronger reason than "to add color".
- **No skeuomorphic shadows on cards.** The hover shadow is `0 12px 32px -16px rgba(0,0,0,0.18)` — that's the ceiling.
- **No gradients on anything.** Including text, borders, and backgrounds.
- **No floating "Trusted by" logo bars** on partner pages.
+80
View File
@@ -0,0 +1,80 @@
# Twenty Website — Product & Brand Context
> Strategic context for design work on the Twenty marketing site (`packages/twenty-website`). Loaded by every `impeccable` invocation.
## Register
**Brand.** The marketing site is a public-facing surface where the design itself is part of the credibility argument. Prospects evaluate Twenty partly by how the site feels. The product app (`packages/twenty-front`) is a separate product-register surface, governed elsewhere.
## Users & Purpose
The primary audience varies by route, but the working assumption for partner-related pages is:
- **Who:** A budget-holding decision maker (founder, RevOps lead, or COO) shopping for a CRM implementation partner. Already on Twenty's site, evaluating a shortlist of partners.
- **Context:** Doing a side-by-side comparison across 25 candidates over a single browsing session. Will spend 3090 seconds on each profile before deciding whether to book a call.
- **Decision being made:** "Is this partner credible, the right size, the right specialty, and within budget? Do I trust them enough to commit 30 minutes to a discovery call?"
What the partner pages must do, in priority order:
1. Communicate credibility (real firm, real person, real work).
2. Surface fit signals fast (skills, region, languages, deployment expertise, budget range).
3. Give the visitor a confident "next step" affordance (book a call or vet via LinkedIn) without pressure.
## Desired Outcome
The redesign should make `/partners/profile/[slug]` feel like a *thoughtfully curated profile of a top-tier partner*, not a generic templated card. A visitor should leave thinking "this firm is serious" even if they don't book a call this session.
Specifically:
- **Confidence over information density.** A short, well-typeset profile beats a packed-but-busy one.
- **Editorial restraint.** White space, deliberate type hierarchy, and a few well-chosen details say more than dozens of small components.
- **Quiet conviction.** No hype copy, no growth-hack patterns, no "Trusted by" logo strips. The partner's own work and intro speak for themselves.
## Brand Personality
**Editorial · Founder-led · Considered.**
The site reads like a thoughtful indie publication, not a SaaS landing page. Serif headlines, plenty of whitespace, deliberate typographic rhythm. Quietly opinionated — Twenty has a point of view about CRM (open-source, customizable, well-designed) and the site reflects that without shouting.
Tonal anchors:
- Stripe's documentation for clarity, Linear's marketing for restraint, an editorial print magazine for typography choices.
## Anti-references
**Reject these patterns. They make the work read as generic AI / generic SaaS:**
- **Generic SaaS landing.** Big-number heroes, identical icon-grid cards, gradient text, navy + lime accent color schemes, "supercharge your workflow" language.
- **Corporate enterprise tone.** Stock photos of diverse handshakes. "Trusted by Fortune 500" logo strips as the primary credibility move. Trust-badge bars.
- **Bento templates.** Repetitive same-size cards. Vercel-style scroll-pin animations on every section.
- **Side-stripe borders, gradient text, glassmorphism, hero-metric templates, identical card grids** — see impeccable's shared absolute bans.
## Strategic Design Principles
1. **Typography carries the design.** The brand has a serif/sans/mono trio. Hierarchy is set by scale + weight contrast, not by color or borders.
2. **Restrained palette.** Tinted neutrals (black/white via CSS variables, with alpha-tone variants for text and borders) carry 90%+ of the surface. Accent color used sparingly when it appears at all.
3. **Whitespace is a feature.** Tight cards feel cheap. Pages should breathe.
4. **Asymmetry over grid.** A 12-col bento is the wrong shape for a profile page. Use asymmetric two-column layouts where one column does heavy lifting.
5. **One opinionated detail per page.** Each surface should have one moment of editorial conviction (a typographic flourish, a precise micro-interaction, a deliberate space) rather than five generic flourishes.
## Accessibility
**WCAG AA + keyboard + screen reader baseline:**
- All interactive elements reachable by keyboard, focus visible (`outline: 2px solid`, not just color shift).
- Semantic landmarks: `<header>`, `<main>`, `<nav>`, `<section aria-labelledby=…>`, headings in order.
- All images with informational content have alt text. Decorative icons have `aria-hidden="true"`.
- Body text ≥ 4.5:1 contrast; large text (≥18pt or 14pt bold) ≥ 3:1.
- Respect `prefers-reduced-motion`. Animations stop, don't slow.
- Forms have explicit labels. Errors are announced.
## Tech & Constraints
- Next.js 16 app router (Server Components by default, `'use client'` for interactivity).
- Linaria styled-components (`@linaria/react`) for zero-runtime CSS-in-JS.
- Lingui (`@lingui/react`) for i18n; never hardcode user-visible strings.
- Theme tokens in `packages/twenty-website/src/theme/`. Colors are CSS variables resolved to OKLCH-tinted neutrals.
- `@tabler/icons-react` for iconography (no Heroicons, no custom SVGs unless purposeful).
- `@radix-ui/react-*` for primitives (Popover etc) where headless behavior is needed.
## Out of Scope for This File
- Detailed visual tokens (colors, type scale, motion specs) live in `DESIGN.md`.
- Per-page IA decisions live in shape briefs (`docs/superpowers/specs/`).
+1 -6
View File
@@ -147,13 +147,9 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
- <a href="https://nestjs.com/"><img src="./packages/twenty-website/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
- <a href="https://reactjs.org/"><img src="./packages/twenty-website/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
# Thanks
<p align="center">
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.webp" height="28" alt="Chromatic" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.webp" height="28" alt="Greptile" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.webp" height="28" alt="Sentry" /></a>
@@ -161,8 +157,7 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.webp" height="28" alt="Crowdin" /></a>
</p>
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
# Join the Community
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

-19
View File
@@ -220,25 +220,6 @@
}
}
},
"chromatic": {
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"commands": [
{
"command": "nx storybook:build {projectName}",
"forwardAllArgs": false
},
"chromatic --storybook-build-dir=storybook-static {args.ci}"
],
"parallel": false
},
"configurations": {
"ci": {
"ci": "--exit-zero-on-changes"
}
}
},
"@nx/jest:jest": {
"cache": true,
"inputs": [
+3 -1
View File
@@ -28,7 +28,7 @@
"resolutions": {
"graphql": "16.8.1",
"type-fest": "4.10.1",
"typescript": "5.9.2",
"typescript": "5.9.3",
"nodemailer": "8.0.4",
"graphql-redis-subscriptions/ioredis": "^5.6.0",
"@lingui/core": "5.1.2",
@@ -52,6 +52,7 @@
"packages/twenty-server",
"packages/twenty-emails",
"packages/twenty-ui",
"packages/twenty-new-ui",
"packages/twenty-utils",
"packages/twenty-zapier",
"packages/twenty-website",
@@ -63,6 +64,7 @@
"packages/twenty-client-sdk",
"packages/twenty-cli",
"packages/create-twenty-app",
"packages/twenty-codex-plugin",
"packages/twenty-oxlint-rules",
"packages/twenty-companion",
"packages/twenty-claude-skills"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "2.7.0",
"version": "2.10.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -50,7 +50,7 @@
"jest": "29.7.0",
"jest-environment-node": "^29.4.1",
"twenty-shared": "workspace:*",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"vite": "^7.0.0",
"vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^4.2.1"
@@ -17,8 +17,7 @@
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "TO-BE-GENERATED",
"twenty-sdk": "TO-BE-GENERATED"
"twenty-client-sdk": "TO-BE-GENERATED"
},
"devDependencies": {
"@types/node": "^24.7.2",
@@ -26,6 +25,7 @@
"oxlint": "^0.16.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"twenty-sdk": "TO-BE-GENERATED",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^3.1.1"
@@ -33,9 +33,11 @@ const TEMPLATE_PACKAGE_JSON = {
license: 'MIT',
scripts: { twenty: 'twenty' },
dependencies: {
'twenty-sdk': '0.0.0',
'twenty-client-sdk': '0.0.0',
},
devDependencies: {
'twenty-sdk': '0.0.0',
},
};
describe('copyBaseApplicationProject', () => {
@@ -147,7 +149,7 @@ describe('copyBaseApplicationProject', () => {
join(testAppDirectory, 'package.json'),
);
expect(packageJson.name).toBe('my-test-app');
expect(packageJson.dependencies['twenty-sdk']).toBe(
expect(packageJson.devDependencies['twenty-sdk']).toBe(
createTwentyAppPackageJson.version,
);
expect(packageJson.dependencies['twenty-client-sdk']).toBe(
@@ -120,7 +120,8 @@ const updatePackageJson = async ({
const packageJson = await fs.readJson(join(appDirectory, 'package.json'));
packageJson.name = appName;
packageJson.dependencies['twenty-sdk'] = createTwentyAppPackageJson.version;
packageJson.devDependencies['twenty-sdk'] =
createTwentyAppPackageJson.version;
packageJson.dependencies['twenty-client-sdk'] =
createTwentyAppPackageJson.version;
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '7d3f2b9e-5c0a-4e8b-ad6e-2f9c3b4d5e6a',
frontComponentUniversalIdentifier: '6c2f1a8d-4b9e-4f7a-9c5d-1e8b2a3d4c5f',
label: 'Contributor Stats',
icon: 'IconChartBar',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '4640992f-c2c9-4bba-b5df-9c8f05dc9e80',
frontComponentUniversalIdentifier: '08f40f82-24ed-4f3e-8c99-695151e90e38',
label: 'Fetch Contributors',
icon: 'IconUsers',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
});
@@ -1,10 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
useRecordId,
} from 'twenty-sdk/front-component';
import { enqueueSnackbar, useRecordId } from 'twenty-sdk/front-component';
import { THEME } from 'src/modules/github/contributor/components/theme';
import { callAppRoute } from 'src/modules/shared/call-app-route';
@@ -682,12 +678,4 @@ export default defineFrontComponent({
description:
'Displays time-bucketed charts of PRs authored (merged only), merged and reviewed by a contributor over the last week, month, 3 months or year.',
component: ContributorStats,
command: {
universalIdentifier: '7d3f2b9e-5c0a-4e8b-ad6e-2f9c3b4d5e6a',
label: 'Contributor Stats',
icon: 'IconChartBar',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
},
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -95,12 +94,4 @@ export default defineFrontComponent({
'Fetches contributors from every configured GitHub repo (GITHUB_REPOS) and upserts them as Contributor records.',
isHeadless: true,
component: FetchContributors,
command: {
universalIdentifier: '4640992f-c2c9-4bba-b5df-9c8f05dc9e80',
label: 'Fetch Contributors',
icon: 'IconUsers',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
},
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'c34f56aa-ff65-43a4-9db2-774945dbcc53',
frontComponentUniversalIdentifier: '9430e4fc-9ecb-428d-9bde-2babeb1f452f',
label: 'Fetch Issues',
icon: 'IconBug',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'issue',
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -98,12 +97,4 @@ export default defineFrontComponent({
description: 'Fetches issues from GitHub repos',
isHeadless: true,
component: FetchIssues,
command: {
universalIdentifier: 'c34f56aa-ff65-43a4-9db2-774945dbcc53',
label: 'Fetch Issues',
icon: 'IconBug',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'issue',
},
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '719cfe1c-d570-4c8c-89e6-88671c6ba1ea',
frontComponentUniversalIdentifier: '7c397b0c-8b19-4fac-924a-8f6aa1dece78',
label: 'Fetch Project Items',
icon: 'IconLayoutKanban',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'projectItem',
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -97,12 +96,4 @@ export default defineFrontComponent({
description: 'Fetches project items from GitHub Projects V2',
isHeadless: true,
component: FetchProjectItems,
command: {
universalIdentifier: '719cfe1c-d570-4c8c-89e6-88671c6ba1ea',
label: 'Fetch Project Items',
icon: 'IconLayoutKanban',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'projectItem',
},
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '0b24e6d6-da0c-4c0e-8d88-5bfd7f3cd75a',
frontComponentUniversalIdentifier: '5e2ba8df-1db5-4964-94d3-44cccfd791a0',
label: 'Fetch Pull Requests',
icon: 'IconGitPullRequest',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'pullRequest',
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -101,12 +100,4 @@ export default defineFrontComponent({
description: 'Fetches pull requests and reviews from GitHub repos',
isHeadless: true,
component: FetchPrs,
command: {
universalIdentifier: '0b24e6d6-da0c-4c0e-8d88-5bfd7f3cd75a',
label: 'Fetch Pull Requests',
icon: 'IconGitPullRequest',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'pullRequest',
},
});
@@ -80,8 +80,8 @@ export async function teardown() {
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
throw new Error(
`App uninstall failed during teardown: ${JSON.stringify(uninstallResult.error, null, 2)}`,
);
}
}
@@ -0,0 +1,17 @@
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
@@ -3,7 +3,7 @@ import { NavigationMenuItemType } from 'twenty-shared/types';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineNavigationMenuItem({
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
universalIdentifier: 'e8031eca-d6ea-4a4b-b828-38227dba896a',
position: 0,
type: NavigationMenuItemType.OBJECT,
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
@@ -0,0 +1,10 @@
import { defineViewField } from 'twenty-sdk/define';
import { ALL_POST_CARDS_VIEW_ID } from '../views/all-post-cards.view';
export default defineViewField({
fieldMetadataUniversalIdentifier: '7b57bd63-5a4c-46ca-9d52-42c8f02d1df6',
position: 5,
universalIdentifier: 'cd582d11-ea21-4dc3-b9c1-0298ce3b6b54',
viewUniversalIdentifier: ALL_POST_CARDS_VIEW_ID,
isVisible: true,
});
@@ -43,7 +43,7 @@ A brief note on the overall tone of the conversation (collaborative, tense, expl
- If the transcript is very short (under 200 words), provide a brief 2-3 sentence summary instead of the full structure.
## Saving the Summary
After generating the summary, use \`update_callRecording\` to save it in the \`summary\` field with the format:
After generating the summary, use \`update_one_call_recording\` to save it in the \`summary\` field with the format:
\`\`\`json
{ "summary": { "blocknote": null, "markdown": "<your summary>" } }
\`\`\`
@@ -0,0 +1,32 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn
# codegen
generated
# dev
/dist/
.twenty
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files
.env*
# typescript
*.tsbuildinfo
*.d.ts
@@ -0,0 +1,70 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": [
"warn",
{ "allow": ["group", "groupCollapsed", "groupEnd"] }
],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"typescript/no-explicit-any": "off"
},
"overrides": [
{
"files": ["**/*.logic-function.ts", "**/logic-functions/**/*.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["twenty-shared", "twenty-shared/*"],
"message": "Logic functions must not import from twenty-shared directly. Import runtime types and helpers from `twenty-sdk/logic-function` instead so the logic-function bundle stays minimal."
}
]
}
]
}
}
]
}
@@ -0,0 +1 @@
nodeLinker: node-modules
@@ -0,0 +1,114 @@
# People Data Labs enrichment app
Enriches **Person** and **Company** records with [People Data Labs](https://www.peopledatalabs.com/) (PDL) data.
> **Status: data-model scaffold.** This package defines the fields, relation, indexes,
> views, role, and manifest. The enrichment **logic function (the "mapper") is not yet
> implemented** — see [What the mapper must do](#what-the-mapper-must-do).
---
## Data-model decisions
### Bundle scope
Only the core PDL company fields are defined. Premium / Comprehensive / specialized fields
(`inferred_revenue`, `linkedin_follower_count`, employee growth/churn/tenure, parent /
subsidiary, exec movement, top employers, `funding_details`, …) are **out of scope** for this app.
### Enums → SELECT / MULTI_SELECT
Every PDL enum that has a canonical file is a SELECT, **validated 0-missing/0-extra against
PDL schema v34.1**:
| Field | Type | Options |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------ |
| `pdlSeniority` (`job_title_levels`, array) | MULTI_SELECT | 10 |
| `pdlFundingStages` (`funding_stages`, array) | MULTI_SELECT | 29 |
| `pdlIndustry` / `pdlJobCompanyIndustry` (`industry`) | SELECT | 147 |
| `pdlJobTitleSubRole` (`job_title_sub_role`) | SELECT | 106 |
| `pdlJobTitleClass`, `pdlInferredSalary`, `pdlSex`, `pdlCompanyType`, `pdlSizeRange`, `pdlJobCompanySize`, `pdlLatestFundingStage`, `pdlLocationContinent`, `pdlLocationMetro`, `pdlMicExchange` | SELECT | 5 / 11 / 2 / 6 / 8 / 8 / 29 / 7 / 384 / 70 |
- Option `value`s are normalized to **GraphQL enum names** (`united states``UNITED_STATES`):
uppercase, accents stripped, non-alphanumeric → `_`, digit-leading prefixed.
- Option `universalIdentifier`s are **unique per field** (shared enums like industry get a
separate id-set per field).
- **Stays `TEXT`** (no canonical PDL enum file exists): `pdlIndustryDetail` (`industry_v2`),
`pdlJobOnetCode`, `pdlLocationRegion`.
### Standard-field mapping
`pdl*` shadows are **removed** where an equivalent standard field exists; the mapper writes
the standard field instead:
| Object | Removed shadow → standard target |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Person | `pdlLinkedinUrl``linkedinLink`, `pdlJobTitle``jobTitle`, `pdlFullName``name`, `pdlWorkEmail`/`pdlPersonalEmails``emails`, `pdlMobilePhone`/`pdlPhoneNumbers``phones` |
| Company | `pdlLinkedinUrl``linkedinLink`, `pdlWebsite``domainName`, `pdlDisplayName``name` |
Shadows are **kept** where no reliable standard field is available: `pdlEmployeeCount`,
`pdlTwitterUrl`.
_Trade-off:_ PDL's work/personal-email and mobile/other-phone distinction is dropped (folded
into the standard bags).
### Location → ADDRESS composite
- **Company** location → the **standard `address`** composite (street/city/state/postcode/country/geo).
- **Person** has no standard address field → dedicated **`pdlLocation` (ADDRESS)**.
- `pdlLocationMetro` (both) and `pdlLocationContinent` (company) stay SELECT — ADDRESS has no slot.
_Trade-off:_ ADDRESS `country` is free text, so the country SELECT was dropped.
### Relation
Dedicated **`pdlCurrentCompany`** (Person `MANY_TO_ONE` → Company) ↔ inverse
**`pdlCurrentEmployees`** (Company `ONE_TO_MANY` → Person). Deliberately **not** the standard
`company` relation, so PDL's detected employer can't overwrite the user's CRM account link.
### Enrichment metadata
- `pdlId` — PDL record id (re-enrich by id: more precise than by email).
- `pdlLikelihood` (Person, NUMBER) — PDL match confidence 110.
- `pdlEnrichmentStatus` (SELECT: `MATCHED` / `NOT_FOUND` / `ERROR`) — distinguishes
"no match" from "never tried" (drives re-enrichment scheduling).
- `pdlLastEnrichedAt` (DATE_TIME), `pdlRawPayload` (RAW_JSON, full response).
### Other
- `pdlTotalFunding` is `CURRENCY` (mapper must convert the bare USD float → micros).
- **Indexes**: `pdlId` and `pdlLastEnrichedAt` on both objects.
- **Views**: a curated "People Data Labs" TABLE view per object.
- **Role**: read/update on Person & Company (object-level; tighten to field-scoped later).
---
## What the mapper must do
The logic function (to be built) must:
**Orchestration**
1. Trigger via manual command-menu action / record create / batch (TBD).
2. Call PDL Person and/or Company Enrichment with `PDL_API_KEY`; pass `min_likelihood` /
`required_fields` to control match quality.
3. On `200` → write fields + set `pdlEnrichmentStatus = MATCHED`; on `404`
`NOT_FOUND`; on error → `ERROR`.
4. Respect PDL rate limits (queue / throttle on `429`).
5. **TTL guard**: skip re-enrichment if `pdlLastEnrichedAt` is recent; prefer re-enriching by `pdlId`.
**Field writing**
6. Write **standard fields** (fill-only-if-empty to avoid overwriting user data):
Person `name`, `emails`, `phones`, `linkedinLink`, `jobTitle`; Company `name`,
`domainName`, `linkedinLink`, `address`.
7. Write `pdl*` fields for everything else.
8. **SELECT guard**: only write a SELECT/MULTI_SELECT value if the normalized value exists in
the field's option set; otherwise skip and keep it in `pdlRawPayload` (handles PDL schema
versions newer than v34.1). Use the same normalization as the option `value`s.
9. **MULTI_SELECT** arrays: `job_title_levels``pdlSeniority`; `funding_stages``pdlFundingStages`.
10. **CURRENCY**: `total_funding_raised` (USD float) → `{ amountMicros: value × 1_000_000, currencyCode: 'USD' }`.
11. **ADDRESS**: split PDL `location.*` into the composite — Company → standard `address`,
Person → `pdlLocation`.
12. **Relation**: resolve `job_company_id` → find/upsert a Company record → link `pdlCurrentCompany`.
13. **Dates**: handle partial PDL dates (`YYYY`, `YYYY-MM`) for `job_start_date`,
`last_funding_date`, `birth_date`.
14. Always set `pdlId`, `pdlLastEnrichedAt`, `pdlRawPayload`, `pdlLikelihood` (person).
@@ -0,0 +1,36 @@
{
"name": "people-data-labs",
"version": "0.1.0",
"description": "People Data Labs enrichment app for Twenty",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"keywords": [
"twenty-app"
],
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "2.4.0",
"twenty-sdk": "2.4.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^19.0.0",
"oxlint": "^0.16.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^3.1.1"
}
}
@@ -0,0 +1,16 @@
import { defineApplication } from 'twenty-sdk/define';
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'People Data Labs',
description: 'Enrich People and Companies with People Data Labs data.',
serverVariables: {
PDL_API_KEY: {
description: 'People Data Labs API key',
isSecret: true,
isRequired: true,
},
},
});
@@ -0,0 +1,19 @@
import { type SelectOptionMeta } from 'src/types/select-option-meta.type';
export const ENRICHMENT_STATUS_OPTIONS: readonly SelectOptionMeta[] = [
{
key: 'matched',
value: 'MATCHED',
label: 'Matched',
color: 'green',
position: 0,
},
{
key: 'notFound',
value: 'NOT_FOUND',
label: 'No Match',
color: 'gray',
position: 1,
},
{ key: 'error', value: 'ERROR', label: 'Error', color: 'red', position: 2 },
];
@@ -0,0 +1,195 @@
import { type SelectOptionMeta } from 'src/types/select-option-meta.type';
export const FUNDING_STAGE_OPTIONS: readonly SelectOptionMeta[] = [
{
key: 'angel',
value: 'ANGEL',
label: 'Angel',
color: 'blue',
position: 0,
},
{
key: 'convertibleNote',
value: 'CONVERTIBLE_NOTE',
label: 'Convertible Note',
color: 'red',
position: 1,
},
{
key: 'corporateRound',
value: 'CORPORATE_ROUND',
label: 'Corporate Round',
color: 'green',
position: 2,
},
{
key: 'debtFinancing',
value: 'DEBT_FINANCING',
label: 'Debt Financing',
color: 'orange',
position: 3,
},
{
key: 'equityCrowdfunding',
value: 'EQUITY_CROWDFUNDING',
label: 'Equity Crowdfunding',
color: 'purple',
position: 4,
},
{
key: 'fundingRound',
value: 'FUNDING_ROUND',
label: 'Funding Round',
color: 'yellow',
position: 5,
},
{ key: 'grant', value: 'GRANT', label: 'Grant', color: 'pink', position: 6 },
{
key: 'initialCoinOffering',
value: 'INITIAL_COIN_OFFERING',
label: 'Initial Coin Offering',
color: 'cyan',
position: 7,
},
{
key: 'nonEquityAssistance',
value: 'NON_EQUITY_ASSISTANCE',
label: 'Non-Equity Assistance',
color: 'brown',
position: 8,
},
{
key: 'postIpoDebt',
value: 'POST_IPO_DEBT',
label: 'Post-IPO Debt',
color: 'lime',
position: 9,
},
{
key: 'postIpoEquity',
value: 'POST_IPO_EQUITY',
label: 'Post-IPO Equity',
color: 'violet',
position: 10,
},
{
key: 'postIpoSecondary',
value: 'POST_IPO_SECONDARY',
label: 'Post-IPO Secondary',
color: 'gold',
position: 11,
},
{
key: 'preSeed',
value: 'PRE_SEED',
label: 'Pre-Seed',
color: 'turquoise',
position: 12,
},
{
key: 'privateEquity',
value: 'PRIVATE_EQUITY',
label: 'Private Equity',
color: 'crimson',
position: 13,
},
{
key: 'productCrowdfunding',
value: 'PRODUCT_CROWDFUNDING',
label: 'Product Crowdfunding',
color: 'sky',
position: 14,
},
{
key: 'secondaryMarket',
value: 'SECONDARY_MARKET',
label: 'Secondary Market',
color: 'amber',
position: 15,
},
{ key: 'seed', value: 'SEED', label: 'Seed', color: 'plum', position: 16 },
{
key: 'seriesA',
value: 'SERIES_A',
label: 'Series A',
color: 'grass',
position: 17,
},
{
key: 'seriesB',
value: 'SERIES_B',
label: 'Series B',
color: 'tomato',
position: 18,
},
{
key: 'seriesC',
value: 'SERIES_C',
label: 'Series C',
color: 'iris',
position: 19,
},
{
key: 'seriesD',
value: 'SERIES_D',
label: 'Series D',
color: 'mint',
position: 20,
},
{
key: 'seriesE',
value: 'SERIES_E',
label: 'Series E',
color: 'ruby',
position: 21,
},
{
key: 'seriesF',
value: 'SERIES_F',
label: 'Series F',
color: 'bronze',
position: 22,
},
{
key: 'seriesG',
value: 'SERIES_G',
label: 'Series G',
color: 'jade',
position: 23,
},
{
key: 'seriesH',
value: 'SERIES_H',
label: 'Series H',
color: 'gray',
position: 24,
},
{
key: 'seriesI',
value: 'SERIES_I',
label: 'Series I',
color: 'blue',
position: 25,
},
{
key: 'seriesJ',
value: 'SERIES_J',
label: 'Series J',
color: 'red',
position: 26,
},
{
key: 'seriesUnknown',
value: 'SERIES_UNKNOWN',
label: 'Series Unknown',
color: 'green',
position: 27,
},
{
key: 'undisclosed',
value: 'UNDISCLOSED',
label: 'Undisclosed',
color: 'orange',
position: 28,
},
];
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,60 @@
import { type SelectOptionMeta } from 'src/types/select-option-meta.type';
export const SIZE_OPTIONS: readonly SelectOptionMeta[] = [
{
key: 'oneToTen',
value: 'ONE_TO_TEN',
label: '1-10',
color: 'blue',
position: 0,
},
{
key: 'elevenToFifty',
value: 'ELEVEN_TO_FIFTY',
label: '11-50',
color: 'red',
position: 1,
},
{
key: 'fiftyOneToTwoHundred',
value: 'FIFTY_ONE_TO_TWO_HUNDRED',
label: '51-200',
color: 'green',
position: 2,
},
{
key: 'twoHundredOneToFiveHundred',
value: 'TWO_HUNDRED_ONE_TO_FIVE_HUNDRED',
label: '201-500',
color: 'orange',
position: 3,
},
{
key: 'fiveHundredOneToOneThousand',
value: 'FIVE_HUNDRED_ONE_TO_ONE_THOUSAND',
label: '501-1000',
color: 'purple',
position: 4,
},
{
key: 'oneThousandOneToFiveThousand',
value: 'ONE_THOUSAND_ONE_TO_FIVE_THOUSAND',
label: '1001-5000',
color: 'yellow',
position: 5,
},
{
key: 'fiveThousandOneToTenThousand',
value: 'FIVE_THOUSAND_ONE_TO_TEN_THOUSAND',
label: '5001-10000',
color: 'pink',
position: 6,
},
{
key: 'tenThousandOnePlus',
value: 'TEN_THOUSAND_ONE_PLUS',
label: '10001+',
color: 'cyan',
position: 7,
},
];
@@ -0,0 +1,19 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlAffiliatedProfiles,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.ARRAY,
name: 'pdlAffiliatedProfiles',
label: 'Affiliated Profiles',
description: 'Affiliated company profiles returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,19 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlAlternativeDomains,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.ARRAY,
name: 'pdlAlternativeDomains',
label: 'Alternative Domains',
description: 'Alternative domains returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,19 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlAlternativeNames,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.ARRAY,
name: 'pdlAlternativeNames',
label: 'Alternative Names',
description: 'Alternative names returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,65 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlCompanyType,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlCompanyType',
label: 'Company Type',
description: 'People Data Labs canonical company type.',
isNullable: true,
options: [
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.public,
value: 'PUBLIC',
label: 'Public',
color: 'blue',
position: 0,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.private,
value: 'PRIVATE',
label: 'Private',
color: 'red',
position: 1,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.publicSubsidiary,
value: 'PUBLIC_SUBSIDIARY',
label: 'Public Subsidiary',
color: 'green',
position: 2,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.educational,
value: 'EDUCATIONAL',
label: 'Educational',
color: 'orange',
position: 3,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.government,
value: 'GOVERNMENT',
label: 'Government',
color: 'purple',
position: 4,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.nonprofit,
value: 'NONPROFIT',
label: 'Nonprofit',
color: 'yellow',
position: 5,
},
],
});
@@ -0,0 +1,28 @@
import {
defineField,
FieldType,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlCurrentEmployees,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.RELATION,
name: 'pdlCurrentEmployees',
label: 'PDL Current Employees',
description:
'People whose current employer is this company, per People Data Labs.',
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlCurrentCompany,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
icon: 'IconUsers',
});
@@ -0,0 +1,20 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlEmployeeCountByCountry,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.RAW_JSON,
name: 'pdlEmployeeCountByCountry',
label: 'Employee Count by Country',
description:
'Employee count broken down by country returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlEmployeeCount,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.NUMBER,
name: 'pdlEmployeeCount',
label: 'Employees',
description: 'Exact employee count returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,28 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ENRICHMENT_STATUS_OPTIONS } from 'src/constants/enrichment-status-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlEnrichmentStatus,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlEnrichmentStatus',
label: 'Enrichment Status',
description: 'Outcome of the latest People Data Labs enrichment attempt.',
isNullable: true,
options: buildSelectOptions(
ENRICHMENT_STATUS_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyEnrichmentStatus,
),
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlFacebookUrl,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.LINKS,
name: 'pdlFacebookUrl',
label: 'Facebook',
description: 'Facebook page returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlFoundedYear,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.NUMBER,
name: 'pdlFoundedYear',
label: 'Founded Year',
description: 'Founding year returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,27 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { FUNDING_STAGE_OPTIONS } from 'src/constants/funding-stage-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlFundingStages,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.MULTI_SELECT,
name: 'pdlFundingStages',
label: 'Funding Stages',
description: 'People Data Labs canonical funding stages.',
isNullable: true,
options: buildSelectOptions(
FUNDING_STAGE_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.fundingStage,
),
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlHeadline,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlHeadline',
label: 'Headline',
description: 'People Data Labs company headline or summary.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlId,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlId',
label: 'PDL ID',
description: 'People Data Labs company identifier.',
isNullable: true,
});
@@ -0,0 +1,19 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlIndustryDetail,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlIndustryDetail',
label: 'Industry (detailed)',
description: 'Detailed People Data Labs industry v2 value.',
isNullable: true,
});
@@ -0,0 +1,27 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { INDUSTRY_OPTIONS } from 'src/constants/industry-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlIndustry,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlIndustry',
label: 'Industry',
description: 'People Data Labs canonical industry.',
isNullable: true,
options: buildSelectOptions(
INDUSTRY_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyIndustry,
),
});
@@ -0,0 +1,19 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLastEnrichedAt,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.DATE_TIME,
name: 'pdlLastEnrichedAt',
label: 'Last Enriched At',
description: 'Timestamp of the latest People Data Labs enrichment.',
isNullable: true,
});
@@ -0,0 +1,19 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLastFundingDate,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.DATE,
name: 'pdlLastFundingDate',
label: 'Last Funding Date',
description: 'Date of the latest funding round returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,28 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { FUNDING_STAGE_OPTIONS } from 'src/constants/funding-stage-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLatestFundingStage,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlLatestFundingStage',
label: 'Latest Funding Stage',
description: 'People Data Labs canonical latest funding stage.',
isNullable: true,
options: buildSelectOptions(
FUNDING_STAGE_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.fundingStage,
),
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLinkedinId,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlLinkedinId',
label: 'LinkedIn ID',
description: 'LinkedIn identifier returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,79 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLocationContinent,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlLocationContinent',
label: 'Continent',
description: 'People Data Labs canonical continent.',
isNullable: true,
options: [
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.africa,
value: 'AFRICA',
label: 'Africa',
color: 'blue',
position: 0,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.antarctica,
value: 'ANTARCTICA',
label: 'Antarctica',
color: 'red',
position: 1,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent.asia,
value: 'ASIA',
label: 'Asia',
color: 'green',
position: 2,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.europe,
value: 'EUROPE',
label: 'Europe',
color: 'orange',
position: 3,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.northAmerica,
value: 'NORTH_AMERICA',
label: 'North America',
color: 'purple',
position: 4,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.oceania,
value: 'OCEANIA',
label: 'Oceania',
color: 'yellow',
position: 5,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.southAmerica,
value: 'SOUTH_AMERICA',
label: 'South America',
color: 'pink',
position: 6,
},
],
});
@@ -0,0 +1,27 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { METRO_OPTIONS } from 'src/constants/metro-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLocationMetro,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlLocationMetro',
label: 'Metro Area',
description: 'People Data Labs canonical metro area.',
isNullable: true,
options: buildSelectOptions(
METRO_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationMetro,
),
});
@@ -0,0 +1,513 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlMicExchange,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlMicExchange',
label: 'Stock Exchange (MIC)',
description: 'People Data Labs canonical market identifier code.',
isNullable: true,
options: [
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.aqse,
value: 'AQSE',
label: 'AQSE',
color: 'blue',
position: 0,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.arcx,
value: 'ARCX',
label: 'ARCX',
color: 'red',
position: 1,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.asex,
value: 'ASEX',
label: 'ASEX',
color: 'green',
position: 2,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.bcxe,
value: 'BCXE',
label: 'BCXE',
color: 'orange',
position: 3,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.bvmf,
value: 'BVMF',
label: 'BVMF',
color: 'purple',
position: 4,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.dsmd,
value: 'DSMD',
label: 'DSMD',
color: 'yellow',
position: 5,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.misx,
value: 'MISX',
label: 'MISX',
color: 'pink',
position: 6,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.neoe,
value: 'NEOE',
label: 'NEOE',
color: 'cyan',
position: 7,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xams,
value: 'XAMS',
label: 'XAMS',
color: 'brown',
position: 8,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xase,
value: 'XASE',
label: 'XASE',
color: 'lime',
position: 9,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xasx,
value: 'XASX',
label: 'XASX',
color: 'violet',
position: 10,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xber,
value: 'XBER',
label: 'XBER',
color: 'gold',
position: 11,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbkk,
value: 'XBKK',
label: 'XBKK',
color: 'turquoise',
position: 12,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbog,
value: 'XBOG',
label: 'XBOG',
color: 'crimson',
position: 13,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbom,
value: 'XBOM',
label: 'XBOM',
color: 'sky',
position: 14,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbru,
value: 'XBRU',
label: 'XBRU',
color: 'amber',
position: 15,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbsp,
value: 'XBSP',
label: 'XBSP',
color: 'plum',
position: 16,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbud,
value: 'XBUD',
label: 'XBUD',
color: 'grass',
position: 17,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbue,
value: 'XBUE',
label: 'XBUE',
color: 'tomato',
position: 18,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcai,
value: 'XCAI',
label: 'XCAI',
color: 'iris',
position: 19,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcbo,
value: 'XCBO',
label: 'XCBO',
color: 'mint',
position: 20,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcnq,
value: 'XCNQ',
label: 'XCNQ',
color: 'ruby',
position: 21,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcse,
value: 'XCSE',
label: 'XCSE',
color: 'bronze',
position: 22,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xdfm,
value: 'XDFM',
label: 'XDFM',
color: 'jade',
position: 23,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xdub,
value: 'XDUB',
label: 'XDUB',
color: 'gray',
position: 24,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xdus,
value: 'XDUS',
label: 'XDUS',
color: 'blue',
position: 25,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xeqy,
value: 'XEQY',
label: 'XEQY',
color: 'red',
position: 26,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xfra,
value: 'XFRA',
label: 'XFRA',
color: 'green',
position: 27,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xham,
value: 'XHAM',
label: 'XHAM',
color: 'orange',
position: 28,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xhel,
value: 'XHEL',
label: 'XHEL',
color: 'purple',
position: 29,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xhkg,
value: 'XHKG',
label: 'XHKG',
color: 'yellow',
position: 30,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xice,
value: 'XICE',
label: 'XICE',
color: 'pink',
position: 31,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xidx,
value: 'XIDX',
label: 'XIDX',
color: 'cyan',
position: 32,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xjpx,
value: 'XJPX',
label: 'XJPX',
color: 'brown',
position: 33,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xjse,
value: 'XJSE',
label: 'XJSE',
color: 'lime',
position: 34,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkls,
value: 'XKLS',
label: 'XKLS',
color: 'violet',
position: 35,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkos,
value: 'XKOS',
label: 'XKOS',
color: 'gold',
position: 36,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkrx,
value: 'XKRX',
label: 'XKRX',
color: 'turquoise',
position: 37,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkuw,
value: 'XKUW',
label: 'XKUW',
color: 'crimson',
position: 38,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xlis,
value: 'XLIS',
label: 'XLIS',
color: 'sky',
position: 39,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xlon,
value: 'XLON',
label: 'XLON',
color: 'amber',
position: 40,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmad,
value: 'XMAD',
label: 'XMAD',
color: 'plum',
position: 41,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmex,
value: 'XMEX',
label: 'XMEX',
color: 'grass',
position: 42,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmil,
value: 'XMIL',
label: 'XMIL',
color: 'tomato',
position: 43,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmun,
value: 'XMUN',
label: 'XMUN',
color: 'iris',
position: 44,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnas,
value: 'XNAS',
label: 'XNAS',
color: 'mint',
position: 45,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnse,
value: 'XNSE',
label: 'XNSE',
color: 'ruby',
position: 46,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnys,
value: 'XNYS',
label: 'XNYS',
color: 'bronze',
position: 47,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnze,
value: 'XNZE',
label: 'XNZE',
color: 'jade',
position: 48,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xosl,
value: 'XOSL',
label: 'XOSL',
color: 'gray',
position: 49,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xotc,
value: 'XOTC',
label: 'XOTC',
color: 'blue',
position: 50,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xpar,
value: 'XPAR',
label: 'XPAR',
color: 'red',
position: 51,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xpra,
value: 'XPRA',
label: 'XPRA',
color: 'green',
position: 52,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xris,
value: 'XRIS',
label: 'XRIS',
color: 'orange',
position: 53,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xsau,
value: 'XSAU',
label: 'XSAU',
color: 'purple',
position: 54,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xses,
value: 'XSES',
label: 'XSES',
color: 'yellow',
position: 55,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xsgo,
value: 'XSGO',
label: 'XSGO',
color: 'pink',
position: 56,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xshe,
value: 'XSHE',
label: 'XSHE',
color: 'cyan',
position: 57,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xshg,
value: 'XSHG',
label: 'XSHG',
color: 'brown',
position: 58,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xstc,
value: 'XSTC',
label: 'XSTC',
color: 'lime',
position: 59,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xsto,
value: 'XSTO',
label: 'XSTO',
color: 'violet',
position: 60,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xstu,
value: 'XSTU',
label: 'XSTU',
color: 'gold',
position: 61,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xswx,
value: 'XSWX',
label: 'XSWX',
color: 'turquoise',
position: 62,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtae,
value: 'XTAE',
label: 'XTAE',
color: 'crimson',
position: 63,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtai,
value: 'XTAI',
label: 'XTAI',
color: 'sky',
position: 64,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtal,
value: 'XTAL',
label: 'XTAL',
color: 'amber',
position: 65,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtse,
value: 'XTSE',
label: 'XTSE',
color: 'plum',
position: 66,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtsx,
value: 'XTSX',
label: 'XTSX',
color: 'grass',
position: 67,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xwar,
value: 'XWAR',
label: 'XWAR',
color: 'tomato',
position: 68,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xwbo,
value: 'XWBO',
label: 'XWBO',
color: 'iris',
position: 69,
},
],
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlNaics,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.RAW_JSON,
name: 'pdlNaics',
label: 'NAICS Codes',
description: 'NAICS classification codes returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,19 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlNumberFundingRounds,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.NUMBER,
name: 'pdlNumberFundingRounds',
label: 'Number of Funding Rounds',
description: 'Number of funding rounds returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlRawPayload,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.RAW_JSON,
name: 'pdlRawPayload',
label: 'PDL Raw Payload',
description: 'Full People Data Labs company enrichment response.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlSic,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.RAW_JSON,
name: 'pdlSic',
label: 'SIC Codes',
description: 'SIC classification codes returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,27 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { SIZE_OPTIONS } from 'src/constants/size-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlSizeRange,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlSizeRange',
label: 'Employee Range',
description: 'People Data Labs canonical self-reported employee range.',
isNullable: true,
options: buildSelectOptions(
SIZE_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.sizeRange,
),
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlSummary,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlSummary',
label: 'Summary',
description: 'Company summary returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlTags,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.ARRAY,
name: 'pdlTags',
label: 'Tags',
description: 'Tags returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlTicker,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlTicker',
label: 'Ticker',
description: 'Company ticker returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlTotalFunding,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.CURRENCY,
name: 'pdlTotalFunding',
label: 'Total Funding Raised',
description: 'Total funding raised in USD returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlTwitterUrl,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.LINKS,
name: 'pdlTwitterUrl',
label: 'X / Twitter',
description: 'X or Twitter company profile returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlBirthDate,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.DATE,
name: 'pdlBirthDate',
label: 'Birth Date',
description: 'Birth date returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlBirthYear,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.NUMBER,
name: 'pdlBirthYear',
label: 'Birth Year',
description: 'Birth year returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlCertifications,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.RAW_JSON,
name: 'pdlCertifications',
label: 'Certifications',
description: 'Certifications returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,26 @@
import {
defineField,
FieldType,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlCurrentCompany,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.RELATION,
name: 'pdlCurrentCompany',
label: 'PDL Current Company',
description: 'Current employer detected by People Data Labs.',
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlCurrentEmployees,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
},
icon: 'IconBuilding',
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlEducation,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.RAW_JSON,
name: 'pdlEducation',
label: 'Education History',
description: 'Education history returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,28 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ENRICHMENT_STATUS_OPTIONS } from 'src/constants/enrichment-status-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlEnrichmentStatus,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlEnrichmentStatus',
label: 'Enrichment Status',
description: 'Outcome of the latest People Data Labs enrichment attempt.',
isNullable: true,
options: buildSelectOptions(
ENRICHMENT_STATUS_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personEnrichmentStatus,
),
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlExperience,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.RAW_JSON,
name: 'pdlExperience',
label: 'Experience History',
description: 'Experience history returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlFacebookUrl,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.LINKS,
name: 'pdlFacebookUrl',
label: 'Facebook',
description: 'Facebook profile returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlGithubUrl,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.LINKS,
name: 'pdlGithubUrl',
label: 'GitHub',
description: 'GitHub profile returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlHeadline,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlHeadline',
label: 'Headline',
description: 'People Data Labs person headline or job summary.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlId,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlId',
label: 'PDL ID',
description: 'People Data Labs person identifier.',
isNullable: true,
});
@@ -0,0 +1,27 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { INDUSTRY_OPTIONS } from 'src/constants/industry-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlIndustry,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlIndustry',
label: 'Industry',
description: 'People Data Labs canonical industry.',
isNullable: true,
options: buildSelectOptions(
INDUSTRY_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personIndustry,
),
});
@@ -0,0 +1,111 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlInferredSalary,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlInferredSalary',
label: 'Inferred Salary (range)',
description: 'People Data Labs canonical inferred salary range.',
isNullable: true,
options: [
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary
.under20000,
value: 'UNDER_20000',
label: '<20,000',
color: 'blue',
position: 0,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary
.from20000To25000,
value: 'FROM_20000_TO_25000',
label: '20,000-25,000',
color: 'red',
position: 1,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary
.from25000To35000,
value: 'FROM_25000_TO_35000',
label: '25,000-35,000',
color: 'green',
position: 2,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary
.from35000To45000,
value: 'FROM_35000_TO_45000',
label: '35,000-45,000',
color: 'orange',
position: 3,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary
.from45000To55000,
value: 'FROM_45000_TO_55000',
label: '45,000-55,000',
color: 'purple',
position: 4,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary
.from55000To70000,
value: 'FROM_55000_TO_70000',
label: '55,000-70,000',
color: 'yellow',
position: 5,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary
.from70000To85000,
value: 'FROM_70000_TO_85000',
label: '70,000-85,000',
color: 'pink',
position: 6,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary
.from85000To100000,
value: 'FROM_85000_TO_100000',
label: '85,000-100,000',
color: 'cyan',
position: 7,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary
.from100000To150000,
value: 'FROM_100000_TO_150000',
label: '100,000-150,000',
color: 'brown',
position: 8,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary
.from150000To250000,
value: 'FROM_150000_TO_250000',
label: '150,000-250,000',
color: 'lime',
position: 9,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary
.over250000,
value: 'OVER_250000',
label: '>250,000',
color: 'violet',
position: 10,
},
],
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlInterests,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.ARRAY,
name: 'pdlInterests',
label: 'Interests',
description: 'Interests returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobCompanyId,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlJobCompanyId',
label: 'Current Company PDL ID',
description: 'Current company PDL identifier returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,29 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { INDUSTRY_OPTIONS } from 'src/constants/industry-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobCompanyIndustry,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlJobCompanyIndustry',
label: 'Current Company Industry',
description:
'Current company canonical industry returned by People Data Labs.',
isNullable: true,
options: buildSelectOptions(
INDUSTRY_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobCompanyIndustry,
),
});
@@ -0,0 +1,19 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobCompanyLinkedinUrl,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.LINKS,
name: 'pdlJobCompanyLinkedinUrl',
label: 'Current Company LinkedIn',
description: 'Current company LinkedIn returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobCompanyName,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlJobCompanyName',
label: 'Current Company',
description: 'Current company name returned by People Data Labs.',
isNullable: true,
});
@@ -0,0 +1,27 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { SIZE_OPTIONS } from 'src/constants/size-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobCompanySize,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlJobCompanySize',
label: 'Current Company Size',
description: 'Current company size range returned by People Data Labs.',
isNullable: true,
options: buildSelectOptions(
SIZE_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobCompanySize,
),
});

Some files were not shown because too many files have changed in this diff Show More