## Summary
- Add E2B logo to the Thanks section (after Crowdin, same height)
- Rename section heading from "Does the world need another CRM?" to "Why
Twenty" for a more confident, direct tone
## Test plan
- [ ] Verify E2B logo renders correctly on GitHub README
- [ ] Confirm all other logos still display properly
Made with [Cursor](https://cursor.com)
Co-authored-by: Cursor <cursoragent@cursor.com>
## Description
- Add `isHeadless` field to `FrontComponent` entity so front components
can run without rendering UI in the command menu
- Introduce headless front component mounting logic:
`HeadlessFrontComponentMountRoot` at the application root,
`useMountHeadlessFrontComponent`, and `useUnmountHeadlessFrontComponent`
hooks to mount/unmount headless components
- Expand the SDK with new action components (`Action`, `ActionLink`,
`ActionOpenSidePanelPage`) and host communication functions
(`openSidePanelPage`, `unmountFrontComponent`)
- Move `CommandMenuPages` type to twenty-shared so the SDK can reference
it for side panel navigation
## Video QA
https://github.com/user-attachments/assets/4f9e3bb1-fcd1-42be-b3f4-a97e80c2add2
## Summary
- Fixes a flaky test in `simple-secret-encryption.util.spec.ts` that
fails ~1 in 256 runs
- AES-256-CBC doesn't guarantee wrong-key decryption throws — PKCS7
padding validation is probabilistic. When padding accidentally looks
valid, decryption silently returns garbage instead of throwing.
- Changed the test to verify the correct security property: wrong-key
decryption must never return the original secret (both throw and garbage
are acceptable)
- Audited both production `decryptSecret` call sites in
`two-factor-authentication.service.ts` — they always use the correct
key, so end users are not affected
## Test plan
- [x] Test passes 5/5 consecutive runs locally
- [x] Lint passes
Made with [Cursor](https://cursor.com)
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
- Consolidates the MCP server from two endpoints (`/mcp` +
`/mcp/metadata`) into a **single `POST /mcp`** endpoint exposing five
high-level tools: `get_tool_catalog`, `learn_tools`, `execute_tool`,
`load_skills`, and `search_help_center`
- Fixes **DATABASE_CRUD tools not accessible via API key auth** by
removing an unnecessary `userId`/`userWorkspaceId` guard in
`DatabaseToolProvider` and threading `ApiKeyWorkspaceAuthContext`
through the tool context chain
- Improves tool descriptions with **STEP 1/2/3 workflow guidance** so AI
clients follow the correct discovery flow (catalog → learn → execute)
instead of guessing tool names
- Simplifies the **frontend AI settings** by removing the schema picker
dropdown (no more "Core Schema" vs "Metadata Schema" choice)
## Changes
### Backend
- **Deleted**: `mcp-metadata.controller.ts`, `mcp-metadata.service.ts`
(merged into core)
- **New**: `get-tool-catalog.tool.ts` — browsable, categorized tool
discovery
- **Fixed**: `DatabaseToolProvider.generateDescriptors` — removed guard
that blocked API key access to CRUD tools
- **Fixed**: `ToolContext` type + `ToolRegistryService` — `authContext`
now flows through so API key CRUD execution works end-to-end
- **Updated**: `McpProtocolService` — builds
`ApiKeyWorkspaceAuthContext` for API key requests
- **Updated**: All MCP tool descriptions with explicit step numbering
### Frontend
- **Simplified**: `SettingsAIMCP.tsx` — removed schema selector
dropdown, shows single MCP config
## Test plan
- [x] All 19 MCP unit tests pass (controller + protocol service)
- [x] Server and frontend lint clean
- [x] Server and frontend typecheck pass
- [x] Live-tested on localhost:3000 with API key: `get_tool_catalog`
returns 236 tools including 196 DATABASE_CRUD tools
- [x] Live-tested `execute_tool` with `find_companies` via API key —
returns real data
- [x] Tested MCP connection from Cursor IDE via project-level
`.cursor/mcp.json`
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
Fixes#12962
- Adds a two-pass ILIKE fallback to the global search service
(`search.service.ts`)
- **Fast path**: runs the tsvector query first (uses GIN index,
sub-millisecond)
- **Fallback**: only if tsvector returns fewer results than the limit,
runs an ILIKE query on `searchVector::text` to catch cases where
PostgreSQL's `simple` text search config fails to tokenize (continuous
CJK text, etc.)
- Zero performance impact for the common case (Latin text where tsvector
works)
- Also adds `escapeForIlike` utility to properly escape `%`, `_`, `\` in
user input
### Why tsvector fails for CJK
PostgreSQL's `simple` config treats continuous CJK text as a single
lexeme:
- `to_tsvector('simple', '示例商业线索')` → `'示例商业线索':1`
- Searching `商业:*` only prefix-matches from the start, so it misses `商业`
in the middle
The ILIKE fallback catches these substring matches when the tsvector
path can't.
### What this fixes
- Global search (command menu / sidebar)
- Relation picker (single and multi-object)
- Morph relation picker
All three use `search.service.ts` under the hood.
Co-authored-by: mykh-hailo (original direction in #18021)
## Test plan
- [ ] Search `示例` with records `示例商业线索` and `示例-商业-线索` → both should
appear
- [ ] Search `商业` → both should appear (previously only the hyphenated
one did)
- [ ] Search for Latin text (e.g. `john`) → same performance, results
unchanged
- [ ] Relation picker search with CJK text → results appear
- [ ] Search input with special chars like `%` or `_` → no SQL
injection, results correct
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: mykh-hailo <mykh-hailo@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
## Automated fix for [bug None](https://sonarly.com/issue/None?type=bug)
**Severity:** `critical`
### Summary
When uploading a person avatar with the new FILES field migration
enabled, the optimistic record validation in
computeOptimisticRecordFromInput throws because the avatarFile field may
not be recognized in the person object metadata, crashing the upload
flow.
### User Impact
Users with the IS_FILES_FIELD_MIGRATED feature flag enabled cannot
upload or change a person's avatar photo. The upload operation throws an
unhandled error, preventing the avatar update from completing.
### Root Cause
The usePersonAvatarUpload hook passes avatarFile as a field in
updateOneRecordInput when calling updateOneRecord. This input goes
through computeOptimisticRecordFromInput, which validates that every
field in the record input exists in objectMetadataItem.fields. The
avatarFile field (type FILES) was recently added as a standard field on
the person object, but may not yet be present in the frontend's cached
object metadata for all workspaces (e.g., workspaces where the metadata
has not been synced after the migration, or SSE events arriving before
metadata refresh). When avatarFile is not found in the metadata fields
array, the validation throws. The useUpdateOneRecord hook supports an
optimisticRecord parameter that bypasses this validation entirely, but
usePersonAvatarUpload did not provide it.
Introduced by Etienne in commit d0c1841f0f on 2026-02-11, which added
the avatar file migration and upload logic but did not supply an
optimisticRecord to bypass the strict field validation in
computeOptimisticRecordFromInput.
**Introduced by:** Etienne on 2026-02-11 in commit
[`d0c1841`](https://github.com/twentyhq/twenty/commit/d0c1841f0f8a6a9069bbe2e9cafacf4ff6137f82)
### Suggested Fix
Pass an explicit optimisticRecord parameter when calling updateOneRecord
from usePersonAvatarUpload. The useUpdateOneRecord hook uses
optimisticRecord via nullish coalescing (optimisticRecord ??
computeOptimisticRecordFromInput(...)), so providing it bypasses the
strict field validation in computeOptimisticRecordFromInput entirely.
The avatarFile value is extracted into a shared variable to avoid
duplication between updateOneRecordInput and optimisticRecord.
---
*Generated by [Sonarly](https://sonarly.com)*
---------
Co-authored-by: Sonarly Claude Code <claude-code@sonarly.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Etienne <etiennejouan@users.noreply.github.com>
## Summary
- The `listPlans` GraphQL query was returning metered tier `upTo` values
in internal credit units (1000x the display value), causing "Credits by
period" and "Credit Plan" dropdowns to show "50M" instead of "50k"
- Applied the existing `INTERNAL_CREDITS_PER_DISPLAY_CREDIT` (1000)
divisor in `formatBillingDatabasePriceToMeteredPriceDTO`, matching the
conversion already used in `getMeteredProductsUsage` resolver
- Updated frontend mock data to reflect the corrected display-unit
values
## Test plan
- [x] Unit tests pass for `format-database-product-to-graphql-dto.util`
- [x] Unit tests pass for `metered-credit.service` and
`billing-credit-rollover.service`
- [ ] Verify billing page shows correct credit amounts (50k, not 50M)
for "Credits by period" and "Credit Plan"
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
### What this PR do ?
Stops the delay fields from updating the workflow on every keystroke by
keeping values locally and saving them on blur, which fixes the cached
relation error
Fixed#15709
---------
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
## Context
Command to backfill record page layouts and related entities for legacy
workspaces.
## Test
Set SHOULD_SEED_STANDARD_RECORD_PAGE_LAYOUTS=false, reset DB then run
the command and compare with Set
SHOULD_SEED_STANDARD_RECORD_PAGE_LAYOUTS=true on a different workspace
## Summary
- **Consolidate logic function services**: Remove
`LogicFunctionMetadataService` and consolidate all logic function CRUD
operations into `LogicFunctionFromSourceService`, with a new
`LogicFunctionFromSourceHelperService` for shared validation/migration
logic
- **Introduce typed conversion utils following the skill pattern**: Add
`fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate`
and `fromUpdateLogicFunctionFromSourceInputToFlatLogicFunctionToUpdate`
that convert DTO inputs directly to flat entities
(`UniversalFlatLogicFunction` / `FlatLogicFunction`), replacing the
previous intermediate `UpdateLogicFunctionMetadataParams` indirection
- **Simplify `CodeStepBuildService`**: Remove ~100 lines of manual
duplication logic by delegating to
`LogicFunctionFromSourceService.duplicateOneWithSource`
- **Remove completed 1-17 migration**: Delete
`MigrateWorkflowCodeStepsCommand` and associated utils that migrated
workflow code steps from serverless functions to logic functions
## Summary
- Upgrades `@swc/core` from 1.13.3 to **1.15.11** (swc_core v56), which
introduces CBOR-based plugin serialization replacing rkyv, eliminating
strict version-matching between SWC core and Wasm plugins
- Upgrades `@lingui/swc-plugin` from ^5.6.0 to **^5.11.0** (swc_core
50.2.3, built with `--cfg=swc_ast_unknown` for cross-version
compatibility)
- Upgrades `@swc/plugin-emotion` from 10.0.4 to **14.6.0** (swc_core 53,
also with backward-compat feature)
- Upgrades companion packages: `@swc-node/register` 1.8.0 → 1.11.1,
`@swc/helpers` ~0.5.2 → ~0.5.18, `@vitejs/plugin-react-swc` 3.11.0 →
4.2.3
### Why this is safe now
Starting from `@swc/core v1.15.0`, SWC replaced the rkyv serialization
scheme with CBOR (a self-describing format) and added `Unknown` AST enum
variants. Plugins built with `swc_core >= 47` and
`--cfg=swc_ast_unknown` are now forward-compatible across `@swc/core`
versions. Both `@lingui/swc-plugin@5.10.1+` and
`@swc/plugin-emotion@14.0.0+` have this support, meaning the old
version-matching nightmare between Lingui and SWC is largely solved.
Reference: https://github.com/lingui/swc-plugin/issues/179
## Test plan
- [x] `yarn install` resolves without errors
- [x] `npx nx build twenty-shared` succeeds
- [x] `npx nx build twenty-ui` succeeds (validates
@swc/plugin-emotion@14.6.0)
- [x] `npx nx typecheck twenty-front` succeeds
- [x] `npx nx build twenty-front` succeeds (validates vite + swc +
lingui pipeline)
- [x] `npx nx build twenty-emails` succeeds (validates lingui plugin)
- [x] Frontend jest tests pass (validates @swc/jest +
@lingui/swc-plugin)
- [x] Server jest tests pass (validates server-side SWC + lingui)
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
## Description
- Adds support for declaring command menu items directly within
`defineFrontComponent` via an optional command config property
- Introduces a new `CommandMenuItemManifest` type in twenty-shared and
wires it through the manifest build pipeline
## Example Of usage
```tsx
import { defineFrontComponent } from "twenty-sdk";
const TestAction = () => {
return <div>Test Action</div>;
};
export default defineFrontComponent({
universalIdentifier: "6c289461-0007-4a62-a99f-69e5c11a4ce7",
name: "test-action",
description: "Test Action",
component: TestAction,
command: {
universalIdentifier: "c07df864-495f-46f3-9f5b-9d3ce2589e9b",
label: "Run My Action",
icon: "IconBolt",
isPinned: false,
},
});
```
## Video QA
https://github.com/user-attachments/assets/f910fc6a-44a9-45d1-87c5-f0ce64bb3878
Managed to create a many to many app with minimal instructions. File
will need to be enriched with more pitfalls.
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
# Introduction
While preparing the twenty-standard as code migration to twenty-app
through sdk I've faced permanent field enum update as the id was
generated dynamically at each twenty standard app construction
Making them deterministic in order to avoid having this noise
Won't backfill this on existing workspace as it's not critical and that
we will rework the options in the future
# Introduction
Atomically create the field and object to be created
And avoid synchronizing unrelated non up to date object and fields
Followup https://github.com/twentyhq/twenty/pull/17398
Introduced an error message on twenty-front CI earlier to try and inform
the user that test failure could be a coverage issue if no individual
test was failing. However, it led to the assumption that it must be
coverage failure in all cases even when it was test failure leading to
the CI being red.
This PR reverts the change.
# Introduction
## Centralize system field definitions
- Extract a single `PARTIAL_SYSTEM_FLAT_FIELD_METADATAS` constant as the
source of truth for all 8 system fields (`id`, `createdAt`, `updatedAt`,
`deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`),
eliminating duplication across custom object and standard app field
builders
- Refactor `buildDefaultFlatFieldMetadatasForCustomObject` to use the
shared constant via a new `buildObjectSystemFlatFieldMetadatas` helper
## Mark system fields as `isSystem: true`
- Fields `id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`,
`updatedBy`, `position`, `searchVector` are now properly flagged as
system fields across all standard objects and custom object creation
- Standard app field builders for all ~30 standard objects updated to
set `isSystem: true` on `createdAt`, `updatedAt`, `deletedAt`,
`createdBy`, `updatedBy`
- System-only standard objects (blocklist, calendar channels, message
threads, etc.) now also include `createdBy`, `updatedBy`, `position`,
`searchVector` field definitions that were previously missing
## Validate system fields on object creation
- New transversal validation (`crossEntityTransversalValidation`) runs
after all atomic entity validations in the build orchestrator, ensuring
all 8 system fields are present with correct `type` and `isSystem: true`
when an object is created
- New `buildUniversalFlatObjectFieldByNameAndJoinColumnMaps` utility to
resolve field names to universal identifiers for a given object
- New exception codes: `MISSING_SYSTEM_FIELD` and `INVALID_SYSTEM_FIELD`
on `ObjectMetadataExceptionCode`
## Protect system fields and objects from mutation
- Field validators now block update/delete of `isSystem` fields by
non-system callers (`FIELD_MUTATION_NOT_ALLOWED`)
- Object validators now block update/delete of `isSystem` objects by
non-system callers
- `POSITION` and `TS_VECTOR` field type validators replaced: instead of
rejecting creation outright, they now validate that the field is named
correctly (`position` / `searchVector`) and has `isSystem: true`
## Distinguish `isSystemBuild` from `isCallerTwentyStandardApp`
- New `isCallerTwentyStandardApp` utility checks whether the caller's
`applicationUniversalIdentifier` matches the twenty standard app
- Name-sync logic (`isFlatFieldMetadataNameSyncedWithLabel`,
`areFlatObjectMetadataNamesSyncedWithLabels`) refactored to use
`isCallerTwentyStandardApp` for custom suffix decisions, keeping
`isSystemBuild` for mutation permission checks
- `WorkspaceMigrationBuilderOptions` type updated to include
`applicationUniversalIdentifier`
## Adapt frontend filtering
- New `HIDDEN_SYSTEM_FIELD_NAMES` constant (`id`, `position`,
`searchVector`) and `isHiddenSystemField` utility to only hide truly
internal fields while keeping user-facing system fields (`createdAt`,
`updatedAt`, `deletedAt`, `createdBy`, `updatedBy`) visible in the UI
- ~20 frontend files updated to replace `!field.isSystem` checks with
`!isHiddenSystemField(field)` across record index, settings, data model,
charts, workflows, spreadsheet import, aggregations, and role
permissions
## Add 1.19 upgrade commands
- **`backfill-system-fields-is-system`**: Raw SQL command to set
`isSystem = true` on existing workspace fields matching system field
names, and fix `position` field type from `NUMBER` to `POSITION` for
`favorite`/`favoriteFolder` objects. Includes proper cache invalidation.
- **`add-missing-system-fields-to-standard-objects`**: Codegen'd
workspace migration to create missing `position`, `searchVector`,
`createdBy`, `updatedBy` fields on standard objects that didn't
previously have them. Runs via `WorkspaceMigrationRunnerService` in a
single transaction with idempotency check. **Known limitation**: assumes
all standard objects exist and are valid in the target workspace.
## Add `universalIdentifier` for system fields in standard object
constants
- `standard-object.constant.ts` updated to include `universalIdentifier`
for `createdBy`, `updatedBy`, `position`, and `searchVector` across all
standard objects
- `fieldManifestType.ts` updated to support the new field manifest shape
## System relation
Completely removed and backfilled all `isSystem` relation to be false
false
As we won't require an object to have any relation system fields
## Add integration tests
- New test suite `failing-sync-application-object-system-fields`
covering: missing system fields, wrong field types (`id` as TEXT,
`createdAt` as TEXT, `position` as TEXT), system field deletion
attempts, and system field update attempts
- New test utilities: `buildDefaultObjectManifest` (builds an object
manifest with all 8 system fields) and `setupApplicationForSync`
(centralizes application setup)
- Existing successful sync test updated to verify system fields are
created with correct properties
## Next step
Make the builder scope the compared entity to be the currently built app
+ nor twenty standard app
## Summary
- Adds an `objectRecordCounts` query on the `/metadata` GraphQL endpoint
that returns approximate record counts for all objects in the workspace
- Uses PostgreSQL's `pg_class.reltuples` catalog stats — a single
instant query instead of N `COUNT(*)` table scans
- Replaces the previous `CombinedFindManyRecords` approach which hit the
server's 20 root resolver limit and silently showed 0 for all counts on
the settings Data Model page
### Server
- `ObjectRecordCountDTO` — GraphQL type with `objectNamePlural` and
`totalCount`
- `ObjectRecordCountService` — reads `pg_class` catalog for the
workspace schema
- Query added to `ObjectMetadataResolver` with `@MetadataResolver()` +
`NoPermissionGuard`
### Frontend
- `OBJECT_RECORD_COUNTS` query added to
`object-metadata/graphql/queries.ts`
- `useCombinedGetTotalCount` simplified to a zero-argument hook using
the new query
- `SettingsObjectTable` simplified to a single hook call
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
with new 'file' FILES field on attachment, UI should display attachment
name from file field value.
Issue with API users updating only 'file' FILES field (and not name
field anymore)