Summary
- capture the updated page structure in a new Playwright YAML artifact
to document the small-ai-chat-fix workstream
- store the generated snapshot under
`.playwright-cli/page-2026-03-09T13-12-30-691Z.yml` for reference
Testing
- Not run (not requested)
## Add standard command menu items
### Summary
This PR introduces standard command menu items, migrating hardcoded
command menu actions to the backend command menu item architecture
powered by front components. It adds a new `twenty-standard-application`
package that defines, builds, and registers front components as standard
command menu items, gated behind the `IS_COMMAND_MENU_ITEM_ENABLED`
feature flag.
### Description
- **New `twenty-standard-application` package**: Contains front
component definitions with an esbuild-based build pipeline that
generates minified `.mjs` bundles and a manifest with checksums.
- **Server-side registration**: New constants register all items with
metadata (labels, icons, positions, availability types, conditional
expressions). A `StandardFrontComponentUploadService` uploads built
components to file storage.
- **`FALLBACK` availability type**: New enum value for command menu
items that appear as fallback options (e.g., "Search Records" fallback).
- **`CommandMenuContextApi` refactor**
- **Conditional availability enhancements**: New array-based helper
functions for evaluating multi-record conditions.
- **Frontend wiring** (twenty-front):
`useCommandMenuItemFrontComponentCommands`
## Next steps
Only simple commands have been implemented for now:
- **Navigation (9)** -- `CommandLink`: go-to-companies,
go-to-dashboards, go-to-notes, go-to-opportunities, go-to-people,
go-to-runs, go-to-settings, go-to-tasks, go-to-workflows
- **Side panel (4)** -- `CommandOpenSidePanelPage`: ask-ai,
search-records, search-records-fallback, view-previous-ai-chats
We still have to implement front components for all the following
commands:
All have placeholder `execute` logic (`async () => {}`) with a `// TODO:
implement execute logic` comment:
**Record (22)**
- `add-to-favorites`, `remove-from-favorites`
- `create-new-record`, `create-new-view`
- `delete-single-record`, `delete-multiple-records`
- `destroy-single-record`, `destroy-multiple-records`
- `restore-single-record`, `restore-multiple-records`
- `export-from-record-index`, `export-from-record-show`,
`export-multiple-records`, `export-note-to-pdf`, `export-view`
- `hide-deleted-records`, `see-deleted-records`
- `import-records`, `merge-multiple-records`, `update-multiple-records`
- `navigate-to-next-record`, `navigate-to-previous-record`
**Page layout (3)** -- `cancel-record-page-layout`,
`edit-record-page-layout`, `save-record-page-layout`
**Dashboard (4)** -- `cancel-dashboard-layout`, `duplicate-dashboard`,
`edit-dashboard-layout`, `save-dashboard-layout`
**Workflow (10)** -- `activate-workflow`, `add-node-workflow`,
`deactivate-workflow`, `discard-draft-workflow`, `duplicate-workflow`,
`see-active-version-workflow`, `see-runs-workflow`,
`see-versions-workflow`, `test-workflow`, `tidy-up-workflow`
**Workflow version (4)** -- `see-runs-workflow-version`,
`see-versions-workflow-version`, `see-workflow-workflow-version`,
`use-as-draft-workflow-version`
**Workflow run (3)** -- `see-version-workflow-run`,
`see-workflow-workflow-run`, `stop-workflow-run`
Fixed#18355
Currency fields ignored the workspace number format when editing:
display showed e.g. 5 982,77 € (French style) but the input forced US
style (5,982.77) and rejected comma as decimal.
Fix: CurrencyInput now uses useNumberFormat() and passes the correct
thousandsSeparator and radix to the IMask input so edit mode matches the
chosen format (comma/space, dot/comma, etc.).
Files: CurrencyInput.tsx (use format for mask), new
CurrencyInput.test.tsx .
---------
Co-authored-by: root <root@dragon.second>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
## Summary
- Move 4 test fixture apps from `twenty-sdk/src/cli/__tests__/apps/` to
`twenty-apps/fixtures/` with meaningful names (`rich-app` →
`postcard-app`, `root-app` → `minimal-app`)
- Replace all `from '@/sdk'` imports with `from 'twenty-sdk'` so fixture
apps are proper, portable twenty-sdk apps
- Remove the fragile `"@/*": ["../../../../../src/*"]` tsconfig hack and
replace with standard `"src/*": ["./src/*"]` paths
- Create a centralized `fixture-paths.ts` utility in twenty-sdk tests
for clean app path resolution
## Why
The fixture apps were deeply nested in twenty-sdk's test directory and
tightly coupled to its internal source layout via a tsconfig path alias
hack. This made them:
- Impossible to reuse outside of SDK CLI tests (e.g., for server-side
dev seeding with `DevSeederService`)
- Fragile — moving any twenty-sdk source file could break the path alias
- Poorly discoverable — buried 5 directories deep in test infrastructure
Moving them to `twenty-apps/fixtures/` makes them first-class portable
apps that can be imported by `twenty-server` for seeding, used in E2E
testing, and serve as canonical examples alongside `hello-world`.
## Test plan
- [x] All 8 twenty-sdk integration tests pass (3 suites: postcard-app,
minimal-app, invalid-app)
- [x] Prettier formatting verified on all changed files
- [ ] CI should confirm E2E tests also pass (these require a running
server)
Made with [Cursor](https://cursor.com)
Reorder validateInput to run before formatInput to prevent
parsePhoneNumber from throwing INVALID_COUNTRY on bad input.
Fixes TWENTY-FRONT-5RQ
/closes https://github.com/twentyhq/twenty/issues/17670
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
## Summary
Added a visual time picker dropdown for DateTime fields, replacing the
previous text input. Users can now select hours and minutes through an
intuitive scrollable interface. (Fixes #15057
## Changes
- **New component**: Add a `TimePickerDropdown` - Visual picker with
scrollable hour/minute columns
- **Updated**: `DateTimePickerHeader` - Implemented time picker dropdown
in `DateTimePickerHeader` and Move month/year picker to right side
## Snapshots
<img width="493" height="421" alt="image"
src="https://github.com/user-attachments/assets/3bd1f0a0-0ac2-473d-935e-d9f28b0e40e2"
/>
https://github.com/user-attachments/assets/daa5cba5-c86c-46aa-a634-0f5c04523af1
**If there is no enough place at right, auto-move month/year selector to
the left side**
Hi, @Bonapara I followed the Figma you shared to complete this feature.
Could you please review it for me? Thanks a lot.
---------
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
## Summary
- Replace regex-based private IP detection in `isPrivateIp` with Node.js
`net.BlockList` for CIDR-based range checking, which properly handles
all IPv4-mapped IPv6 representations (both dotted-decimal and hex forms)
- Add missing non-routable IP ranges: carrier-grade NAT
(`100.64.0.0/10`), IANA special purpose, documentation networks,
benchmarking, multicast, and reserved ranges
- Add protocol allowlist (http/https only) as an axios request
interceptor in `SecureHttpClientService` and as a Zod refinement in the
HTTP tool schema
## Test plan
- [x] All 100 existing + new tests pass across 4 secure-http-client test
suites
- [x] New tests cover carrier-grade NAT range boundaries (100.64.0.0 –
100.127.255.255)
- [x] New tests cover documentation, benchmarking, multicast, and
reserved ranges
- [x] New tests cover hex-form IPv4-mapped IPv6 addresses (the form
Node.js URL parser actually produces)
- [x] New tests verify protocol interceptor blocks `ftp:` and `file:`
schemes
- [x] New tests verify protocol interceptor is only active when safe
mode is enabled
Made with [Cursor](https://cursor.com)
## Summary
- Removes the `IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED` feature
flag, consolidating tarball-based app installation under the existing
`IS_APPLICATION_ENABLED` flag
- Removes the runtime feature flag check in `runWorkspaceMigration`
resolver (the `@RequireFeatureFlag(IS_APPLICATION_ENABLED)` decorator
already gates this endpoint)
- Cleans up related integration test setup/teardown and mock feature
flag maps
## Test plan
- [ ] Verify tarball-based app installation still works when
`IS_APPLICATION_ENABLED` is true
- [ ] Verify app installation is blocked when `IS_APPLICATION_ENABLED`
is false
- [ ] Run `failing-install-application.integration-spec.ts` to confirm
it passes without the removed flag
Made with [Cursor](https://cursor.com)
## Summary
- Restructures the developer Extend documentation: moves API and
Webhooks to top-level pages, creates dedicated Apps section with Getting
Started, Building, and Publishing pages
- Updates navigation structure (`docs.json`, `base-structure.json`,
`navigation.template.json`)
- Updates translated docs for all locales and LLMS.md references across
app packages
## Test plan
- [ ] Run `mintlify dev` locally and verify navigation structure
- [ ] Check that all links in the Extend section work correctly
- [ ] Verify translated pages render properly
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: github-actions <github-actions@twenty.com>
Fixes#18356
## Summary
Setting `LOG_LEVELS=debug,info,error,warn` crashes with `TypeError:
logLevels.map is not a function` because `CastToLogLevelArray` silently
returns `undefined` for invalid levels.
Now it throws a clear error message listing the invalid levels and valid
options:
```
Invalid log level(s): info. Valid levels are: log, error, warn, debug, verbose
```
## Changes
- Throw descriptive `Error` when invalid log levels are provided instead
of returning `undefined`
- Updated tests to verify the error message
## Test plan
- [x] All 8 existing tests passing
- [x] `"toto"` → throws `Invalid log level(s): toto. Valid levels are:
log, error, warn, debug, verbose`
- [x] `"verbose,error,toto"` → throws listing only `toto` as invalid
- [x] Valid levels (`log,error,warn,debug,verbose`) continue working as
before
# Intoduction
Closes https://github.com/twentyhq/core-team-issues/issues/2289
In this PR all the clients becomes available under `twenty-sdk/clients`,
this is a breaking change but generated was too vague and thats still
the now or never best timing to do so
## CoreClient
The core client is now shipped with a default stub empty class for both
the schema and the client
Allowing its import, will still raises typescript errors when consumed
as generated but not generated
## MetadataClient
The metadata client is workspace agnostic, it's now generated and
commited in the repo. added a ci that prevents any schema desync due to
twenty-server additions
Same behavior than for the twenty-front generated graphql schema
## Summary
- Replace all `ubuntu-latest-4-cores` (paid larger runners) with
`ubuntu-latest` across CI workflows
- The free `ubuntu-latest` runner for public repos already provides **4
vCPUs + 16 GB RAM** — identical specs to the paid 4-core larger runner
- Affects 4 workflow files: `ci-server.yaml`, `ci-front.yaml`,
`ci-sdk.yaml`, `ci-zapier.yaml` (8 job definitions total, including the
10-shard integration test matrix)
- The `ubuntu-latest-8-cores` runners are intentionally **kept** for
memory-heavy jobs (frontend build, storybook build, E2E tests) where the
extra capacity (8 vCPUs, 32 GB RAM) is needed
actions are being renamed to command menu item, they will be migrated to
server and will be served as headless front components
---------
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
## Summary
- The `createOneApplication` GraphQL mutation was removed from the
server during the application architecture refactor (#18432), but the
SDK CLI (`app:dev`, `app:build --sync`) still called it, causing
failures.
- Simplified the SDK to use `syncApplication` (which now internally
creates the `ApplicationEntity` via `ensureApplicationExists`) instead
of a separate create step.
- On first run (clean install), the orchestrator now runs an initial
sync before initializing the file uploader, so file uploads can proceed
(they require the `ApplicationEntity` to exist).
## Test plan
- [x] Typecheck passes for both `twenty-sdk` and `twenty-server`
- [x] `app:dev` tested locally with existing app (finds app, uploads,
syncs)
- [x] `app:dev` tested locally after `app:uninstall` (creates app via
sync, uploads, syncs)
- [x] SDK unit tests pass (23/26 files, 3 pre-existing failures
unrelated)
Made with [Cursor](https://cursor.com)
Solves [Sonarly Issue 8116](https://sonarly.com/issue/8116).
### Problem
Editing a morph relation field (e.g. "Parent Object" on Task) via the
field widget was broken in two ways:
1. **Setting a value** sent the wrong foreign key name (`parentObjectId`
instead of target-specific keys like `parentObjectCompanyId`), causing
the relation to not save.
2. **Detaching** never sent a request at all — the early return check
`valueToPersist?.id === currentValue?.id` evaluated to `undefined ===
undefined` when the morph field wasn't loaded in the store, silently
skipping the update.
The record detail section worked fine because it uses a separate hook
(`useMorphPersistManyToOne`).
### Fix
Added proper morph relation handling in `usePersistField` so all
persistence goes through this single hook consistently:
- Compute the correct FK name using `computeMorphRelationFieldName`
(e.g. `parentObjectCompanyId`) instead of deriving it from the field
name directly.
- Null all morph FK columns before setting the target one, ensuring only
one FK is non-null at a time (consistent with
`useMorphPersistManyToOne`).
- Fix the early return to only skip when **setting** a value that
matches the current one — detach always proceeds.
- Derive `currentRelationId` via a type guard instead of an `as` cast.
Fixes#15327
The issue occurred because the drag clone's visual state was previously
tied strictly to hovering over the `VISIBLE_TABS` boundaries. When a tab
was dragged outside this area (such as the last tab naturally crossing
into the `MORE_BUTTON` hover zone), the drag clone incorrectly fell back
to the dropdown menu item style.
We fixed this by making the `isHoveringTabList` logic more robust.
Instead of enforcing the tab style only within the `VISIBLE_TABS`
boundary, the dropdown style is now strictly restricted to the
`OVERFLOW_TABS` boundary.
With this change:
- Visible tabs successfully maintain their appearance when dragged
anywhere outside the dropdown.
- Dropdown tabs correctly transition to the normal tab style when
dragged out of the dropdown area, improving UX.
https://github.com/user-attachments/assets/9474e4c1-26a8-46e3-b9ee-4c6dbd8a4ea6
---------
Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: ehconitin <nitinkoche03@gmail.com>
Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
## Summary
- Re-enable one lint rule that was temporarily disabled during the
ESLint-to-Oxlint migration:
- **`twenty/sort-css-properties-alphabetically`** in twenty-front — 578
violations auto-fixed across 390 files
- Document why **`typescript/consistent-type-imports`** cannot be
auto-fixed in twenty-server: NestJS relies on `emitDecoratorMetadata`
for DI, so converting constructor parameter imports to `import type`
erases them at compile time and breaks dependency injection at runtime
- Right-size CI runners, reducing 8-core usage from 18 jobs to 3:
| Change | Jobs | Rationale |
|--------|------|-----------|
| **Keep 8-core** | `ci-merge-queue/e2e-test`,
`ci-front/front-sb-build`, `ci-front/front-build` | Heavy builds needing
max CPU + memory (10GB NODE_OPTIONS, full Storybook webpack bundling) |
| **8-core → 4-core** | `ci-server` (build, lint-typecheck, validation,
test, integration-test), `ci-front/front-sb-test`,
`ci-zapier/server-setup`, `ci-sdk/sdk-e2e-test` | Already sharded into
10-12 parallel instances, I/O-bound (DB/Redis), or moderate single
builds |
| **8-core → 2-core** | `ci-emails/emails-test` | Trivially lightweight
(build + curl health check) |
| **Removed** | `ci-front/front-chromatic-deployment` | Dead code —
permanently disabled with `if: false` |
- Fix merge queue CI issues:
- **Concurrency**: Use `merge_group.base_ref` instead of unique merge
group ref so new queue entries cancel previous runs
- **Required status checks**: Add `merge_group` trigger to all 6
required CI workflows (front, server, shared, website, docker-compose,
sdk) with `changed-files-check` auto-skipped for merge_group events —
status check jobs auto-pass without re-running full CI
- **Build caching**: Add Nx build cache restore/save to E2E test job
with fallback to `main` branch cache for faster frontend and server
builds
## Test plan
- [ ] CI passes on this PR (verifies lint rule auto-fix works)
- [ ] Verify 4-core runner jobs complete within their 30-minute timeouts
- [ ] Verify merge queue status checks auto-pass (ci-front-status-check,
ci-server-status-check, etc.)
- [ ] Verify merge queue E2E concurrency cancels previous runs when a
new PR enters the queue
REST API allowed users to pass in targetOpportunity, targetPerson,
targetCompany etc when trying to create a noteTarget or a taskTarget.
The request went through, we got back a 201, the record was created, but
the relationship was never established since the FK was empty in the
database.
This PR enforces users to send in the property with the `Id` suffix for
consistency. So, the user sends in targetOpportunityId, targetPersonId,
targetCompanyId etc.
<p align="center">
<img width="854" height="480" alt="image"
src="https://github.com/user-attachments/assets/ed50a623-68d4-4266-baf1-e94a657c3fd4"
/>
</p>
If the users try to send without the "Id" suffix, they get an error
explaining what to do.
<p align="center">
<img width="854" height="480" alt="image"
src="https://github.com/user-attachments/assets/8bab399b-7a04-4b6a-86b5-6f0e0b1ecd5d"
/>
</p>
Additionally, the documentation itself contains the correct property
names.
<p align="center">
<img width="854" height="480" alt="image"
src="https://github.com/user-attachments/assets/83e51cd6-8ef7-4a4d-8696-ab37cc4a9dd6"
/>
</p>
Finally, the filters also enforce this "Id" suffix convention in the GET
request.
<p align="center">
<img width="854" height="480" alt="image"
src="https://github.com/user-attachments/assets/168a2f09-1242-40fa-bd84-1f7d9c60357c"
/>
</p>
Edit: Updated error messages after the screenshots were taken to make
them a little generic. Secondly, this PR also fixes the issue of morph
relation ids and objects not appearing in the response (when depth is
1).
## Summary
- **Merge queue optimization**: Created a dedicated
`ci-merge-queue.yaml` workflow that only runs Playwright E2E tests on
`ubuntu-latest-8-cores`. Removed `merge_group` trigger from all 7
existing CI workflows (front, server, shared, website, sdk, zapier,
docker-compose). The merge queue goes from ~30+ parallel jobs to a
single focused E2E job.
- **Label-based merge queue simulation**: Added `run-merge-queue` label
support so developers can trigger the exact merge queue E2E pipeline on
any open PR before it enters the queue.
- **Prettier in lint**: Chained `prettier --check` into `lint` and
`prettier --write` into `lint --configuration=fix` across `nx.json`
defaults, `twenty-front`, and `twenty-server`. Prettier formatting
errors are now caught by `lint` and fixed by `lint:fix` /
`lint:diff-with-main --configuration=fix`.
## After merge (manual repo settings)
Update GitHub branch protection required status checks:
1. Remove old per-workflow merge queue checks (`ci-front-status-check`,
`ci-e2e-status-check`, `ci-server-status-check`, etc.)
2. Add `ci-merge-queue-status-check` as the required check for the merge
queue
## Context
- fuse.js was imported in navigate-app-tool.ts but only declared in the
root package.json (has been like this for months but for the first time
being used in the server)
- This works locally due to yarn hoisting, but breaks in Docker
production builds
```bash
Error: Cannot find module 'fuse.js'
Require stack:
- /app/packages/twenty-server/dist/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.js
- /app/packages/twenty-server/dist/engine/core-modules/tool-provider/providers/action-tool.provider.js
- /app/packages/twenty-server/dist/engine/core-modules/tool-provider/tool-provider.module.js
- /app/packages/twenty-server/dist/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module.js
- /app/packages/twenty-server/dist/engine/metadata-modules/ai/ai-agent-monitor/ai-agent-monitor.module.js
- /app/packages/twenty-server/dist/engine/metadata-modules/metadata-engine.module.js
- /app/packages/twenty-server/dist/engine/api/graphql/core-graphql-api.module.js
- /app/packages/twenty-server/dist/app.module.js
- /app/packages/twenty-server/dist/main.js
at Module._resolveFilename (node:internal/modules/cjs/loader:1456:15)
at defaultResolveImpl (node:internal/modules/cjs/loader:1066:19)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1071:22)
at Module._load (node:internal/modules/cjs/loader:1242:25)
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
at Module.require (node:internal/modules/cjs/loader:1556:12)
at require (node:internal/modules/helpers:152:16)
at Object.<anonymous> (/app/packages/twenty-server/dist/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.js:13:54)
at Module._compile (node:internal/modules/cjs/loader:1812:14)
at Object..js (node:internal/modules/cjs/loader:1943:10) {
code: 'MODULE_NOT_FOUND',
```
Fix: Adding the dependency in twenty-server package.json to make it available in production builds
## Summary
Front Before:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/b978f67c-c0a6-49fc-bedd-a443f11c365d"
/>
Front After:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/a4939dbb-a8b4-4c74-978c-daa7f27d00f3"
/>
Server Before:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/da53e97f-ec65-4224-a656-ca41040aef6e"
/>
Server After:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/8cdf3885-f515-4d6c-989f-a421a4e8206c"
/>
### CI Server Pipeline Restructuring
- Split monolithic `server-setup` job into three parallel jobs:
`server-build`, `server-lint-typecheck`, and `server-validation`
- `server-build` only handles build + Nx cache save (~1m vs old 3.5m),
unblocking downstream jobs faster
- `server-lint-typecheck` runs in parallel with no DB dependency
- `server-validation` handles DB setup, migration checks, and GraphQL
generation checks in parallel with tests
- Make `server-test` (unit tests) fully independent — no longer waits
for server-setup, builds its own artifacts
- Increase integration test shards from 8 to 10 for better parallelism
- Expected critical path reduction: ~10m → ~7m (~30% faster)
### CI Front Pipeline Improvements
- Use artifact upload/download for storybook build instead of rebuilding
in test shards
- Serve pre-built storybook via `http-server` in test jobs, with
`STORYBOOK_URL` env var
- Update `vitest.config.ts` to use `storybookUrl` when `STORYBOOK_URL`
is set
- Remove redundant `twenty-shared`, `twenty-ui`, `twenty-sdk` builds
from storybook test shards
### Vite Build Optimizations
- Conditionally enable `rollup-plugin-visualizer` behind `ANALYZE=true`
env var (not loaded by default)
- Broaden Istanbul coverage exclusions to skip test files, stories,
mocks, and decorators
- Remove `@tabler/icons-react` alias from twenty-front and storybook
configs
- Bundle `@tabler/icons-react` into twenty-ui instead of treating it as
an external dependency
- Add lazy loading with `React.lazy` + `Suspense` for all page-level
route components in `useCreateAppRouter`
fix for #18331
## Issue
The height of the container for the kanban board and calendar was set
after calculating the offset from the top bar which contains the
filters. The main issue was that it was calculated wrong. To fix this, I
have added flex:1 to ensure that the board and calendar will grow into
the empty space
## Proof of successful change
The video below shows that the scrollbar is accessible now and that the
calendar view is also adjusted to not cause any errors because of the
new change introduced.
https://github.com/user-attachments/assets/7f58342f-6cbf-4d30-878a-ec57f1e6666a
---------
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
## Summary
- **Module reorganization**: Moved `ApplicationUpgradeService` and cron
jobs to `application-upgrade/`, `ApplicationSyncService` to
`application-manifest/`, and
`runWorkspaceMigration`/`uninstallApplication` mutations to the manifest
resolver — each module now has a single clear responsibility.
- **Explicit install flow**: Removed implicit `ApplicationEntity`
creation from `ApplicationSyncService`. The install service and dev
resolver now explicitly create the `ApplicationEntity` before syncing.
npm packages are resolved at registration time to extract manifest
metadata (universalIdentifier, name, description, etc.), eliminating the
`reconcileUniversalIdentifier` hack.
- **Better error handling**: Frontend hooks now surface actual server
error messages in snackbars instead of swallowing them. Replaced the
ugly `ConfirmationModal` for transfer ownership with a proper form
modal. Fixed `SettingsAdminTableCard` row height overflow and corrected
the `yarn-engine` asset path.
## Test plan
- [ ] Register an npm package — verify manifest metadata (name,
description, universalIdentifier) is extracted correctly
- [ ] Install a registered npm app on a workspace — verify
ApplicationEntity is created and sync succeeds
- [ ] Test `app:dev` CLI flow — verify local app registration and sync
work
- [ ] Upload a tarball — verify registration and install flow
- [ ] Transfer ownership — verify the new modal UX works
- [ ] Verify error messages appear correctly in snackbars when
operations fail
Made with [Cursor](https://cursor.com)
## Summary
- Disable manual record creation (add button, + header button, add new
row) for **WorkflowRun** and **WorkflowVersion** objects since these are
system-managed and should not be created manually
- Fix vertical centering of the record table empty state placeholder
(regression from `styled(Component)` refactor in #18430 — the wrapper
lost `height: 100%` / `width: 100%`)
## Test plan
- [ ] Navigate to Workflow Runs index page → empty state should show
centered placeholder **without** "Add a Workflow Run" button
- [ ] Navigate to Workflow Versions index page → empty state should show
centered placeholder **without** "Add a Workflow Version" button
- [ ] Navigate to any other object index page (e.g. People, Companies) →
empty state should still show the "Add a ..." button and be centered
- [ ] Verify the + button in the record table header is hidden for
workflow runs/versions
- [ ] Verify the "Add New" row at the bottom of the table is hidden for
workflow runs/versions
## Summary
Fixes the workflow show page being blank after the `styled(Component)`
removal in #18430.
- PR #18430 replaced `styled(PageBody)` with a plain `div` wrapper
(`StyledPageBodyForDesktopContainer`) around `PageBody`, but the wrapper
defaulted to `display: block`
- `PageBody`'s internal container uses `flex: 1 1 auto` to size itself,
which requires a flex parent — the block wrapper broke height
propagation, causing React Flow's container to have 0 height
- Added `display: flex; flex-direction: column` to both
`StyledPageBodyForDesktopContainer` and
`StyledPageBodyForMobileContainer` to restore the flex chain
## Test plan
- [x] Open a workflow record show page → diagram nodes are visible
- [x] Open a company/person record show page → fields, tabs, and content
render correctly
- [x] Lint and typecheck pass
## Summary
- Add `align-items: center` to tab list `StyledContainer` so the
overflow button aligns vertically with tabs
- Remove ineffective `> * { height }` hack from `TabMoreButton` (was
being reset by `all: unset` in `StyledTabButton`)
## Test plan
- Open a record detail page with enough tabs to trigger the "+N More"
overflow
- Verify the overflow button is vertically centered with the visible
tabs
- The schema generator marked both the FK scalar and connect relation
input as required for non-nullable `MANY_TO_ONE` relations, but the
resolver rejects when both are provided making create mutations
impossible
- Fixed by making the connect input always optional in create input
types (the FK scalar still enforces the constraint)
- Added `createOne` pre-query hook for blocklist with ownership
validation
https://github.com/user-attachments/assets/aaae83d4-4747-4d16-a87c-8d8cad79d25d
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
This PR removes the complex logic that was used to manage z-index
switching to have the hovered cell portal correctly behave when its
borders were overlaping cells ones.
We now have the hovered portal inside a cell, thus removing the need for
a z-index dynamic logic.
The code has been simplified in the parts where the logic was
implemented and the constant that holds the all z indices for the tables
is still needed but with less options.
## Summary
Removes `vite-plugin-checker` and all references to
`VITE_DISABLE_TYPESCRIPT_CHECKER` / `VITE_DISABLE_ESLINT_CHECKER`.
These background checks are no longer needed because our dev experience
now relies on **independent** linters and type-checkers:
- `npx nx lint:diff-with-main twenty-front` for ESLint
- `npx nx typecheck twenty-front` for TypeScript
Running these as separate processes (rather than inside Vite) is faster,
gives cleaner output, and avoids the significant memory overhead that
`vite-plugin-checker` introduces during `vite dev` and `vite build`. The
old env vars to disable them are removed from `vite.config.ts`,
`package.json` scripts, `nx.json`, `.env.example`, and all translated
docs.
Introduce two new ESLint rules that prevent the use of `jotaiStore` and
direct calls to `.atomFamily()` or `.selectorFamily()` within component
selector `get` callbacks.
These rules promote cleaner and more reactive code practices.
Fixed file touched by those new rules :
`calendarDayRecordIdsComponentFamilySelector`
## Problem
While working on the IS/IS_NOT filter feature (#15317 ), I found this
problem. So I want to submit a separate pr to fix it at first.
In the advanced filter, clicking on a composite field (Emails, Phones,
Links) was not showing the sub-field selection menu.
## Root cause
Related to #18178 (Recoil → Jotai migration).
`AdvancedFilterFieldSelectMenu` was writing composite field states using
`advancedFilterFieldSelectDropdownId` as the instance ID. But the reader
components (`AdvancedFilterFieldSelectDropdownContent`,
`AdvancedFilterSubFieldSelectMenu`) resolve the instance ID from React
context, which has a different value — so they were reading from a
different Jotai atom instance and `isSelectingCompositeField` was always
`false`.
## Fix
Remove the 3 explicit instance IDs so the writer uses context, matching
the readers.
## Before
https://github.com/user-attachments/assets/dde54077-0eaf-453c-a638-bec6d6fe4d55
## After
https://github.com/user-attachments/assets/01916bcf-0ee8-49ad-bb54-9d8f10571069
Hope I understood it correctly.
---------
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
# Introduction
While testing the sdk and overall apps in
https://github.com/prastoin/twenty-app-hello-world
Faced a lot of pure `CJS` external dependencies import issue
Replaced all the cjs deps to either esm equivalent or node native
replacement
# Introduction
Previously the auth jwt stragegy would lod the whole user entity in the
auth user context
On an exception it would completely get logged on the pods
## Security layer
- 0/ Updating the type system ( devxp only though )
- 1/ The jwt auth stragegy only load a specific sub set of the user
entity
- 2/ Sanitizing at the exception log level directly in case of a user
context
- 3/ Sanitizing at the console driver
The last two sanitization could sound a bit redundant though they're
still good fallback to keep in case new path occurs in the cb
This PR adds what is necessary for having SSE working for view relations
: fields, filters, filter groups and sorts.
This should allow to have AI working well while creating views with
detailed filtering and sorting.
## Demo
https://github.com/user-attachments/assets/026c7fb5-8e1a-4498-b7f4-d16993e5a7c4
## Fixes
Also fixed in this PR while working on the filter area :
- Advanced filter does not update
- Advanced filter sub field selection is broken (due to Jotai migration)
- No view fields when creating a new view
- Error on advanced filter deletion (cascade delete wasn't taken into
account on the frontend)
- Bug advanced filter creation
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This PR adds the necessary tool to create a demo workspace with :
relevant custom objects and fields, mock data and a real dashboard with
graph widgets.
It is still a bit under-optimized and slow but it works.
This PR also adds an AI tool that allows to see what happens in real
time, it navigates the app and waits when necessary.
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Steps now throw WorkflowStepExecutorException. Then workflow executor
decides if error should be catch or not.
Since tools are not only used in workflow and these do not throw, we may
still miss errors here.
Workflow jobs now only catch errors to end the workflow run and throw.
Bumps [@clickhouse/client](https://github.com/ClickHouse/clickhouse-js)
from 1.11.0 to 1.18.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ClickHouse/clickhouse-js/releases"><code>@clickhouse/client</code>'s
releases</a>.</em></p>
<blockquote>
<h2>1.18.1</h2>
<h2>Improvements</h2>
<ul>
<li>Setting <code>log.level</code> default value to
<code>ClickHouseLogLevel.WARN</code> instead of
<code>ClickHouseLogLevel.OFF</code> to provide better visibility into
potential issues without overwhelming users with too much information by
default.</li>
</ul>
<pre lang="ts"><code>const client = createClient({
// ...
log: {
level: ClickHouseLogLevel.WARN, // default is now
ClickHouseLogLevel.WARN instead of ClickHouseLogLevel.OFF
},
})
</code></pre>
<ul>
<li>Logging is now lazy, which means that the log messages will only be
constructed if the log level is appropriate for the message. This can
improve performance in cases where constructing the log message is
expensive, and the log level is set to ignore such messages. See
<code>ClickHouseLogLevel</code> enum for the complete list of log
levels. (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/520">#520</a>)</li>
</ul>
<pre lang="ts"><code>const client = createClient({
// ...
log: {
level: ClickHouseLogLevel.TRACE, // to log everything available down to
the network level events
},
})
</code></pre>
<ul>
<li>Enhanced the logging of the HTTP request / socket lifecycle with
additional trace messages and context such as Connection ID (UUID) and
Request ID and Socket ID that embed the connection ID for ease of
tracing the logs of a particular request across the connection
lifecycle. To enable such logs, set the <code>log.level</code> config
option to <code>ClickHouseLogLevel.TRACE</code>. (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/567">#567</a>)</li>
</ul>
<pre
lang="console"><code>[2026-02-25T09:19:13.511Z][TRACE][@clickhouse/client][Connection]
Insert: received 'close' event, 'free' listener removed
Arguments: {
operation: 'Insert',
connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c',
query_id: '9dfda627-39a2-41a6-9fc9-8f8716574826',
request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:3',
socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2',
event: 'close'
}
[2026-02-25T09:19:13.502Z][TRACE][@clickhouse/client][Connection] Query:
reusing socket
Arguments: {
operation: 'Query',
connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c',
query_id: 'ad0127e8-b1c7-4ed6-9681-c0162f7a0ea9',
request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:4',
socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2',
usage_count: 1
}
</code></pre>
<ul>
<li>A step towards structured logging: the client now passes rich
context to the logger <code>args</code> parameter (e.g.
<code>connection_id</code>, <code>query_id</code>,
<code>request_id</code>, <code>socket_id</code>). (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/576">#576</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ClickHouse/clickhouse-js/blob/main/CHANGELOG.md"><code>@clickhouse/client</code>'s
changelog</a>.</em></p>
<blockquote>
<h1>1.18.1</h1>
<h2>Improvements</h2>
<ul>
<li>Setting <code>log.level</code> default value to
<code>ClickHouseLogLevel.WARN</code> instead of
<code>ClickHouseLogLevel.OFF</code> to provide better visibility into
potential issues without overwhelming users with too much information by
default.</li>
</ul>
<pre lang="ts"><code>const client = createClient({
// ...
log: {
level: ClickHouseLogLevel.WARN, // default is now
ClickHouseLogLevel.WARN instead of ClickHouseLogLevel.OFF
},
})
</code></pre>
<ul>
<li>Logging is now lazy, which means that the log messages will only be
constructed if the log level is appropriate for the message. This can
improve performance in cases where constructing the log message is
expensive, and the log level is set to ignore such messages. See
<code>ClickHouseLogLevel</code> enum for the complete list of log
levels. (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/520">#520</a>)</li>
</ul>
<pre lang="ts"><code>const client = createClient({
// ...
log: {
level: ClickHouseLogLevel.TRACE, // to log everything available down to
the network level events
},
})
</code></pre>
<ul>
<li>Enhanced the logging of the HTTP request / socket lifecycle with
additional trace messages and context such as Connection ID (UUID) and
Request ID and Socket ID that embed the connection ID for ease of
tracing the logs of a particular request across the connection
lifecycle. To enable such logs, set the <code>log.level</code> config
option to <code>ClickHouseLogLevel.TRACE</code>. (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/567">#567</a>)</li>
</ul>
<pre
lang="console"><code>[2026-02-25T09:19:13.511Z][TRACE][@clickhouse/client][Connection]
Insert: received 'close' event, 'free' listener removed
Arguments: {
operation: 'Insert',
connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c',
query_id: '9dfda627-39a2-41a6-9fc9-8f8716574826',
request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:3',
socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2',
event: 'close'
}
[2026-02-25T09:19:13.502Z][TRACE][@clickhouse/client][Connection] Query:
reusing socket
Arguments: {
operation: 'Query',
connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c',
query_id: 'ad0127e8-b1c7-4ed6-9681-c0162f7a0ea9',
request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:4',
socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2',
usage_count: 1
}
</code></pre>
<ul>
<li>A step towards structured logging: the client now passes rich
context to the logger <code>args</code> parameter (e.g.
<code>connection_id</code>, <code>query_id</code>,
<code>request_id</code>, <code>socket_id</code>). (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/576">#576</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/cbdd7bf20904626956e0ff7808d17015813400c1"><code>cbdd7bf</code></a>
Release 1.18.1 (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/590">#590</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/c9f61ebb3a2ec6201f87417e30c4fc4271451ae8"><code>c9f61eb</code></a>
Beta 1.18.0 (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/588">#588</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/d0f67b71ef896d47fc3d8d0942612ced22aa79dc"><code>d0f67b7</code></a>
Split public and internal <code>drainStream</code> and cover with tests
(<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/578">#578</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/535e9b726e328ce8468c159f935c27910731f4bb"><code>535e9b7</code></a>
Remove <code>unsafeLogUnredactedQueries</code> for now (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/580">#580</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/44e73c73019a3956c1fac67ce0f4f170b3a4f19a"><code>44e73c7</code></a>
Default log level to <code>WARN</code> (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/581">#581</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/5146fbc13e5c23d08e2cc5773bea0adf58d83a5c"><code>5146fbc</code></a>
Focus AI on security and API stability (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/579">#579</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/b7b1d8d7ffe9b6786c9e883379d3edc5a5ed5c58"><code>b7b1d8d</code></a>
Trivial E2E test against <code>beta</code> (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/577">#577</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/761e29ebb5d1bd7a107d2535b385b6565b7120f5"><code>761e29e</code></a>
Structured logs, part 1 (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/576">#576</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/fd23dd7fc9e91ff810a7bb45984a24682ba25482"><code>fd23dd7</code></a>
Provide more context in logs for connection and request handling (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/567">#567</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/a7866e72e356244cae9d20d9fef38a6aafe68ba8"><code>a7866e7</code></a>
Adjusting CI DevX (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/574">#574</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/ClickHouse/clickhouse-js/compare/1.11.0...1.18.1">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/~4b819b88c84b">4b819b88c84b</a>, a new
releaser for <code>@clickhouse/client</code> since your current
version.</p>
</details>
<br />
[](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: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
Upgraded NX to resolve some dependabot alerts caused by transitive
dependencies, but after the upgrade, it appears those transitive
dependency issues were not fixed by NX in the first place.
Creating this PR with the upgrade regardless to avoid wasted work. Used
`npx nx@latest migrate latest` from the documentation to automate the
upgrade and it bumped all the dependencies changed in `package.json` for
compatibility - `react-router-dom` and `swc` ones too.
Ran tests, ran builds, started the development server and used the
application - everything looks good after the upgrade.
This PR fixes a good number of dependabot alerts associated to minimatch
transitive import.
There are a total of 28 alerts, merging this shall confirm which ones
remain open afterwards and make it easier to diagnose.
- Remove feature flag
- Remove legacy methods in file-upload and file-service
- Migrate AI Chat to new file management
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
## Summary
Now that Twenty has fully migrated from Emotion to Linaria, the theme
system has been simplified to remove unnecessary complexity that existed
only to support the old runtime injection pattern.
### What changed
- **Deleted** `generateThemeConstants.ts` script and the entire
`generated/` directory — no more auto-generation
- **Added** `theme-light.css` and `theme-dark.css`: static CSS files
with 991 custom properties each, scoped under `.light` and `.dark`
selectors respectively
- **Moved** `themeCssVariables.ts` out of `generated/` and hand-maintain
it as a static `as const` object of `var(--t-*)` references (Linaria can
statically evaluate these at build time)
- **Extracted** numeric constants (`MOBILE_VIEWPORT`, `ICON_SIZES`,
`ICON_STROKES`) into a new `constants.ts` — CSS variables can't be used
in media queries or as numeric icon size props
- **Simplified** `ThemeContextProvider`: removed
`ThemeCssVariableInjectorEffect` entirely; now uses a single
`useLayoutEffect` to toggle `.light`/`.dark` class on `<html>`
- **Added** `class="light"` to `index.html` as default to prevent FOUC
before React hydration
### Why
The previous setup maintained a dual system: JS theme objects
(`THEME_LIGHT`/`THEME_DARK`) used at runtime, plus a generation script
that produced CSS variable entry arrays, which were then injected into
the DOM by `ThemeCssVariableInjectorEffect`. With Linaria, theme values
only need to be CSS custom properties — the JS objects were redundant.
This PR removes ~250 lines of infrastructure while keeping the same
theming capabilities.
Fixes an edge case when a user signs up with Google and the profile
avatar network request times out, we crash instead of creating the user
without an avatar.
Added `axios-retry` to retry max 2 times and if it still fails we
gracefully skip avatar image instead of crashing
Fixes
Sentry TWENTY-SERVER-FDQ
Sonarly https://sonarly.com/issue/6564
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Workflow crons take a few minutes to run. Loading each repo takes ~200
to 300ms locally. Adding a lite mode so it takes less than 100ms.
Also doing batch promises.
Finally, cleaning runs timeout when there are too many. Doing batches as
well.
## Summary
- add a working `Forgot your password?` flow on `app.twenty.com` sign-in
- keep existing workspace-domain reset behavior
- when triggered without workspace context, resolve a workspace from the
user when possible, otherwise fallback to `app.twenty.com` reset URL
## Backend
- make `workspaceId` optional in `emailPasswordResetLink` input
- allow nullable `workspaceId` in password reset token DTO
- update reset token generation to accept optional `workspaceId`
- when missing, resolve first workspace by user membership
- if no workspace is found, persist token with `workspaceId = null`
- send reset links via:
- workspace URL when `workspaceId` exists
- app front URL + reset path when `workspaceId` is null
## Frontend
- make reset-link mutation `workspaceId` variable optional
- regenerate/patched generated metadata types accordingly
- add `Forgot your password?` in global password step
- allow reset request without workspace context in
`useHandleResetPassword`
- make reset page auto sign-in domain-aware (`workspace` vs `app`)
- apply design-system spacing above the global forgot-password link
(`theme.spacing(4)`)
## Tests
- extend reset-password service tests for:
- explicit workspace id
- inferred workspace when workspace id is missing
- app-domain fallback when no workspace is found
- extend reset-password hook tests for with/without workspace context
- add focused global form test for forgot-password link rendering/click
behavior
## Product behavior for users with multiple workspaces
- no workspace chooser is shown in this flow
- backend uses the first resolvable workspace membership for the
reset-link domain
- password change remains account-level and works across all workspaces
Feature has been tested and is working
<img width="3268" height="2106" alt="CleanShot 2026-02-26 at 14 09
14@2x"
src="https://github.com/user-attachments/assets/b5db3bed-f3aa-4d35-b54e-66e4d99141f9"
/>
---------
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
## Summary
This PR improves Linaria/WYW pre-build speed and continues the migration
of `twenty-ui` components away from runtime `ThemeContext` reads toward
static CSS variables and theme constants.
### Linaria/WYW profiling plugin improvements (`twenty-shared`)
- **Babel JIT warmup**: added a `buildStart` warmup step that triggers
WYW's Babel JIT compilation before the real build starts, so the first
real file doesn't pay the cold-start penalty
- **`configResolved` hook**: detects dev vs prod mode and resolves the
correct warmup file path relative to `config.root`
- **Dev-only per-file logging**: slow file warnings are now gated behind
`isDevMode`, keeping production/CI build output clean
- **`closeBundle` summary**: moved the final top-slow-files report to
`closeBundle` for accurate end-of-build reporting
- **Removed noisy progress interval logging** in favor of the warmup log
+ final summary
### Migration from `ThemeContext` to static CSS variables / constants
Across `twenty-ui`, replaced runtime `useTheme()` reads with:
- `themeCssVariables` CSS custom properties (colors, spacing)
- Hard-coded design-system constants (`ICON.size.md` → `16`,
`ICON.stroke.sm` → `1.6`) so components no longer need a React context
at render time — enabling Linaria static extraction
**Components migrated:**
- `Button`, `AnimatedButton`, `LightButton`, `LightIconButton`,
`AnimatedLightIconButton`, `ButtonIcon`, `ButtonSoon`
- `ProgressBar` (Framer Motion width animation → CSS `transition`)
- `Info`, `HorizontalSeparator`, `LinkChip`
- `MenuPicker`, `MenuItemLeftContent`, `MenuItemIconWithGripSwap`,
`NavigationBarItem`
- `JsonArrow`, `JsonNestedNode`
- `ModalHeader`
### Other
- Added `aria-valuenow` to `ProgressBar` for accessibility
- `VisibilityHidden` component updated to inline accessibility styles
## PR Description
- Uses `expr-eval` to enable front components (SDK plugins) to define
conditional availability as declarative expressions.
- Moves shared types and constants to `twenty-shared`
- Introduces a `conditionalAvailabilityExpression` field on
`CommandMenuItemEntity`, allowing command menu items to store an
`expr-eval` compatible expression string that is evaluated against a
CommandMenuContext to determine if the item should be shown.
- Creates an esbuild transform plugin
`conditional-availability-transform-plugin` in `twenty-sdk` that
converts TypeScript conditional availability expressions into
`expr-eval` compatible syntax at build time, so SDK developers can write
natural TS expressions that get transformed to evaluable strings.
- Removes deprecated `forceRegisteredActionsByKey` state and its usage.
- Creates `useCommandMenuContext` hook that builds the full
`CommandMenuContext` object from React state, which is then passed to
`useCommandMenuItemFrontComponentActions` for evaluating conditional
availability expressions.
Closes https://github.com/twentyhq/core-team-issues/issues/1627
**FilterArgProcessor consolidation:**
Refactored to both validate AND transform filter values in a single pass
Coerced string inputs to native types (e.g., "1" → 1, "true" → true -
useful for Rest input)
Returns transformed filter instead of just validating
Removed overrideFilterByFieldMetadata calls from all computeArgs methods
**QueryRunnerArgsFactory cleanup**
**Testing:**
Add unit testing
uncomment integration tests
## Summary
- Move Modal UI components (`Modal`, `ModalContent`, `ModalHeader`,
`ModalFooter`, `ModalBackdrop`) from `twenty-front` to `twenty-ui` as
stateless, reusable components
- Create `ModalStatefulWrapper` in `twenty-front` that connects Jotai
state (`isModalOpenedComponentState`) to the stateless `Modal` via an
`isOpen` prop
- Rename `modalVariant` prop to `overlay` with clearer values: `'dark'`
(default), `'light'` (in-container), `'transparent'` (invisible panel).
Remove unused `'medium'` overlay
- Rename `modalId` to `modalInstanceId` across the entire modal zone
(~30 consumer files)
- Extract `ModalProps` to its own file in
`twenty-ui/types/ModalProps.ts`; extract `ModalStatefulWrapperProps` to
its own file using `Pick<ModalProps, ...>` for shared props
- Extract `ModalBackdrop` to its own file and export from `twenty-ui`;
use it in `UserOrMetadataLoader` instead of a local styled component
- Use `ModalFooter` in `StepNavigationButton` and `ModalHeader` in
`SpreadsheetImportStepperContainer` instead of duplicated `styled.div`
definitions
- Remove unused `onClose` prop from stateless `Modal`; fix `typeof
document` guard in `ModalStatefulWrapper`
- Split shared types into individual files: `ModalSize.ts`,
`ModalPadding.ts`, `ModalOverlay.ts`
- Extract wyw profiling instrumentation from `vite.config.ts` into
reusable `createWywProfilingPlugin` with parametrized threshold and
improved logging
- Delete old `Modal.tsx`, `Modal.styles.ts`, `ModalContent.tsx`,
`ModalHeader.tsx`, `ModalFooter.tsx` from `twenty-front`
- Add comprehensive Storybook stories in `twenty-ui` covering Default,
Confirmation, Small, ExtraLarge, Closed, and Interactive variants
# Introduction
Adding integration test scaffold to the create twenty app and an example
to the hello world app
This PR also fixes all the sdk e2e tests in local
## `HELLO_WORLD`
Removed the legacy implem in the `twenty-apps` folder, replacing it by
an exhaustive app generation
## Next step
Will in another PR add workflows for CI testing
## Open question
- Should we still add vitest config and dep even if the user did not ask
for the integration test example ? -> currently we don't
- That's the perfect timing to identify if we're ok to handle seed
workspace authentication with the known api key
## Summary
Completes the migration of the frontend styling system from **Emotion**
(`@emotion/styled`, `@emotion/react`) to **Linaria** (`@linaria/react`,
`@linaria/core`), a zero-runtime CSS-in-JS library where styles are
extracted at build time.
This is the final step of the migration — all ~494 files across
`twenty-front`, `twenty-ui`, `twenty-website`, and `twenty-sdk` are now
fully converted.
## Changes
### Styling Migration (across ~480 component files)
- Replaced all `@emotion/styled` imports with `@linaria/react`
- Converted runtime theme access patterns (`({ theme }) => theme.x.y`)
to build-time `themeCssVariables` CSS custom properties
- Replaced `useTheme()` hook (from Emotion) with
`useContext(ThemeContext)` where runtime theme values are still needed
(e.g., passing colors to non-CSS props like icon components)
- Removed `@emotion/react` `css` helper usages in favor of Linaria
template literals
### Dependency & Configuration Changes
- **Removed**: `@emotion/react`, `@emotion/styled` from root
`package.json`
- **Added**: `@wyw-in-js/babel-preset`, `next-with-linaria` (for
twenty-website SSR support)
- Updated Nx generator defaults from `@emotion/styled` to
`@linaria/react` in `nx.json`
- Simplified `vite.config.ts` (removed Emotion-specific configuration)
- Updated `twenty-website/next.config.js` to use `next-with-linaria` for
SSR Linaria support
### Storybook & Testing
- Removed `ThemeProvider` from Emotion in Storybook previews
(`twenty-front`, `twenty-sdk`)
- Now relies solely on `ThemeContextProvider` for theme injection
### Documentation
- Removed the temporary `docs/emotion-to-linaria-migration-plan.md`
(migration complete)
- Updated `CLAUDE.md` and `README.md` to reflect Linaria as the styling
stack
- Updated frontend style guide docs across all locales
## How it works
Linaria extracts styles at build time via the `@wyw-in-js/vite` plugin.
All expressions in `styled` template literals must be **statically
evaluable** — no runtime theme objects or closures over component state.
- **Static styles** use `themeCssVariables` which map to CSS custom
properties (`var(--theme-color-x)`)
- **Runtime theme access** (for non-CSS use cases like icon `color`
props) uses `useContext(ThemeContext)` instead of Emotion's `useTheme()`
- apollo enrich application (via OAuth 2)
- add applicationId to var env in logic function executor
- update `getDefaultUrl` logic
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
<img width="450" height="212" alt="Capture d’écran 2026-03-03 à 11 41
54"
src="https://github.com/user-attachments/assets/b2c29a48-7dc0-4b16-a085-8f305d21f7ca"
/>
New status `FAIL_SAFE` added. This status propagates to the following
nodes until reaching the iterator, that will start the new iteration.
The difference with `SKIP` is that, when the parent nodes have at least
one `FAIL_SAFE`, it becomes `FAIL_SAFE` too. While a parent 1 `SKIP` +
parent 2 `SUCCESS` => to be executed.
I also thought about just going back to the iterator as a break would,
but since we have branches, it may lead to inconsistent statuses with
parallel updates.
## Summary
- Replaces all `depot-ubuntu-24.04` runners with `ubuntu-latest`
- Replaces all `depot-ubuntu-24.04-8` runners with
`ubuntu-latest-8-cores`
- Updates storybook build cache keys in ci-front.yaml to reflect the
runner name change
Reverts the temporary Depot migration introduced in #18163 / #18179
across all 23 workflow files.
# Introduction
Allow a consumer call the commands programmatically instead of passing
by the exec
To do so extract from the command definition all the core logic, created
a new error api that allow keeping same error logs granularity than
before
## Usage
```ts
import { authLogin, appUninstall, functionExecute } from 'twenty-sdk/cli';
const result = await authLogin({
apiKey: 'my-key',
apiUrl: 'https://my-twenty.com',
});
if (!result.success) {
throw new Error(result.error);
}
```
## `app:build`
Introduced a new command that will allow building the whole project
without any watch setup
- Build and validate manifest
- Get or create app
- Synchronize manifest with twenty-sdk stub and no typecheck
- generate client
- Run typecheck
- Synchronize manifest again
# Introduction
We need to build and validate the flat entity operation in the following
order delete update and create
For example if not, if a created field has the same name than a deleted
one than it will fail whereas it should not
Closes#17089
### 1. Can't reopen record after having navigated to its show page
After opening a record in the show page from the command menu and going
back to the index, clicking the same record again did nothing. The
command menu navigation stack was not cleared when opening in the show
page, so the "already open" check skipped reopening. We now clear the
command menu navigation stack before navigating to the show page (in
`RecordShowRightDrawerOpenRecordButton`), so the same record can be
reopened from the index.
### 2. Row doesn't highlight when opening command menu after return from
show page
After returning from the record show page to the index, the first row
click opened the command menu but the row did not highlight. The "side
panel close" event was emitted not only when the panel actually closed,
but also when opening the command menu (cleanup ran with
`isCommandMenuClosing` and always emitted the event). Listeners like
`RecordTableDeactivateRecordTableRowEffect` then deactivated the row. We
now emit the side panel close event only when the close animation
actually completes (`CommandMenuSidePanelForDesktop`), and skip emitting
it when cleanup is run from the open path (`useNavigateCommandMenu`
passes `emitSidePanelCloseEvent: false`). The table still deactivates
the row when the user closes the panel, but no longer when they open the
command menu by clicking a row.
Fixes#13838
When creating a note from the command menu side panel (e.g. clicking
"Add Note" in a related notes section on an Opportunity/company/people
page), the title field was not auto-focused — focus point went to body
instead.
## Root Cause
When a record opens in the side panel, there is no page navigation, so
`PageChangeEffect` (which handles title auto-focus for full-page views)
never runs. `openNewRecordTitleCell()` was simply never called for the
side-panel path.
## Fix
`openRecordInCommandMenu` is the single entry point for all side-panel
record opens, so title auto-focus is handled there once for all callers.
Previously, `useCreateNewIndexRecord` called `openRecordInCommandMenu`
and then called `openNewRecordTitleCell` separately, which would have
caused a double invocation after this fix. The redundant call has been
removed.
## Before
https://github.com/user-attachments/assets/df0d9e4f-dc25-4a0d-a49e-898a14f9c0a0
## After
https://github.com/user-attachments/assets/1a5044f7-6bb7-4333-8934-c1081b935e97
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
## Summary
- Implement a return-to-path mechanism that preserves the user's
intended destination across authentication flows (login, magic link,
cross-domain redirects)
- Uses layered persistence: Jotai atom (in-memory), sessionStorage with
TTL (tab-switch resilience), URL query parameter (cross-domain
propagation)
- Includes path validation to prevent open redirects, automatic cleanup
after successful login, and comprehensive test coverage
- Replaces the unused `previousUrlState` with a robust
`returnToPathState` system
## Test plan
- [ ] Visit a deep link (e.g. `/objects/tasks`) while logged out —
should redirect to login, then back to `/objects/tasks` after logging in
- [ ] Visit an OAuth authorize link while logged out — should redirect
to login, then to the authorize page
- [ ] Test magic link flow: click sign-in link that opens new tab —
should still redirect to original destination
- [ ] Test cross-domain: visit `app.twenty.com/objects/tasks` — should
preserve path through workspace domain redirect
- [ ] Verify auth/onboarding paths are excluded from being saved as
return paths
- [ ] Verify return-to-path is cleared after successful navigation
- [ ] All 215 existing `usePageChangeEffectNavigateLocation` tests pass
Made with [Cursor](https://cursor.com)
This PR adds an explicit role selector to the "Invite by email" flow,
requires a role choice before sending, and stores the selected role with
each invitation. The backend now accepts and persists `roleId` on
invitations and applies it when the invite is accepted, while keeping it
optional to avoid breaking existing clients and legacy invites.
---
### Frontend
- **Settings → Members → Invite by email**
- New **Role** dropdown (same `Select` pattern as member/API key role
selectors) between the email input and Invite button.
- Roles are loaded via `SettingsRolesQueryEffect` and
`settingsAllRolesSelector`; only roles with `canBeAssignedToUsers` are
shown.
- Role is **required**: form validates `roleId` (e.g.
`z.string().min(1)`) and the Invite button is disabled until a role is
selected and emails are valid.
- `WorkspaceInviteTeam` receives `roles` as a prop from the parent;
layout is responsive (e.g. stacked on small viewports).
- **Pending invitations table**
- New **Role** column showing the invitation’s role label (or "Unknown
role" for legacy invites without `roleId`), using the same roles source
for lookup.
- **Onboarding invite step**
- When sending invites during onboarding, the workspace **default role**
is used when available (`currentWorkspace?.defaultRole?.id`), so no role
selector is added there.
- **GraphQL**
- `sendInvitations` mutation accepts optional `roleId`;
`findWorkspaceInvitations` and resend mutation responses include
`roleId` on `WorkspaceInvitation`. Frontend types (e.g.
`WorkspaceInvitation`, hook variables) updated accordingly.
---
### Backend
- **API**
- `SendInvitationsInput` has an **optional** `roleId` (UUID, nullable).
The resolver normalises `null` to `undefined` so existing callers and
legacy flows are not broken.
- **Validation (when `roleId` is provided)**
- Role checks are centralised in **RoleValidationService**
(`RoleValidationModule`, in `metadata-modules/role-validation/`). It
validates that the role exists in the workspace and has
`canBeAssignedToUsers`, and throws a permissions-style error otherwise.
This avoids circular dependencies (e.g. `RoleModule` imports
`UserWorkspaceModule`, so invite/accept flows cannot depend on
`RoleModule`).
- **Send flow:** `WorkspaceInvitationResolver` and
`WorkspaceInvitationService.sendInvitations` both call
`RoleValidationService.validateRoleAssignableToUsersOrThrow` when
`roleId` is present (resolver before calling the service; service again
before creating tokens so that **resend** also validates the stored role
and fails fast if the role was deleted or made unassignable).
- **Accept flow:**
`UserWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace` uses the
same service in `resolveRoleIdForNewMember` when an invitation provides
a `roleId`, then falls back to `workspace.defaultRoleId` when not.
Role/default is resolved and validated before any user/workspace/member
creation.
- **Persistence**
- Invitation app tokens store `roleId` in `context` next to `email`
(`context: { email, roleId? }`). `generateInvitationToken` and
`createWorkspaceInvitation` accept an optional `roleId` and only add it
to `context` when defined.
- **Resend**
- Resend passes the existing invitation’s `context.roleId` into
`sendInvitations`. The service validates that role (when present) before
creating the new token, so if the role was deleted or made unassignable,
resend fails with a clear error instead of sending a broken link.
- **Response shape**
- `SendInvitationsOutput.result` remains `WorkspaceInvitation[]`. When
`usePersonalInvitation` is false we only push full invitation records
(from `castAppTokenToWorkspaceInvitationUtil`), so the result always
matches the GraphQL type (`id`, `email`, `roleId`, `expiresAt`).
- **Modules**
- `WorkspaceInvitationModule` and `UserWorkspaceModule` import
**RoleValidationModule** (not `RoleModule`) and inject
**RoleValidationService** for validation. `RoleModule` imports
`RoleValidationModule` and `RoleService` delegates to
`RoleValidationService` for the same validation where the module graph
allows.
---
### Backward compatibility
- **Optional `roleId`**: Clients that don’t send `roleId` (or send
`null`) are unchanged; invitations are created without a role and the
accept flow uses the workspace default role.
- **Legacy invitations**: App tokens with only `context.email` still
work; `context.roleId` is optional and the UI can show e.g. "Unknown
role" for those in the pending-invitations table.
## Summary
Cleans up the chip component hierarchy in `twenty-ui`:
- **Fix twenty-ui Storybook** — The `wyw-in-js` Vite plugin crashed on
`/@react-refresh` virtual module. Fixed by setting `enforce: 'pre'` so
it runs before the React refresh plugin injects virtual imports.
- **Rename `AvatarChip` → `AvatarOrIcon`** — The old name was
misleading. This component is not a chip — it's a polymorphic renderer
that displays either an `Avatar` (image/initials) or an `Icon` (plain or
with colored background). It's typically slotted into `Chip`/`LinkChip`
as `leftComponent`.
- **Move `rightComponentDivider` to `Chip`/`LinkChip`** — The vertical
separator between chip content and a right action (e.g. a close button)
is a chip layout concern, not an avatar concern. Added
`rightComponentDivider` boolean prop to `Chip` and `LinkChip`.
- **Remove `MultipleAvatarChip`** — Zero consumers in the codebase. The
command menu implements its own overlapping avatar layout.
- **Migrate raw icon usages** — `CalendarEventDetails` and `FileIcon`
(small size) now use `AvatarOrIcon` for consistent Chip icon rendering.
- **Enhance stories** — Full `CatalogDecorator` coverage for `Chip` and
`LinkChip` showing all variants, sizes, accents, and states.
## Component hierarchy
```
AvatarOrIcon (twenty-ui)
├── No Icon → renders Avatar (image or initials)
├── Icon + background → renders icon in colored square
└── Icon only → renders plain icon
Used as leftComponent/rightComponent in Chip or standalone
Chip (twenty-ui)
├── leftComponent (typically AvatarOrIcon)
├── label (with overflow tooltip)
├── rightComponentDivider (optional vertical separator)
└── rightComponent (e.g. close icon via AvatarOrIcon)
LinkChip (twenty-ui)
└── Wraps Chip inside a react-router <Link>
RecordChip (twenty-front)
└── Composes Chip/LinkChip + AvatarOrIcon with record data
```
## `Chip` API additions
| Prop | Type | Description |
|------|------|-------------|
| `rightComponentDivider` | `boolean` | Renders a vertical separator
before `rightComponent` |
## Stories
<img width="1032" height="576" alt="image"
src="https://github.com/user-attachments/assets/fe7c7666-9b16-4545-b87e-1b53e22d462d"
/>
## Context
Part 1 of migrating gridPosition in favor of typed position
FE should now always send both values to the BE and use both.
Next steps:
- Update the backend to enforce and validate the new position field + DB
migrations gridPositon -> position (type: GRID)
- Cleanup frontend usage
- Cleanup backend
If-else branches cannot be recreated once deleted. Only else-if branches
can. On if-else branches removal, we now remplace the node by an empty
node instead of only deleting
Also fixing nested if-else.
## Summary
Follow-up to #18267. Hardens the OAuth implementation with security
fixes identified during audit:
**P0 — Critical:**
- Bind authorization codes to `client_id` in context to prevent auth
code injection (RFC 6749 §4.1.3)
- Store PKCE `code_challenge` directly in auth code context instead of a
separate `CodeChallenge` token — cryptographically binds the challenge
to its code
- Enforce `code_verifier` when `code_challenge` was used during
authorization
- Hash authorization codes (SHA-256) before storage to prevent exposure
if DB is compromised
- Add `Cache-Control: no-store` + `Pragma: no-cache` headers on token
responses (RFC 6749 §5.1)
- Add rate limiting on `/oauth/token` endpoint (20 req/min per client
via existing `ThrottlerService`)
**P1 — High:**
- Return HTTP 401 for `invalid_client` errors instead of 400 (RFC 6749
§5.2)
- Verify refresh tokens belong to the presenting client (cross-client
token theft prevention)
- Limit fields exposed by public `findApplicationRegistrationByClientId`
query to only what the frontend needs (`id`, `name`, `logoUrl`,
`websiteUrl`, `oAuthScopes`)
- Require `API_KEYS_AND_WEBHOOKS` permission for
`createApplicationRegistration` mutation
**P2/P3 — Medium/Low:**
- Add error handling and loading states to frontend Authorize page
- Rename redirect URL param from `authorizationCode` to `code` (RFC
standard)
- Add unit tests for `validateRedirectUri` utility (8 test cases)
## Test plan
- [ ] Existing OAuth integration tests updated for all changes (hashed
codes, context-based PKCE, client binding, 401 status codes, cache
headers)
- [ ] New test: auth code rejected when presented by a different client
- [ ] New test: refresh token rejected when presented by a different
client
- [ ] New test: `code_verifier` required when PKCE was used in
authorization
- [ ] New test: `Cache-Control: no-store` header present on responses
- [ ] New unit tests for `validateRedirectUri` (HTTPS, localhost,
fragments, invalid URIs)
- [ ] Verify frontend authorize page shows errors gracefully
Made with [Cursor](https://cursor.com)
- Fixes self host application
- add new telemetry information
- add serverId to identify a server instance
- remove .twenty from git tracking
- tree-shake "twenty-sdk" usage in built logic functions and front
components
- fix "twenty-sdk" version usage
- fix twenty-zapier cli
---------
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
The `repository_dispatch` API endpoint requires `contents: write`
permission on the GITHUB_TOKEN, not `actions: write`. Our security
hardening PR inadvertently changed this to `contents: read`, breaking
the self-dispatch to the keepalive workflow.
Made-with: Cursor
## Summary
- Fix expression injection vulnerabilities in composite actions
(`restore-cache`, `nx-affected`) and workflow files (`claude.yml`)
- Reduce overly broad permissions in `ci-utils.yaml` (Danger.js) and
`ci-breaking-changes.yaml`
- Restructure `preview-env-dispatch.yaml`: auto-trigger for members,
opt-in for contributor PRs via `preview-app` label (safe because
keepalive has no write tokens)
- Isolate all write-access operations (PR comments, cross-repo posting)
to a new dedicated
[`twentyhq/ci-privileged`](https://github.com/twentyhq/ci-privileged)
repo via `repository_dispatch`, so that workflows in twenty that execute
contributor code never have write tokens
- Create `post-ci-comments.yaml` (`workflow_run` bridge) to dispatch
breaking changes results to ci-privileged, solving the [fork PR comment
issue](https://github.com/twentyhq/twenty/pull/13713#issuecomment-3168999083)
- Delete 5 unused secrets and broken `i18n-qa-report` workflow
- Remove `TWENTY_DISPATCH_TOKEN` from twenty (moved to ci-privileged as
`CORE_TEAM_ISSUES_COMMENT_TOKEN`)
- Use `toJSON()` for all `client-payload` values to prevent JSON
injection
## Security model after this PR
| Workflow | Executes fork code? | Write tokens available? |
|----------|---------------------|------------------------|
| preview-env-keepalive | Yes | None (contents: read only) |
| preview-env-dispatch | No (base branch) | CI_PRIVILEGED_DISPATCH_TOKEN
only |
| ci-breaking-changes | Yes | None (contents: read only) |
| post-ci-comments (workflow_run) | No (default branch) |
CI_PRIVILEGED_DISPATCH_TOKEN only |
| claude.yml | No (base branch) | CI_PRIVILEGED_DISPATCH_TOKEN,
CLAUDE_CODE_OAUTH_TOKEN |
| ci-utils (Danger.js) | No (base branch) | GITHUB_TOKEN (scoped) |
All actual write tokens (`TWENTY_PR_COMMENT_TOKEN`,
`CORE_TEAM_ISSUES_COMMENT_TOKEN`) live in `twentyhq/ci-privileged` with
strict CODEOWNERS review and branch protection.
## Test plan
- [ ] Verify preview environment comments still appear on member PRs
- [ ] Verify adding `preview-app` label triggers preview for contributor
PRs
- [ ] Verify breaking changes reports still post on PRs (including fork
PRs)
- [ ] Verify Claude cross-repo responses still post on core-team-issues
- [ ] Confirm ci-privileged branch protection is enforced
## Summary
- **Settings selector**: The Settings navigation item is now rendered as
a `<button>` (via `NavigationDrawerItem` with `onClick`) instead of an
`<a>` link (with `to`). Updated `leftMenu.ts` POM and
`create-kanban-view.spec.ts` to use `getByRole('button', { name:
'Settings' })`.
- **create-record URL field**: The Linkedin field interaction was
missing an initial label click to trigger the hover portal rendering.
Added `recordFieldList.getByText('Linkedin').first().click()` before the
value click, matching the pattern used by the working Emails field.
## Test plan
- [ ] E2E `signup_invite_email.spec.ts` passes (uses
`leftMenu.goToSettings()`)
- [ ] E2E `create-kanban-view.spec.ts` passes (uses Settings click
directly)
- [ ] E2E `create-record.spec.ts` passes (Linkedin URL field
interaction)
- [ ] Existing passing E2E tests remain green
Made with [Cursor](https://cursor.com)
## Summary
- Fixes a script injection vulnerability in the `claude-cross-repo`
job's `actions/github-script` step where `${{ steps.prompt.outputs.repo
}}` and `${{ steps.prompt.outputs.issue_number }}` were interpolated
directly into JavaScript string literals. A crafted dispatch payload
could inject arbitrary JavaScript with access to
`secrets.TWENTY_DISPATCH_TOKEN`.
- Values are now passed via `env:` and accessed through `process.env`,
which treats them as data rather than code.
## Context
Motivated by the [hackerbot-claw
campaign](https://www.stepsecurity.io/blog/hackerbot-claw-github-actions-exploitation)
which exploited similar `${{ }}` expression injection patterns in
workflows at Microsoft, DataDog, and CNCF projects.
The broader analysis found that our workflow is **not vulnerable** to
the primary attack vector (Pwn Request via `pull_request_target` +
untrusted checkout), and `claude-code-action` already gates on write
access internally. This expression injection in the cross-repo dispatch
job was the only concrete vulnerability identified.
## Test plan
- [ ] Verify the `claude-cross-repo` job still posts comments back to
the source issue after a dispatch run
- [ ] Confirm `TARGET_REPO` and `TARGET_ISSUE` env vars are correctly
resolved from step outputs
Made with [Cursor](https://cursor.com)
## Migrate twenty-ui from Emotion to Linaria
Completes the migration of all `twenty-ui` components from Emotion
(runtime CSS-in-JS) to Linaria (zero-runtime, CSS extracted at build
time).
- Replaced `@emotion/styled` with `@linaria/react` across ~170 files
- Removed all Emotion dependencies from `twenty-ui`
- Introduced a CSS custom properties-based theme system:
`themeCssVariables` where every leaf is a `var(--t-xxx)` reference,
injected onto `document.documentElement` by
`ThemeCssVariableInjectorEffect`
- No more `theme` prop threading — styled components reference
`themeCssVariables.x.y` directly at build time
- Updated `twenty-front` consumers to remove `theme={theme}` prop
passing
**Before / After:**
```tsx
// Emotion
color: ${({ theme }) => theme.font.color.primary};
padding: ${({ theme }) => theme.spacing(4)};
// Linaria
color: ${themeCssVariables.font.color.primary};
padding: ${themeCssVariables.spacing[4]};
```
### Theme architecture
Two build-time utilities produce the theme system:
- **`buildThemeReferencingRootCssVariables`** — walks the theme object
and builds a nested mirror where every leaf is a `var(--t-xxx)` string
(evaluated at build time by wyw-in-js)
- **`prepareThemeForRootCssVariableInjection`** — walks the runtime
theme and collects flat `[--css-variable-name, value]` pairs, injected
onto `document.documentElement` by `ThemeCssVariableInjectorEffect`
Both share naming conventions (`camelToKebab`, `SPACING_VALUES`,
`formatSpacingKey`) and are unit tested.
### Spacing cleanup
Spacing scale now uses integers 0–32 (generated via loop), with `0.5`
and `1.5` as the only fractional exceptions. All other fractional
spacing usages (`0.25`, `0.75`, `1.25`, `2.5`, `3.5`) were replaced with
literal pixel values across ~20 twenty-front files.
### Framer Motion integration
Linaria doesn't support `styled(motion.div)` — wrapping a motion element
with `styled()` causes the component body to be stripped at build time.
Instead, we define the styled component first, then wrap it with
`motion.create()`:
```tsx
const StyledBarBase = styled.div`
background-color: ${themeCssVariables.font.color.primary};
height: 100%;
`;
const StyledBar = motion.create(StyledBarBase);
```
### Block interpolations
Linaria doesn't support interpolations that return multiple CSS
declarations (Linaria wraps the entire block in a single `var()`,
producing invalid CSS). These were split into individual property
interpolations:
```tsx
// Emotion — single interpolation returning multiple declarations
border-left: ${({ divider, theme }) => {
const border = `1px solid ${theme.border.color.light}`;
return divider ? `border-${divider}: ${border}` : '';
}}
// Linaria — one interpolation per property
border-left: ${({ divider }) =>
divider === 'left' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
border-right: ${({ divider }) =>
divider === 'right' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
```
### Dynamic styles via CSS variables
When a component needs to compute styles from multiple props with
complex branching logic (e.g. `Button` combining `variant`, `accent`,
`inverted`, `disabled`, `focus`, `position`), Linaria's prop
interpolations become unwieldy. In those cases we use a
`computeDynamicStyles` function that returns a `CSSProperties` object
injected via `style={}`, referenced from the static CSS with `var()`:
```tsx
const StyledButton = styled.button`
background: var(--btn-bg);
border-color: var(--btn-border-color);
&:hover { background: var(--btn-hover-bg); }
`;
const dynamicStyles = useMemo(() => {
const s = computeButtonDynamicStyles(variant, accent, ...);
return { '--btn-bg': s.background, '--btn-hover-bg': s.hoverBackground } as CSSProperties;
}, [variant, accent, ...]);
return <StyledButton style={dynamicStyles} />;
```
### CSS var + unit concatenation
CSS custom properties can't be concatenated with unit suffixes directly
(`var(--x)px` is invalid). Values that need units use `calc()`:
```tsx
// Broken
transition: background ${themeCssVariables.animation.duration.instant}s ease;
// Fixed
transition: background calc(${themeCssVariables.animation.duration.instant} * 1s) ease;
```
Adds color support for navigation menu items.
---------
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
Co-authored-by: Charles Bochet <charles@twenty.com>
## Summary
Add Home/Chat tabs and a dedicated threads list in the navigation
drawer.
## Changes
- **Navbar tabs:** Tabs in the drawer to switch between Home and Chat
(with “New chat” button). Shown on desktop when expanded and on mobile
below the workspace selector.
- **Navbar threads list:** New `NavigationDrawerAIChatThreadsList` for
the Chat tab with date groups (Today / Yesterday / Older), thread rows
as `NavigationDrawerItem` (IconComment, title, timestamp). Shared
`useAIChatThreadClick` hook used by navbar and command menu; navbar
passes `resetNavigationStack: true`.
- **NavigationDrawerItem:** New `alwaysShowRightOptions` prop so the
timestamp is always visible (no hover-only).
---------
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
## Summary
- Migrate more hand-written test mocks to auto-generated data from a
real Twenty instance
- Add generators for views, billing plans, API keys; extend record
generator for workspace members, favorites, connected accounts, calendar
events
- Remove 9 hand-written mock files replaced by generated equivalents
- Update 16 test/story files to use generated data
- Fix WorkflowEditActionEmailBase story assertion to match configured
recipient email
## Test plan
- [x] Lint, typecheck, unit tests pass
- [ ] Storybook tests pass in CI
This PR pgrades all BlockNote packages (@blocknote/core,
@blocknote/react, @blocknote/mantine, @blocknote/server-util,
@blocknote/xl-docx-exporter, @blocknote/xl-pdf-exporter) to 0.47.0 and
adapts the codebase to the new API.
### Changes
- Dependency upgrades: Bumped all BlockNote packages to 0.47.0, added
required Mantine v8 peer dependencies, removed unnecessary prosemirror
resolutions
- Formatting toolbar: Replaced the manual reimplementation of
FormattingToolbarController (which handled visibility, positioning,
portal rendering, text-alignment-based placement, and a
dangerouslySetInnerHTML transition trick) with BlockNote's built-in
FormattingToolbarController. The toolbar buttons themselves are
unchanged.
- Side menu: Replaced manual drag handle menu positioning and rendering
(DashboardBlockDragHandleMenu, DashboardBlockColorPicker, and their
floating configs) with BlockNote's built-in SideMenuController,
DragHandleButton, and DragHandleMenu components. Deleted 4 files that
became dead code.
- Extension API migration: Replaced deprecated editor.suggestionMenus
and editor.formattingToolbar APIs with the new extension system
(SuggestionMenu, useExtensionState, editor.getExtension())
- Slash menu fixes: Filtered out BlockNote's new default "File" item
(added in 0.47) to avoid duplicates with our custom one; added icon
mappings for new block types (Toggle List, Divider, Toggle Headings,
Headings 4-6)
- Server-side: Switched @blocknote/server-util to dynamic import() to
handle ESM-only transitive dependencies in CJS context
## Summary
Unifies test mocking tooling across Jest and Storybook, replaces
handcrafted mock data with auto-generated server-fetched data, and
restructures the mock data generation script for maintainability.
### Mock data generation
- Split `generate-mock-data.ts` into three focused modules under
`scripts/mock-data/`:
- `utils.ts` — shared authentication, GraphQL client, and file writer
- `generate-metadata.ts` — fetches object metadata from `/metadata`
- `generate-record-data.ts` — fetches record data from `/graphql` using
metadata-driven dynamic queries
- The orchestrator (`generate-mock-data.ts`) authenticates once and
passes the token to both generators
- Company records are now fetched from the actual server (limited to 10
records) instead of being handcrafted
- Generated files are organized under `generated/metadata/objects/` and
`generated/data/companies/`
### Unified test utilities
- Consolidated Jest and MSW mocking into shared utilities that compose
production code (`prefillRecord`, `getRecordNodeFromRecord`,
`getRecordConnectionFromRecords`) with mock metadata
- Renamed `generateEmptyJestRecordNode` → `generateMockRecordNode` and
moved to `testing/utils/`
- Extracted `generateMockRecordConnection` into its own file
- Removed `sanitizeInputForPrefill` workaround (no longer needed with
correctly shaped generated data)
- Add media-specific events
- Extend `iFrame` with missing properties
- Enrich `SerializedEventData` with media-related target fields so
serialized events carry the media element state.
- Refactor the `remote-elements` code generator to support per-element
custom events
## Remove all Recoil references and replace with Jotai
### Summary
- Removed every occurrence of Recoil from the entire codebase, replacing
with Jotai equivalents where applicable
- Updated `README.md` tech stack: `Recoil` → `Jotai`
- Rewrote documentation code examples to use
`createAtomState`/`useAtomState` instead of `atom`/`useRecoilState`, and
removed `RecoilRoot` wrappers
- Cleaned up source code comment and Cursor rules that referenced Recoil
- Applied changes across all 13 locale translations (ar, cs, de, es, fr,
it, ja, ko, pt, ro, ru, tr, zh)
## Summary
The global `jotaiStore` singleton was shared across all tests without
reset, leaking state between test suites and causing unbounded heap
growth within each Jest worker.
- `getJestMetadataAndApolloMocksWrapper` now creates a **fresh Jotai
store per wrapper** (equivalent of RecoilRoot isolation), so each test
gets clean state
- Added `workerIdleMemoryLimit: '512MB'` to recycle workers that still
accumulate memory
- Set `maxWorkers: 3` for CI
## Performance
Measured on 48 test suites (command-menu + views + object-record hooks),
single worker:
| Metric | Before | After |
|---|---|---|
| Peak heap | **1519 MB** | **~530 MB** (-65%) |
| Final heap | **1519 MB** | **437 MB** (-71%) |
| Growth pattern | Monotonic (never freed) | Bounded sawtooth (recycled
at 512MB) |
## Fix SDK first-run build failure when generated API client is missing
### Problem
When running `twenty app:dev` for the first time, the build pipeline
crashes because:
1. The esbuild front-component watcher tries to resolve
`twenty-sdk/generated`, but the generated API client doesn't exist yet —
it's only created later in the pipeline after syncing with the server.
2. The typecheck plugin (`tsc --noEmit`) runs as a blocking esbuild
`onStart` hook, so even with stub files, type errors on the empty client
classes (e.g. `Property 'query' does not exist on type 'CoreApiClient'`)
cause the build to fail before the real client can be generated.
This creates a chicken-and-egg problem: the watchers need the generated
client to build, but the client is generated after the watchers produce
their output.
### Solution
**1. Stub generated client on startup**
Added `ensureGeneratedClientStub` to `ClientService` that writes minimal
placeholder files (`CoreApiClient`, `MetadataApiClient`) into
`node_modules/twenty-sdk/generated/` if the directory doesn't already
exist. This is called in `DevModeOrchestrator.start()` before any
watchers are created, so the `twenty-sdk/generated` import always
resolves.
**2. Skip typecheck on first sync round**
Made the esbuild typecheck plugin accept a `shouldSkipTypecheck`
callback. The orchestrator starts with `skipTypecheck = true` and passes
`() => this.skipTypecheck` through the watcher chain. After the real API
client is generated, the flag is flipped to `false`, so subsequent
rebuilds enforce full type checking with the real generated types.
This PR allows to handle ObjectMetadataItem, FieldMetadataItem and
NavigationMenuItem SSE events.
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
## Add codegen script for frontend test mock data
### Summary
- Adds a new `npx nx mock:generate twenty-front` and
`generate-mock-data.ts` script that fetches object metadata from a
running server's `/metadata` endpoint, authenticates with default seeds,
and writes the result to a generated TypeScript file
(`src/testing/mock-data/generated/mock-metadata-query-result.ts`). This
replaces hand-maintained mock metadata with server-sourced data,
ensuring tests always reflect the real schema.
- Updates all frontend tests to be compatible with the newly generated
metadata, fixing hard-coded GraphQL queries, Zod validation schemas,
snapshot expectations, and Apollo mock mismatches.
### What changed
**New files**
- `scripts/generate-mock-data.ts` — codegen script that authenticates
against the server, queries `/metadata` for all object metadata (with
explicit `__typename` at every level), and writes a typed `.ts` file.
- `project.json` — added `mock:generate` Nx target (`dotenv npx tsx
scripts/generate-mock-data.ts`).
**Schema validation updates**
- `objectMetadataItemSchema.ts` — added `universalIdentifier`, made
`duplicateCriteria` nullable.
- `fieldMetadataItemSchema.ts` — added `universalIdentifier`, `morphId`,
`morphRelations`, restructured relation schema.
- `indexMetadataItemSchema.ts` — added optional `isCustom` field.
# Introduction
Resolving each create|updated|deleted entities related entities
application through their universal foreingKey ( silently failing if not
found leaving the validator handling that )
In order to compute all the required application in the dependency flat
entity maps
## Tested
Creating a field on a custom local twenty-apps installed on the
workspace + view field
## Out of scope
- updated snapshot
- update graphql generated front
## Summary
Removes the `recoil` dependency entirely from `package.json` and
`twenty-front/package.json`, completing the migration to Jotai as the
sole state management library.
Removes all Recoil infrastructure: `RecoilRoot` wrapper from `App.tsx`
and test decorators, `RecoilDebugObserver`, Recoil-specific ESLint rules
(`use-getLoadable-and-getValue-to-get-atoms`,
`useRecoilCallback-has-dependency-array`), and legacy Recoil utility
hooks/types (`useRecoilComponentState`, `useRecoilComponentValue`,
`createComponentState`, `createFamilyState`, `getSnapshotValue`,
`cookieStorageEffect`, `localStorageEffect`, etc.).
Renames all `V2`-suffixed Jotai state files and types to their canonical
names (e.g., `ComponentStateV2` -> `ComponentState`,
`agentChatInputStateV2` -> `agentChatInputState`, `SelectorCallbacksV2`
-> `SelectorCallbacks`), and removes the now-redundant V1 counterparts.
Updates ~433 files across the codebase to use the renamed Jotai imports,
remove Recoil imports, and clean up test wrappers (`RecoilRootDecorator`
-> `JotaiRootDecorator`).
## Automated fix for [bug 5360](https://sonarly.com/issue/5360?type=bug)
**Severity:** `critical`
### Summary
When an existing user created via OAuth (with no password hash) attempts
to sign in through the signInUp password flow, their null passwordHash
is passed directly to bcrypt.compare, which throws an unhandled 'data
and hash arguments required' error instead of a user-friendly message.
### User Impact
Users who originally registered via Google or Microsoft OAuth and then
attempt to sign in using email/password through the signInUp flow cannot
authenticate. They see a server error instead of a helpful message like
'User was not created with email/password'. This blocks the user from
accessing the CRM entirely.
### Root Cause
Proximate cause: bcrypt.compare() at auth.util.ts:19 throws 'data and
hash arguments required' because the passwordHash argument is null.
1. Why did bcrypt throw? Because compareHash() was called with a null
passwordHash value. The compareHash function at auth.util.ts:19 passes
its arguments directly to bcrypt.compare without any null check.
2. Why was passwordHash null? Because SignInUpService.validatePassword()
at sign-in-up.service.ts:154 received a null passwordHash from
AuthService.validatePassword() at auth.service.ts:232. The value
userData.existingUser.passwordHash is null for users who were originally
created via an OAuth provider (Google, Microsoft) and never set a
password.
3. Why was there no null check before calling bcrypt? Because
AuthService.validatePassword() at line 229-233 does not check whether
userData.existingUser.passwordHash is null before passing it to
signInUpService.validatePassword(). The TypeScript type annotation says
passwordHash is string, but the UserEntity column is declared nullable:
true (user.entity.ts:73), so the runtime value can be null despite the
type system.
4. Why does this code path lack the null check when another path has it?
The validateLoginWithPassword() method at auth.service.ts:177 correctly
checks if (!user.passwordHash) and throws a user-friendly AuthException
'Incorrect login method'. This guard was added by Thomas Trompette on
2024-08-07 (commit 2abb6adb61). However, the signInUp flow's
validatePassword() method was introduced later by Antoine Moreaux on
2025-03-17 (commit bda835b9f8) without replicating this same defensive
check.
5. Why was this not caught? The signInUp validatePassword method was
written assuming the existingUser would always have a passwordHash when
the auth provider is Password. This assumption is incorrect because the
signInUp flow can match an existing OAuth-created user (who has no
passwordHash) when someone tries to sign up with email/password using an
email already registered via OAuth. No test covers this cross-provider
scenario in the signInUp path.
Root cause: The AuthService.validatePassword() method
(auth.service.ts:229-233) in the signInUp code path is missing a null
guard on userData.existingUser.passwordHash before passing it to
bcrypt.compare. The fix should check for null/undefined passwordHash and
throw a descriptive AuthException, mirroring the existing guard in
validateLoginWithPassword at line 177.
**Introduced by:** Antoine Moreaux on 2025-03-17 in commit
[`bda835b`](https://github.com/twentyhq/twenty/commit/bda835b9f82)
### Suggested Fix
Added a null check on userData.existingUser.passwordHash inside the
validatePassword private method of AuthService, immediately before
calling signInUpService.validatePassword. When passwordHash is null or
undefined (i.e. the user was created via OAuth and never set a
password), an AuthException with code INVALID_INPUT is thrown with the
user-friendly message 'User was not created with email/password'. This
mirrors the identical guard that already exists in
validateLoginWithPassword at line 177, preventing bcrypt.compare from
receiving a null argument and crashing with an unhandled error.
### Evidence
- **Code:**
[packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts:229
- if (userData.type === 'existingUser')
{](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts#L229)
---
*Generated by [Sonarly](https://sonarly.com)*
Co-authored-by: Sonarly Claude Code <claude-code@sonarly.com>
`twenty-sdk` and `create-twenty-app` are published as dual-format npm packages (ESM `.mjs` + CJS `.cjs`). Dependencies listed in `dependencies` are **externalized** by the Vite/Rollup build — they are not bundled, and consumers resolve them from `node_modules` at runtime.
This means **CJS-only dependencies break the ESM output**. When Rollup emits `import { foo } from 'cjs-package'`, Node.js ESM cannot resolve named exports from CommonJS modules, causing `SyntaxError: Named export 'foo' not found`.
## Rules
### Only add ESM-compatible dependencies
Before adding a new dependency to `package.json`, verify it supports ESM:
- Check for `"type": "module"` in its `package.json`
- Or check for an `"exports"` map with ESM entries
- Or check for a `"module"` field pointing to an ESM build
### Use native `node:fs/promises` for standard fs operations
```typescript
// ✅ Import native fs functions directly
import { readFile, writeFile, mkdir, rm, cp } from 'node:fs/promises';
import { createWriteStream, existsSync } from 'node:fs';
// ✅ Import only custom helpers from fs-utils (no native re-exports)
// ❌ Don't use fs-extra (CJS-only, breaks ESM bundle)
import * as fs from 'fs-extra';
// ❌ Don't use import * as fs from fs-utils (it doesn't re-export native fs)
import * as fs from '@/cli/utilities/file/fs-utils';
```
### Use `@/cli/utilities/string/kebab-case` instead of lodash
```typescript
// ✅ Use internal utility
import { kebabCase } from '@/cli/utilities/string/kebab-case';
// ❌ Don't use lodash single-function packages (CJS-only, unmaintained)
import kebabCase from 'lodash.kebabcase';
```
### When no ESM alternative exists
If a CJS-only package has no ESM replacement (e.g. `archiver`), add it to the `cjsOnlyPackages` list in `vite.config.node.ts` so it gets inlined into the bundle instead of externalized.
branchStateNote = '\n\n⚠️ **Note**: Could not merge with `main` due to conflicts. This comparison shows changes between the current branch and `main` as separate states.\n';
}
// Check if there are any breaking changes detected
let hasBreakingChanges = false;
let breakingChangeNote = '';
// Check for breaking changes in any of the diff files
- name:Fetch custom Github Actions and base branch history
uses:actions/checkout@v4
with:
fetch-depth:10
- name:Install dependencies
uses:./.github/actions/yarn-install
- name:Build twenty-shared
run:npx nx build twenty-shared
- name:Server / Run lint & typecheck
uses:./.github/actions/nx-affected
with:
tag:scope:backend
tasks:lint,typecheck
server-validation:
needs:server-build
timeout-minutes:30
runs-on:ubuntu-latest
services:
postgres:
image:twentycrm/twenty-postgres-spilo
@@ -55,21 +104,15 @@ jobs:
- name:Fetch custom Github Actions and base branch history
uses:actions/checkout@v4
with:
fetch-depth:0
fetch-depth:10
- name:Install dependencies
uses:./.github/actions/yarn-install
- name:Restore server setup
id:restore-server-setup-cache
- name:Restore server build cache
uses:./.github/actions/restore-cache
with:
key:${{ env.SERVER_SETUP_CACHE_KEY }}
key:${{ env.SERVER_BUILD_CACHE_KEY }}
- name:Build twenty-shared
run:npx nx build twenty-shared
- name:Server / Run lint & typecheck
uses:./.github/actions/nx-affected
with:
tag:scope:backend
tasks:lint,typecheck
- name:Server / Write .env
run:npx nx reset:env twenty-server
- name:Server / Build
@@ -84,10 +127,8 @@ jobs:
run:|
timeout 30s npx nx run twenty-server:worker || exit_code=$?
if [ $exit_code -eq 124 ]; then
# If timeout was reached (exit code 124), consider it a success
exit 0
elif [ $exit_code -ne 0 ]; then
# If worker failed for other reasons, fail the build
exit $exit_code
fi
- name:Server / Start
@@ -118,13 +159,13 @@ jobs:
exit 1
fi
- name:GraphQL / Check for Pending Generation
- name:Check for Pending Code Generation
run:|
# Run GraphQL generation commands
HAS_ERRORS=false
npx nx run twenty-front:graphql:generate
npx nx run twenty-front:graphql:generate --configuration=metadata
# Check if GraphQL generated files were modified
if ! git diff --quiet -- packages/twenty-front/src/generated packages/twenty-front/src/generated-metadata; then
echo "::error::GraphQL schema changes detected. Please run 'npx nx run twenty-front:graphql:generate' and 'npx nx run twenty-front:graphql:generate --configuration=metadata' and commit the changes."
echo "Please run 'npx nx run twenty-front:graphql:generate' and 'npx nx run twenty-front:graphql:generate --configuration=metadata' and commit the changes."
HAS_ERRORS=true
fi
npx nx run twenty-sdk:generate-metadata-client
if ! git diff --quiet -- packages/twenty-sdk/src/clients/generated/metadata; then
echo "::error::SDK metadata client changes detected. Please run 'npx nx run twenty-sdk:generate-metadata-client' and commit the changes."
echo ""
echo "The following SDK metadata client changes were detected:"
BODY="⚠️ Claude ran out of turns before creating a PR. Work has been pushed to [\`$BRANCH\`](https://github.com/${{ github.repository }}/tree/$ENCODED_BRANCH).\n\n[**Create PR →**]($PR_URL)"
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
**Example files (controlled by scaffolding mode):**
-`objects/example-object.ts` — Example custom object with a text field
-`fields/example-field.ts` — Example standalone field extending the example object
-`logic-functions/hello-world.ts` — Example logic function with HTTP trigger
@@ -108,16 +99,18 @@ In interactive mode, you can pick from:
-`views/example-view.ts` — Example saved view for the example object
-`navigation-menu-items/example-navigation-menu-item.ts` — Example sidebar navigation link
-`skills/example-skill.ts` — Example AI agent skill definition
-`__tests__/app-install.integration-test.ts` — Integration test that builds, installs, and verifies the app (includes `vitest.config.ts`, `tsconfig.spec.json`, and a setup file)
## Next steps
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
-Types are auto‑generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`.
-`CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty app:dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
## Publish your application
Applications are currently stored in `twenty/packages/twenty-apps`.
You can share your application with all Twenty users:
@@ -142,9 +135,11 @@ git push
Our team reviews contributions for quality, security, and reusability before merging.
## Troubleshooting
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
- Types not generated: ensure `yarn twenty app:dev` is running — it auto‑generates the typed client.
## Contributing
- See our [GitHub](https://github.com/twentyhq/twenty)
@@ -36,6 +36,17 @@ yarn twenty function:execute # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
```
## Integration Tests
If your project includes the example integration test (`src/__tests__/app-install.integration-test.ts`), you can run it with:
```bash
# Make sure a Twenty server is running at http://localhost:3000
yarn test
```
The test builds and installs the app, then verifies it appears in the applications list. Test configuration (API URL and API key) is defined in `vitest.config.ts`.
## LLMs instructions
Main docs and pitfalls are available in LLMS.md file.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.