Compare commits

...

656 commits

Author SHA1 Message Date
Abdullah.
7947b1b843
Fix self-hosting pricing page design. (#19930)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push docs to Crowdin / Push documentation to Crowdin (push) Waiting to run
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
Resolve:
https://discord.com/channels/1130383047699738754/1496108238909739079
2026-04-21 13:03:17 +00:00
Abdullah.
2b399fc94e
[Website] Fix flickering of faq illustration. (#19920)
Before:


https://github.com/user-attachments/assets/da189675-5de2-47be-b491-0d52eb11331e

After:


https://github.com/user-attachments/assets/7e382523-3539-4978-873c-e4ea79e264a7
2026-04-21 15:05:58 +02:00
github-actions[bot]
44ba7725ae
i18n - docs translations (#19934)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 14:46:37 +02:00
Paul Rastoin
65e01400c0
Cross version ci placeholder (#19932)
Created this empty workflow so it appears on main and is pickable from a
different branch to start testing the whole flow
2026-04-21 14:19:28 +02:00
neo773
62ea14a072
fix email workflow (#19929)
fixes JSON parse crash with proper resolution of variables and tiptap
rich text classification
2026-04-21 13:47:19 +02:00
github-actions[bot]
8cdd2a3319
i18n - docs translations (#19928)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 12:49:35 +02:00
Etienne
1db242d399
Website - small fixes (#19918)
- Fix release note link in footer
- Fix Talk to partner CTA in pricing page
2026-04-21 09:50:04 +00:00
Paul Rastoin
a583ee405b
Cross version and upgrade status docs (#19926) 2026-04-21 09:40:23 +00:00
github-actions[bot]
cd73088be6
i18n - docs translations (#19925)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 10:57:27 +02:00
Thomas des Francs
15938c1fca
Add 2.0.0 release changelog (#19923)
## Summary
- Add 2.0.0 release notes and illustrations to `twenty-website-new`
- Remove old release mdx files from the legacy `twenty-website`
- Fix the resources-menu "Releases" preview to pull the latest release
dynamically, using the first (hero) image of the latest mdx

## Test plan
- [ ] Open any page on `twenty-website-new`, hover "Resources" → the
Releases preview shows "See what shipped in 2.0.0" with the Build-an-app
hero illustration
- [ ] Visit `/releases` and confirm 2.0.0 renders with all five sections
and images
- [ ] Confirm legacy `twenty-website` no longer ships the deleted
release pages

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 08:45:00 +00:00
neo773
071980d511
Revert "fix compute folders to update util (#19749)" (#19921)
This reverts commit 64470baa1e
2026-04-21 08:02:24 +00:00
Abdul Rahman
e3d7d0199d
Fix side panel hotkeys breaking when opening records from table (#19849)
## Summary

- Fix side panel hotkeys (Ctrl+K, Escape, etc.) breaking when opening
records from the record index table
- Ensure `side-panel-focus` is always restored in the focus stack when
navigating within an already-open side panel
- Remove stale `globalHotkeysConfig` on `record-index` focus item that
persisted after the side panel closed

## Problem

When clicking records in the table to open them in the side panel,
`useLeaveTableFocus` called `resetFocusStackToRecordIndex` which wiped
the entire focus stack, including the `side-panel-focus` entry. Since
`openSidePanel` early-returned when the panel was already open,
`side-panel-focus` was never restored. Additionally,
`resetFocusStackToRecordIndex` set `enableGlobalHotkeysWithModifiers:
false` on the remaining `record-index` item when the side panel was
open, and this stale config persisted after the panel closed,
permanently blocking all hotkeys.

## Fix

- **`useNavigateSidePanel.ts`**: Move `pushFocusItemToFocusStack` before
the `isSidePanelOpened` early-return so the side panel's focus entry is
always present in the stack
- **`useResetFocusStackToRecordIndex.ts`**: Always set
`enableGlobalHotkeysWithModifiers: true` on the `record-index` item.
Hotkey scoping when the side panel is open is handled by
`side-panel-focus` sitting on top of the focus stack



https://github.com/user-attachments/assets/ad25befb-338d-4166-9580-18d4e92d6f9b
2026-04-21 07:44:00 +00:00
Félix Malfait
30b8663a74
chore: remove IS_AI_ENABLED feature flag (#19916)
## Summary
- AI is now GA, so the public/lab `IS_AI_ENABLED` flag is removed from
`FeatureFlagKey`, the public flag catalog, and the dev seeder.
- Drops every backend `@RequireFeatureFlag(IS_AI_ENABLED)` guard (agent,
agent chat, chat subscription, role-to-agent assignment, workflow AI
step creation) and the now-unused `FeatureFlagModule`/`FeatureFlagGuard`
wiring in the AI and workflow modules.
- Removes frontend gating from settings nav, role
permissions/assignment/applicability, command menu hotkeys, side panel,
mobile/drawer nav, and the agent chat provider so AI UI is always on.
Tests and generated GraphQL/SDK schemas updated accordingly.

## Test plan
- [x] `npx nx typecheck twenty-shared`
- [x] `npx nx typecheck twenty-server`
- [x] `npx nx typecheck twenty-front`
- [x] `npx nx lint:diff-with-main twenty-server`
- [x] `npx nx lint:diff-with-main twenty-front`
- [x] `npx jest --config=packages/twenty-server/jest.config.mjs
feature-flag`
- [x] `npx jest --config=packages/twenty-server/jest.config.mjs
workspace-entity-manager`
- [ ] Manual smoke test: AI features still accessible without any flag
row in `featureFlag`

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:49:46 +02:00
Félix Malfait
69868a0ab6
docs: remove alpha warning from apps pages except skills & agents (#19919)
## Summary
- Remove the \"Apps are currently in alpha\" warning from 8 pages under
`developers/extend/apps/` (getting-started, architecture/building,
data-model, layout, logic-functions, front-components, cli-and-testing,
publishing).
- Keep the warning on the Skills & Agents page only, and reword it to
scope it to that feature: \"Skills and agents are currently in alpha.
The feature works but is still evolving.\"

## Test plan
- [ ] Preview docs build and confirm the warning banner no longer
appears on the 8 pages above.
- [ ] Confirm the warning still renders on the Skills & Agents page with
the updated wording.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:49:37 +02:00
Félix Malfait
4f88aab57f
chore(website-new): reword FAQ copy on hosting and Organization plan (#19917)
## Summary
- Hosting FAQ: drops the inaccurate "most teams run it on our managed
cloud" claim and presents self-hosting and cloud as equal options.
- Pricing FAQ: replaces the awkward "for teams needing enterprise-grade
security" with "for teams that need finer access control", which more
accurately describes what SSO and row-level permissions do.

## Test plan
- [ ] Visually verify the FAQ section on the website renders the updated
copy.

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:19:18 +02:00
Félix Malfait
5d438bb70c
Docs: restructure navigation, add halftone illustrations, clean up hero images (#19728)
## Summary

- **New Getting Started section** with quickstart guide and restructured
navigation
- **Halftone-style illustrations** for User Guide and Developer
introduction cards using a Canvas 2D filter script
- **Removed hero images** (`image:` frontmatter + `<Frame><img>` blocks)
from all user-guide article pages
- **Cleaned up translations** (13 languages): removed hero images and
updated introduction cards to use halftone style
- **Cleaned up twenty-ui pages**: removed outdated hero images from
component docs
- **Deleted orphaned images**: `table.png`, `kanban.png`
- **Developer page**: fixed duplicate icon, switched to 3-column layout

## Test plan

- [ ] Verify docs site builds without errors
- [ ] Check User Guide introduction page renders halftone card images in
both light and dark mode
- [ ] Check Developer introduction page renders 3-column layout with
distinct icons
- [ ] Confirm article pages no longer show hero images at the top
- [ ] Spot-check a few translated pages to ensure hero images are
removed

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 09:13:55 +02:00
neo773
a1de37e424
Fix Email composer rich text to HTML conversion (#19872) 2026-04-21 08:52:05 +02:00
github-actions[bot]
a8d4039629
chore: sync AI model catalog from models.dev (#19914)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-21 08:32:40 +02:00
github-actions[bot]
4fd80ee470
i18n - translations (#19915)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 08:32:36 +02:00
Charles Bochet
8d24551e71
fix(settings): force display "Standard" and "Custom" for app chips (#19912)
## Summary
- Override the `AppChip` label so the Twenty standard application always
renders as `Standard` and the workspace custom application always
renders as `Custom`, instead of leaking each app's underlying name (e.g.
`Twenty Eng's custom application`).
- Detection mirrors the logic already used in
`useApplicationAvatarColors`, relying on
`TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER` /
`TWENTY_STANDARD_APPLICATION_NAME` and
`currentWorkspace.workspaceCustomApplication.id`.
- The `This app` label for the current application context and the
original `application.name` fallback for any other installed app are
preserved.

## Affected UI
- Settings → Data model → Existing objects (App column).
- Anywhere else `AppChip` / `useApplicationChipData` is used.
2026-04-21 08:26:26 +02:00
Charles Bochet
34e3b1b90b
feat(website-new): add robots.txt, sitemap.xml and legacy redirects (#19911)
## Summary

Application-side preparation so `twenty-website-new` can take over the
canonical `twenty.com` hostname from the legacy `twenty-website`
(Vercel) deployment without breaking SEO or existing inbound links.

### What's added

- **`src/app/robots.ts`** — serves `/robots.txt` and points crawlers
  at the new `/sitemap.xml`. Honours `NEXT_PUBLIC_WEBSITE_URL` with a
  `https://twenty.com` fallback.
- **`src/app/sitemap.ts`** — serves `/sitemap.xml` listing the
  canonical public routes of the new website (home, why-twenty,
  product, pricing, partners, releases, customers + each case study,
  privacy-policy, terms).
- **`next.config.ts` `redirects()`** — adds:
  - The existing `docs.twenty.com` permanent redirects from the legacy
    site (`/user-guide`, `/developers`, `/twenty-ui` and their nested
    variants).
  - 308-redirects for renamed/restructured pages so existing inbound
    links and Google results keep working:

| From | To |
|-------------------------------------|-----------------------------|
| `/story` | `/why-twenty` |
| `/legal/privacy` | `/privacy-policy` |
| `/legal/terms` | `/terms` |
| `/legal/dpa` | `/terms` |
| `/case-studies/9-dots-story` | `/customers/9dots` |
| `/case-studies/act-immi-story` | `/customers/act-education` |
| `/case-studies/:slug*` | `/customers` |
| `/implementation-services` | `/partners` |
| `/onboarding-packages` | `/partners` |

### What's intentionally **not** added

Routes that exist on the legacy site but have no equivalent on the
new website are left as honest 404s for now (we can decide on landing
pages later):

- `/jobs`, `/jobs/*`
- `/contributors`, `/contributors/*`
- `/oss-friends`

## Cutover order

1. Merge this PR.
2. Bump the website-new image tag in `twenty-infra-releases`
   (`prod-eu`) so the new robots / sitemap / redirects are live on
   `https://website-new.twenty.com`.
3. Smoke test on `https://website-new.twenty.com`:
   - `curl -sI https://website-new.twenty.com/robots.txt`
   - `curl -sI https://website-new.twenty.com/sitemap.xml`
- `curl -sI https://website-new.twenty.com/story` — expect 308 to
`/why-twenty`
- `curl -sI https://website-new.twenty.com/legal/privacy` — expect 308
to `/privacy-policy`
4. Merge the companion `twenty-infra` PR
([twentyhq/twenty-infra#589](https://github.com/twentyhq/twenty-infra/pull/589))
   so the ingress accepts `Host: twenty.com` and `Host: www.twenty.com`.
5. Flip the Cloudflare DNS records for `twenty.com` and `www` to the
   EKS NLB and purge the Cloudflare cache.
2026-04-21 08:26:11 +02:00
Abdullah.
e1c200527d
fix: pricing card cutoff on website (#19913)
Before:
<img width="1387" height="406" alt="image"
src="https://github.com/user-attachments/assets/902181f7-3b46-426c-a3e6-8adb706c4425"
/>

After:
<img width="1396" height="432" alt="image"
src="https://github.com/user-attachments/assets/dd0da62e-5d1a-4a89-b6f0-1a48c0573af0"
/>
2026-04-21 08:25:46 +02:00
Thomas des Francs
3ee1b528a1
Website last fixes (#19895)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 05:43:06 +00:00
Charles Bochet
6ef15713b1
Release v2.0.0 for twenty-sdk, twenty-client-sdk, and create-twenty-app (#19910)
## Summary
- Bump `twenty-sdk` from `1.23.0` to `2.0.0`
- Bump `twenty-client-sdk` from `1.23.0` to `2.0.0`
- Bump `create-twenty-app` from `1.23.0` to `2.0.0`
2026-04-21 07:40:44 +02:00
github-actions[bot]
dc50dbdb20
i18n - docs translations (#19909)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / docs-lint (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push docs to Crowdin / Push documentation to Crowdin (push) Waiting to run
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 03:01:02 +02:00
Charles Bochet
0adaf8aa7b
docs: use twenty-sdk/define subpath in docs and website demo (#19908)
## Summary

Following the recent move of `defineXXX` exports (e.g.
`defineLogicFunction`, `defineObject`, `defineFrontComponent`, …) from
the `twenty-sdk` root entry to the `twenty-sdk/define` subpath, this PR
aligns the documentation and the marketing site so users see the correct
import paths.

- `packages/twenty-docs/developers/extend/apps/building.mdx`: every code
snippet now imports `defineXXX` and related types/enums (`FieldType`,
`RelationType`, `OnDeleteAction`,
`STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`, `PermissionFlag`, `ViewKey`,
`NavigationMenuItemType`, `PageLayoutTabLayoutMode`,
`getPublicAssetUrl`, `DatabaseEventPayload`, `RoutePayload`,
`InstallPayload`, …) from `twenty-sdk/define`. Mixed imports were split
so that hooks and host-API helpers (`useRecordId`, `useUserId`,
`useFrontComponentId`, `enqueueSnackbar`, `closeSidePanel`, `pageType`,
`numberOfSelectedRecords`, `objectPermissions`, `everyEquals`,
`isDefined`) come from `twenty-sdk/front-component`.
-
`packages/twenty-website-new/.../DraggableTerminal/TerminalEditor/editorData.ts`:
the 29 demo source strings shown in the homepage's draggable terminal
now import from `twenty-sdk/define`.

Example apps under `packages/twenty-apps/{examples,internal,fixtures}`
were already using the right subpaths, so no code changes were needed
there.

Translations under `packages/twenty-docs/l/` are intentionally left
untouched — they will be refreshed via Crowdin from the English source.

## Test plan

- [ ] Skim the rendered `building.mdx` on Mintlify preview to confirm
code snippets look right.
- [ ] Visual check on the website's draggable terminal demo.


Made with [Cursor](https://cursor.com)
2026-04-21 02:08:02 +02:00
Charles Bochet
41ee6eac7a
chore(server): bump current version to 2.0.0 and add 2.1.0 as next (#19907)
## Summary

We are releasing Twenty v2.0. This PR sets up the
upgrade-version-command machinery for the new release line:

- Move `1.23.0` into `TWENTY_PREVIOUS_VERSIONS` (it just shipped)
- Set `TWENTY_CURRENT_VERSION` to `2.0.0` (no specific upgrade commands
— this is just the major version cut)
- Set `TWENTY_NEXT_VERSIONS` to `['2.1.0']` so future PRs that
previously would have targeted `1.24.0` now target `2.1.0`
- Add empty `V2_0_UpgradeVersionCommandModule` and
`V2_1_UpgradeVersionCommandModule` and wire them into
`WorkspaceCommandProviderModule`
- Refresh the `InstanceCommandGenerationService` snapshots to reflect
the new current version (`2.0.0` / `2-0-` slug)

The `2-0/` directory is intentionally empty — there are no specific
upgrade commands for the v2.0 cut. New upgrade commands authored after
this merges should land in `2-1/` (or be generated against `--version
2.1.0`).

## Test plan

- [x] `npx jest` on the impacted upgrade test files
(`upgrade-sequence-reader`, `upgrade-command-registry`,
`instance-command-generation`) passes (41 tests, 8 snapshots)
- [x] `prettier --check` and `oxlint` clean on touched files
- [ ] Manual: open `nx run twenty-server:command -- upgrade --dry-run`
against a local stack with workspaces still on `1.23.0` and confirm the
sequence is computed without errors

Made with [Cursor](https://cursor.com)
2026-04-21 02:05:27 +02:00
Charles Bochet
b4f996e0c4
Release v1.23.0 for twenty-sdk, twenty-client-sdk, and create-twenty-app (#19906)
## Summary
- Bump `twenty-sdk` from `1.23.0-canary.9` to `1.23.0`
- Bump `twenty-client-sdk` from `1.23.0-canary.9` to `1.23.0`
- Bump `create-twenty-app` from `1.23.0-canary.9` to `1.23.0`

Made with [Cursor](https://cursor.com)
2026-04-21 01:34:10 +02:00
Weiko
5c58254eb4
Fix activity relation picker (#19898)
## Context
ActivityTargetsInlineCell passed editModeContent via
RecordInlineCellContext, but that context key was no longer read.
Fix aligns the code with the rest of the codebase.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-21 01:30:23 +02:00
Charles Bochet
192a842f57
fix(website-new): pre-resolve wyw-in-js babel presets to absolute paths (#19905)
## Problem

Building `twenty-website-new` in any environment that does **not** also
include `twenty-website` (e.g. the Docker image used by the deployment
workflow) fails with:

```
Error: Turbopack build failed with 99 errors:
Error evaluating Node.js code
Error: Cannot find module 'next/babel'
Require stack:
- /app/node_modules/@babel/core/lib/config/files/plugins.js
- ...
- /app/node_modules/babel-merge/src/index.js
- /app/packages/twenty-website-new/node_modules/@wyw-in-js/transform/lib/plugins/babel-transform.js
- /app/packages/twenty-website-new/node_modules/next-with-linaria/lib/loaders/turbopack-transform-loader.js
```

## Root cause

`packages/twenty-website-new/wyw-in-js.config.cjs` references presets by
bare name:

```js
presets: ['next/babel', '@wyw-in-js'],
```

These options flow through
[`babel-merge`](https://github.com/cellog/babel-merge/blob/master/src/index.js#L11),
which calls `@babel/core`'s `resolvePreset(name)` **without** a
`dirname` argument. With no `dirname`, `@babel/core` falls back to
`require.resolve(id)` from its own file location — so resolution starts
at `node_modules/@babel/core/...` and only walks parent `node_modules`
directories from there, never down into individual workspace packages.

In a normal local install both presets happen to be hoisted to the
workspace root (because `twenty-website` pins `next@^14` and wins the
hoist), so resolution succeeds by accident. In the single-workspace
Docker build only `twenty-website-new` is present, so `next` (16.1.7)
and `@wyw-in-js/babel-preset` are nested in
`packages/twenty-website-new/node_modules` and Babel cannot reach them —
hence the failure.

## Fix

Pre-resolve both presets with `require.resolve(...)` in the wyw-in-js
config so Babel receives absolute paths and resolution becomes
independent of hoisting layout.

## Verification

- `yarn nx build twenty-website-new` — passes locally with the full
workspace
- Reproduced the original failure with a simulated single-workspace
install (only `twenty-website-new` and `twenty-oxlint-rules` present),
confirmed it fails on `main` and passes with this patch
- This unblocks the `twenty-infra` `Deploy Website New` workflow
([related infra PR](https://github.com/twentyhq/twenty-infra/pull/586))


Made with [Cursor](https://cursor.com)
2026-04-21 01:03:26 +02:00
Charles Bochet
1b469168c8
chore(workflow): temporarily lift credit-cap gate on workflow steps (#19904)
## Summary

- Removes the per-step `canBillMeteredProduct(WORKFLOW_NODE_EXECUTION)`
gate in `WorkflowExecutorWorkspaceService.executeStep` so workflows keep
running when a workspace reaches `hasReachedCurrentPeriodCap`.
Previously every step failed with
`BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE` (\"No remaining credits to
execute workflow…\").
- Drops the now-unused `BillingService` injection, related imports, and
the helper `canBillWorkflowNodeExecution`. Updates the spec to drop the
corresponding billing-validation case and mock.
- Leaves the constant file and `BillingService` itself in place, plus a
TODO at the previous gate site, so the behavior can be re-enabled with a
small, reviewable revert.

## Notes

- Usage events are still emitted (`USAGE_RECORDED` /
`UsageResourceType.WORKFLOW`), and `EnforceUsageCapJob` keeps computing
the cap and flipping `hasReachedCurrentPeriodCap` — only the executor
stops consulting that flag.
- The runner-level `canFeatureBeUsed` check in
`WorkflowRunnerWorkspaceService.run` was already log-only (subscription
presence, not credits), so no change there.
- AI chat (`agent-chat.resolver.ts`) keeps its own
`BILLING_CREDITS_EXHAUSTED` gate; this PR does not touch it.

## Test plan

- [x] `npx jest workflow-executor.workspace-service.spec.ts` (17/17
pass)
- [ ] Manual: with billing enabled and the metered subscription item
flagged `hasReachedCurrentPeriodCap = true`, trigger a workflow run and
verify steps execute end-to-end instead of failing with the billing
error.

Made with [Cursor](https://cursor.com)
2026-04-21 01:00:28 +02:00
github-actions[bot]
57de05ea74
i18n - translations (#19903)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 00:50:40 +02:00
Charles Bochet
a174aff5c8
fix(infra): copy nx.json and tsconfig.base.json into website-new image (#19902)
## Summary

Fix the website-new Docker build which currently fails with:

\`\`\`
NX   \"production\" is an invalid fileset.
All filesets have to start with either {workspaceRoot} or {projectRoot}.
\`\`\`

\`packages/twenty-website-new/project.json\` declares \`\"inputs\":
[\"production\", \"^production\"]\` — a named input defined in the root
\`nx.json\`. Without copying \`nx.json\` into the image, nx can't
resolve it and the build fails.

Mirrors what the main twenty Dockerfile already does (line 9 of
\`packages/twenty-docker/twenty/Dockerfile\` copies both
\`tsconfig.base.json\` and \`nx.json\`).

## Test plan

- [ ] Re-run twenty-infra's \`Deploy Website New\` workflow (dev) —
build step should now pass

Made with [Cursor](https://cursor.com)
2026-04-21 00:45:17 +02:00
Charles Bochet
96fc98e710
Fix Apps UI: replace 'Managed' label with actual app name and unify app icons (#19897)
## Summary

- The Data Model table was labeling core Twenty objects (e.g. Person,
Company) as **Managed** even though they are part of the standard
application. This PR teaches the frontend to resolve an `applicationId`
back to its real application name (`Standard`, `Custom`, or any
installed app), and removes the misleading **Managed** label entirely.
- Introduces a single, consistent way to render an "app badge" across
the settings UI:
- new `Avatar` variant `type="app"` (rounded 4px corners + 1px
deterministic border derived from `placeholderColorSeed`)
- new `AppChip` component (icon + name) backed by a new
`useApplicationChipData` hook
- new `useApplicationsByIdMap` hook + `CurrentApplicationContext` so the
chip can render **This app** when shown inside the matching app's detail
page
- Reuses these primitives on:
- the application detail page header (`SettingsApplicationDetailTitle`)
  - the Installed / My apps tables (`SettingsApplicationTableRow`)
  - the NPM packages list (`SettingsApplicationsDeveloperTab`)
- Backend: exposes a minimal `installedApplications { id name
universalIdentifier }` field on `Workspace` (resolved from the workspace
cache, soft-deleted entries filtered out) so the frontend can resolve
`applicationId` -> name without N+1 fetches.
- Cleanup: deletes `getItemTagInfo` and inlines its tiny
responsibilities into the components that need them, matching the
`RecordChip` pattern.
2026-04-21 00:44:14 +02:00
Charles Bochet
9a963ddeca
feat(infra): add Dockerfile for twenty-website-new (#19901)
## Summary

Adds the Docker build for the new marketing website at
`packages/twenty-website-new`, mirroring the existing
`packages/twenty-docker/twenty-website/Dockerfile`.

Differences from the existing `twenty-website` Dockerfile:

- Uses `nx build twenty-website-new` / `nx start twenty-website-new`
- Drops the `KEYSTATIC_*` build-time fake env (the new website doesn't
use Keystatic)
- Doesn't copy `twenty-ui` source (the new website has no workspace
dependency on it)

The image will be built by the new `deploy-website-new.yaml` workflow in
[`twentyhq/twenty-infra`](https://github.com/twentyhq/twenty-infra) and
pushed to ECR repos `dev-website-new` / `staging-website-new`.

Companion PRs:
- twentyhq/twenty-infra: Helm chart + ArgoCD app + deploy workflow
- twentyhq/twenty-infra-releases: bootstrap tags.yaml

## Test plan

- [ ] Local build: \`docker build -f
packages/twenty-docker/twenty-website-new/Dockerfile .\`
- [ ] First run of \`Deploy Website New\` workflow on dev succeeds
(build + push to ECR)
- [ ] ArgoCD \`website-new\` application becomes Healthy on dev
- [ ] https://website-new.twenty-main.com serves the new website

Made with [Cursor](https://cursor.com)
2026-04-21 00:27:11 +02:00
Félix Malfait
13afef5d1d
fix(server): scope loadingMessage wrap/strip to AI-chat callers (#19896)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
## Summary

MCP tool execution crashed with \`Cannot destructure property
'loadingMessage' of 'parameters' as it is undefined\` whenever
\`execute_tool\` was called without an inner \`arguments\` field. Root
cause: \`loadingMessage\` is an AI-chat UX affordance (lets the LLM
narrate progress so the chat UI can show "Sending email…") but it was
being wrapped into **every** tool schema — including those advertised to
external MCP clients — and \`dispatch\` unconditionally stripped it,
crashing on \`undefined\` args.

The fix scopes the wrap/strip pair to AI-chat callers only:

- Pair wrap and strip inside \`hydrateToolSet\` (they belong together).
- New \`includeLoadingMessage\` option on \`hydrateToolSet\` /
\`getToolsByName\` / \`getToolsByCategories\` (default \`true\` so
AI-chat behavior is unchanged).
- MCP opts out → external clients see clean inputSchemas without a
required \`loadingMessage\` field.
- \`dispatch\` no longer strips; args default to \`{}\` defensively.
- \`execute_tool\` defaults \`arguments\` to \`{}\` at the LLM boundary.

## Test plan

- [x] \`npx nx typecheck twenty-server\` passes
- [x] \`npx oxlint\` clean on changed files
- [x] \`npx jest mcp-protocol mcp-tool-executor\` — 23/23 tests pass
- [ ] Manually: call \`execute_tool\` via MCP with and without inner
\`arguments\` — verify no crash, endpoints execute
- [ ] Manually: inspect MCP \`tools/list\` response — verify
\`search_help_center\` schema no longer contains \`loadingMessage\`
- [ ] Regression: AI chat still streams loading messages as the LLM
calls tools

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:43:16 +02:00
Abdullah.
83bc6d1a1b
[Website] Self-host billing migration and some responsiveness fixes. (#19894)
Closes the following issues.

https://github.com/twentyhq/core-team-issues/issues/2371
https://github.com/twentyhq/core-team-issues/issues/2379
https://github.com/twentyhq/core-team-issues/issues/2383
2026-04-20 21:23:54 +02:00
github-actions[bot]
755f1c92d1
i18n - translations (#19893)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 19:45:24 +02:00
Weiko
6d3ff4c9ce
Translate standard page layouts (#19890)
## Context
Standard page layout tabs, page layout widgets, and view field group
titles were hardcoded English in the backend. This PR brings them under
the same translation pipeline as views.

Notes: Once a standard widget/tab/section title is overriden, the
backend returns its value without translation
2026-04-20 17:29:33 +00:00
Charles Bochet
9a95cd02ed
Fix applications query cartesian product causing read timeouts (#19892)
## Summary

Same fix pattern as #19511 (`rolesPermissions` cartesian product).

The `Settings > Applications` page was hitting query read timeouts in
production. The offending SQL came from
`ApplicationService.findManyApplications` / `findOneApplication`, which
loaded **5 `OneToMany` children** in a single query via TypeORM
`relations`:

```
logicFunctions × agents × frontComponents × objects × applicationVariables
```

Postgres returns the Cartesian product of all five — e.g. 20 logic
functions × 5 agents × 30 front components × 100 objects × 10 variables
= **3M rows for ~165 distinct records**, which trivially exceeds the
read timeout.

## Changes

- **`findManyApplications`** — dropped all `OneToMany` relations. The
frontend `FIND_MANY_APPLICATIONS` query only selects scalar fields and
the `applicationRegistration` ManyToOne, so joining the children was
pure waste at the list level.
- **`findOneApplication`** — kept the cheap `ManyToOne` / `OneToOne`
joins (`packageJsonFile`, `yarnLockFile`, `applicationRegistration`) on
the main query and fetched the 5 `OneToMany` children in parallel via
`Promise.all`, reattaching them on the entity. Same shape as
`WorkspaceRolesPermissionsCacheService.computeForCache` after #19511.
- **`application.module.ts`** — registered the 5 child entity
repositories via `TypeOrmModule.forFeature`.

The other internal caller (`front-component.service.ts →
findOneApplicationOrThrow`) only reads
`application.universalIdentifier`, so the extra parallel single-key
lookups remain far cheaper than the previous 8-way join with row
explosion.
2026-04-20 17:04:10 +00:00
Abdullah.
27e1caf9cc
Cleanup files that were committed with website PR, but should not be there. (#19891)
Some files went through with the last PR from Thomas I merged - some
screenshots at root, others inside output folder. This PR removes them.
2026-04-20 17:46:10 +02:00
Etienne
b140f70260
Website - Plan pricing update (#19887) 2026-04-20 14:58:00 +00:00
Charles Bochet
13c4a71594
fix(ui): make CardPicker hover cover the whole card and align content left (#19884)
## Summary

Two small visual issues with the shared `CardPicker` (used in the
Enterprise plan modal and the onboarding plan picker):

- Labels like \`Monthly\` / \`Yearly\` were center-aligned inside their
cards while the subtitle (\`\$25 / seat / month\`) stayed left-aligned,
because the underlying \`<button>\` element's default \`text-align:
center\` was leaking into the children.
- The hover background was painted on the same element that owned the
inner padding, so the hover surface didn't visually feel like the whole
card.

This PR:
- Moves the content padding into a new \`StyledCardInner\` so the outer
\`<button>\` is just the card chrome (border + radius + background +
hover).
- Adds \`text-align: left\` so titles align with their subtitles.
- Hoists \`cursor: pointer\` out of \`:hover\` (it should be on by
default for the card).

Affects:
- \`EnterprisePlanModal\` (Settings → Enterprise)
- \`ChooseYourPlanContent\` (onboarding trial picker)
2026-04-20 17:00:12 +02:00
github-actions[bot]
4ecde0e161
i18n - translations (#19888)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 16:45:06 +02:00
Etienne
117909e10a
Billing - Adapt to new unit (#19886) 2026-04-20 14:28:07 +00:00
Félix Malfait
9f5688ab13
Redesign why-twenty page with three-section narrative (#19882)
## Summary
- Restructures the why-twenty page into a clearer three-act story (the
shift / what this means / the opportunity), with new copy across hero
subtitle, all editorials, marquee, quote and signoff.
- Adds visual rhythm via left/right section anchoring (sections 1 and 3
left-aligned, section 2 right-aligned) and per-section `GuideCrosshair`
markers at the top edge of each editorial.
- Adds a CTA `Signoff` section ("Get started") at the end of the page.
- Bumps the 3D quotation marks (`Quotes` illustration) so the Quote can
serve as a visual section break.

## Changes
- **Editorial section**
([Editorial.Heading](packages/twenty-website-new/src/sections/Editorial/components/Heading/Heading.tsx),
[Editorial.Body](packages/twenty-website-new/src/sections/Editorial/components/Body/Body.tsx),
[Editorial.Root](packages/twenty-website-new/src/sections/Editorial/components/Root/Root.tsx)):
  - Default heading size `xl` → `lg`
- New `two-column-left` and `two-column-right` body layouts via
`data-align` on `TwoColumnGrid`
- New optional `crosshair` prop on `Editorial.Root` that anchors a
`GuideCrosshair` to the section
- **Why-twenty constants** — fresh copy in `hero.ts`, `editorial-one`,
`editorial-three`, `editorial-four`, `marquee.ts`, `quote.ts`,
`signoff.ts`
- **Page layout**
([why-twenty/page.tsx](packages/twenty-website-new/src/app/why-twenty/page.tsx)):
  - Section 1 → left content + crosshair on right
  - Section 2 → right content + crosshair on left
  - Section 3 → left content + crosshair on right
  - Adds `Signoff` block with `LinkButton` "Get started" CTA
- **Quote 3D illustration** — `previewDistance` 6 → 4 and bigger
`StyledVisualMount` (added a one-line `oxlint-disable` for the
pre-existing `@ts-nocheck` that the diff surfaced)
- **Signoff** — keeps the GuideCrosshair behavior limited to Partners
(per-page config map remains in place)

Editorial is only consumed on the why-twenty page so the heading/body
changes don't affect any other page.

## Test plan
- [ ] Visit `/why-twenty` on desktop — verify the three editorials read
as left/right/left with crosshairs at section top edges
- [ ] Verify the Signoff CTA renders white "Get started" pill button on
dark background and links to `app.twenty.com/welcome`
- [ ] Verify mobile layout — crosshairs hidden, content left-aligned,
body stacks single column
- [ ] Lighthouse / no console errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-20 15:32:02 +02:00
Charles Bochet
c959998111
Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 1.23.0-canary.9 (#19883)
## Summary
- Bumps `twenty-sdk`, `twenty-client-sdk`, and `create-twenty-app` from
`1.23.0-canary.2` to `1.23.0-canary.9`.

## Test plan
- [ ] Canary publish workflow succeeds for the three packages.

Made with [Cursor](https://cursor.com)
2026-04-20 13:21:00 +00:00
neo773
ade55e293f
fix 1.22 upgrade command add-workspace-id-to-indirect-entities (#19868)
/closes #19863
2026-04-20 13:19:10 +00:00
Charles Bochet
10c49a49c4
feat(sdk): support viewSorts in app manifests (#19881)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push docs to Crowdin / Push documentation to Crowdin (push) Waiting to run
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
## Summary

Today the SDK lets apps declare `filters` on a view but not `sorts`, so
any view installed via an app manifest can never have a default
ordering. This PR adds declarative view sorts end-to-end: SDK manifest
type, `defineView` validation, CLI scaffold, and the application
install/sync pipeline that converts the manifest into the universal flat
entity used by workspace migrations. The persistence layer
(`ViewSortEntity`, resolvers, action handlers, builders…) already
existed server-side; the missing piece was the manifest → universal-flat
converter and the relation wiring on `view`.

## Changes

**`twenty-shared`**
- Add `ViewSortDirection` enum (`ASC` | `DESC`) and re-export it from
`twenty-shared/types`.
- Add `ViewSortManifest` type and an optional `sorts?:
ViewSortManifest[]` on `ViewManifest`, exported from
`twenty-shared/application`.

**`twenty-sdk`**
- Validate `sorts` entries in `defineView` (`universalIdentifier`,
`fieldMetadataUniversalIdentifier`, `direction` ∈ `ASC`/`DESC`).
- Add a commented `// sorts: [ ... ]` example to the CLI view scaffold
template + matching snapshot assertion.

**`twenty-server`**
- Re-export `ViewSortDirection` from `twenty-shared/types` in
`view-sort/enums/view-sort-direction.ts` (single source of truth,
backward compatible for existing imports).
- New converter `fromViewSortManifestToUniversalFlatViewSort` (+ unit
tests for `ASC` and `DESC`).
- Wire the converter into
`computeApplicationManifestAllUniversalFlatEntityMaps` so
`viewManifest.sorts` are added to `flatViewSortMaps`, mirroring how
filters are processed.
- Replace the `// @ts-expect-error TODO migrate viewSort to v2 /
viewSorts: null` placeholder in `ALL_ONE_TO_MANY_METADATA_RELATIONS`
with the proper relation (`viewSortIds` /
`viewSortUniversalIdentifiers`).
- Update affected snapshots (`get-metadata-related-metadata-names`,
`all-universal-flat-entity-foreign-key-aggregator-properties`).

## Example usage

\`\`\`ts
defineView({
  name: 'All issues',
  objectUniversalIdentifier: 'issue',
  sorts: [
    {
      universalIdentifier: 'all-issues__sort-created-at',
      fieldMetadataUniversalIdentifier: 'createdAt',
      direction: 'DESC',
    },
  ],
});
\`\`\`
2026-04-20 14:31:06 +02:00
Charles Bochet
5c2a0cf115
fix(front): gate renewToken Apollo logger on IS_DEBUG_MODE (#19878)
## Summary

The standalone Apollo client used by `AuthService.renewToken` attached
`loggerLink` unconditionally, while the main `apollo.factory` client
correctly gates it on `isDebugMode`. As a result, **every token refresh
in production printed the `renewToken` response — including the new
access and refresh JWTs — to the browser console** via the `loggerLink`
`RESULT` group.

Reproduced in production: opening devtools shows a
`Twenty-Refresh::Generic` collapsed group on every token renewal,
containing `HEADERS`, `VARIABLES`, `QUERY` and a `RESULT` payload with
the full token strings.

The fix mirrors the gating already used in `apollo.factory.ts`
(`...(isDebugMode ? [logger] : [])`), so the logger is only attached
when `IS_DEBUG_MODE=true`. Local debug behavior is unchanged.
2026-04-20 11:49:28 +00:00
github-actions[bot]
f9768d057e
i18n - docs translations (#19880)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 12:51:54 +02:00
Abdullah.
fd2288bfff
fix: prototype pollution via parse in nodejs flatted (#19870)
Resolves [Dependabot Alert
686](https://github.com/twentyhq/twenty/security/dependabot/686).
2026-04-20 10:18:51 +00:00
Charles Bochet
de1e592cd3
fix(front): suppress full-page skeleton inside auth modal (#19875)
## Summary

- Lazy auth-flow routes (`SignInUp`, `Invite`, `ResetPassword`,
`CreateWorkspace`, `CreateProfile`, `SyncEmails`, `InviteTeam`,
`PlanRequired`, `PlanRequiredSuccess`, `BookCallDecision`, `BookCall`)
render through `<Outlet/>` inside `<AuthModal>`. Their `LazyRoute`
`<Suspense>` fallback was the page-level `PageContentSkeletonLoader`, so
the two grey shimmer bars painted **inside the modal box** for a few
hundred ms while each chunk downloaded.
- `LazyRoute` now accepts an optional `fallback` prop (default
unchanged: the existing page skeleton). Every auth-modal route passes
`fallback={null}` so the modal stays empty until the lazy chunk resolves
instead of flashing the shimmer.
- `AuthModal`'s inner `StyledContent` gets a `min-height: 320px` so the
framer-motion `layout` animation doesn't rapidly resize the modal as
inner steps (loader → form → password → 2FA / workspace selection) swap.
The modal can still grow for taller steps; only the rapid jump is
removed.

## Why default-parameter syntax for `fallback`

`fallback ?? <LazyRouteFallback/>` would treat an explicit `null` as "no
value" and still render the default skeleton. Using a default parameter
(`fallback = <LazyRouteFallback/>`) preserves an explicit `null` because
defaults only kick in for `undefined`.
2026-04-20 09:39:09 +00:00
github-actions[bot]
01928fc786
i18n - docs translations (#19874)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 11:06:06 +02:00
github-actions[bot]
e4d8cbdb39
i18n - translations (#19873)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 10:57:21 +02:00
Etienne
e68842c268
Billing - fixes (#19867)
- Uniformize credit formating : In UI, 1$=1credit. In BE 1 UI credit =
1_000_000 BE "crédits"
- Add crédit rollover information + Link to documentation +
Documentation update
<img width="291" height="317" alt="Screenshot 2026-04-17 at 18 22 59"
src="https://github.com/user-attachments/assets/2519fb9f-159d-4c85-95f4-a6e005a8a1a3"
/>
<img width="848" height="763" alt="Screenshot 2026-04-17 at 14 12 20"
src="https://github.com/user-attachments/assets/a3cc0874-f275-49ea-819f-305ec314bdfe"
/>
<img width="797" height="757" alt="Screenshot 2026-04-17 at 14 12 13"
src="https://github.com/user-attachments/assets/9048409b-d5a2-435a-b735-70370705e668"
/>

- Enable direct top-up (or subscription if in trial) from AI chat
<img width="333" height="215" alt="Screenshot 2026-04-17 at 22 52 00"
src="https://github.com/user-attachments/assets/7a20c627-2806-4bcf-a037-b45752232be9"
/>
<img width="457" height="769" alt="Screenshot 2026-04-17 at 22 51 41"
src="https://github.com/user-attachments/assets/d2a90c1b-271f-4fe9-8891-baeb2fabb86d"
/>

- Inform users if credit limit is reached - Banner
<img width="1130" height="127" alt="Screenshot 2026-04-17 at 19 15 11"
src="https://github.com/user-attachments/assets/30723e5e-c07e-462f-8eb8-e08f52bbab1c"
/>
2026-04-20 08:43:02 +00:00
github-actions[bot]
903ae5cb09
i18n - translations (#19869)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 09:35:54 +02:00
martmull
5dd7eba911
Fix app design 6 (#19827)
Unify application display page and isntalled page

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-20 09:29:25 +02:00
Félix Malfait
42f57db005
fix(server): add registration_client_uri to DCR response for Claude.ai connector (#19858)
## Summary

Claude.ai's custom remote MCP connector fails with "Couldn't reach the
MCP server" after successfully completing OAuth dynamic client
registration. Driving the flow through Chrome DevTools showed Claude's
backend creates our DCR client (many hundreds of orphan rows visible in
the admin panel), then never returns the user to `/authorize` — it gives
up silently.

**Empirical comparison against known-working MCP servers Claude.ai
connects to identified one concrete difference**: every server that
works returns `registration_client_uri` in the DCR response. We didn't.

| Server | DCR `registration_client_uri` | Claude.ai web connector |
|---|---|---|
| Linear (`mcp.linear.app`) | `/register/<client_id>` |  works |
| Sentry (`mcp.sentry.dev`) | `/oauth/register/<client_id>` |  works |
| Atlassian (`mcp.atlassian.com`) | yes |  works |
| **Twenty** (before this PR) | **missing** |  "Couldn't reach" |

## What this PR changes

### 1. Add `registration_client_uri` to the DCR response

```
{
  "client_id": "…",
  …existing fields…,
+ "registration_client_uri": "<issuer>/oauth/register/<client_id>"
}
```

Pointer at the registration's management endpoint per RFC 7591 §3.2.1.
Marked OPTIONAL in the spec but empirically required by Claude.ai.

### 2. New `GET /oauth/register/:clientId` endpoint (RFC 7592 read-back)

Returns public registration metadata (`client_name`, `redirect_uris`,
`grant_types`, `scope`, etc.). 404 for unknown clients.

No `registration_access_token` is issued (and none required to hit this
endpoint): the `client_id` is an unguessable UUID and the fields
returned are already public-readable via
`findApplicationRegistrationByClientId` GraphQL. This matches Linear's
behaviour — they return a `registration_client_uri` but issue no access
token.

### 3. Advertise `response_modes_supported: ["query"]` in AS metadata

RFC 8414 default, but explicitly listed by Linear / Sentry / Atlassian
and absent from ours. Some clients treat its absence as a capability
gap.

## Why I'm confident this is the root cause

- The failure mode exactly matches an orphaned-DCR retry loop (hundreds
of registrations, none `installed` on a workspace).
- #19847 reporter confirmed Claude Desktop + VS Code work — those
clients use the MCP Python SDK which doesn't require
`registration_client_uri`. **Claude.ai web** uses Anthropic's
proprietary backend client (`User-Agent: Claude-User`), which
empirically does.
- All 3 working reference servers return the field; we were the odd one
out.

## Test plan

- [x] `tsc --noEmit` clean on touched files
- [x] `yarn jest
--testPathPatterns="oauth-discovery.controller|mcp-auth.guard"` → 4/4
pass
- [ ] After deploy:
  ```bash
curl -s -X POST https://<host>/oauth/register -H 'Content-Type:
application/json' \
-d
'{"client_name":"probe","redirect_uris":["https://claude.ai/api/mcp/auth_callback"],"token_endpoint_auth_method":"none"}'
\
    | jq .registration_client_uri
  # expect: "https://<host>/oauth/register/<uuid>"
  ```
- [ ] After deploy: add the MCP connector in Claude.ai — user should now
reach the Twenty `/authorize` page

## Honesty

This is the nth fix in a long debugging chain. Unlike the earlier round
of fixes (which were real spec-compliance bugs but not Claude's
blocker), this one is backed by empirical evidence across 3
known-working implementations. If Claude.ai still fails after this
deploys, the remaining delta is `cli_client_id` in AS metadata
(non-standard field, could confuse strict parsers) or a field we
advertise that others don't (e.g. `client_credentials` grant) — both
small, removable, not disruptive.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 09:28:36 +02:00
Thomas des Francs
0729ad27b7
Partners, customers and more (#19862)
## Summary
- Refresh the Twenty website with updated homepage, product, pricing,
partner, customer, case study, and release content
- Add and replace supporting imagery, illustrations, and Lottie assets
used across the site
- Adjust layout constants, navigation/footer content, and page-level
copy for the updated marketing experience
- Update Next.js config and ignore rules to support the new assets and
build output

## Testing
- Not run (not requested)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-20 07:13:56 +00:00
github-actions[bot]
46aedcf133
chore: sync AI model catalog from models.dev (#19866)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-20 08:38:46 +02:00
Félix Malfait
75848ff8ea
feat: move admin panel to dedicated /admin-panel GraphQL endpoint (#19852)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Emails / emails-test (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
## Summary

Splits admin-panel resolvers off the shared `/metadata` GraphQL endpoint
onto a dedicated `/admin-panel` endpoint. The backend plumbing mirrors
the existing `metadata` / `core` pattern (new scope, decorator, module,
factory), and admin types now live in their own
`generated-admin/graphql.ts` on the frontend — dropping 877 lines of
admin noise from `generated-metadata`.

## Why

- **Smaller attack surface on `/metadata`** — every authenticated user
hits that endpoint; admin ops don't belong there.
- **Independent complexity limits and monitoring** per endpoint.
- **Cleaner module boundaries** — admin is a cross-cutting concern that
doesn't match the "shared-schema configuration" meaning of `/metadata`.
- **Deploy / blast-radius isolation** — a broken admin query can't
affect `/metadata`.

Runtime behavior, auth, and authorization are unchanged — this is a
relocation, not a re-permissioning. All existing guards
(`WorkspaceAuthGuard`, `UserAuthGuard`,
`SettingsPermissionGuard(SECURITY)` at class level; `AdminPanelGuard` /
`ServerLevelImpersonateGuard` at method level) remain on
`AdminPanelResolver`.

## What changed

### Backend
- `@AdminResolver()` decorator with scope `'admin'`, naming parallels
`CoreResolver` / `MetadataResolver`.
- `AdminPanelGraphQLApiModule` + `adminPanelModuleFactory` registered at
`/admin-panel`, same Yoga hook set as the metadata factory (Sentry
tracing, error handler, introspection-disabling in prod, complexity
validation).
- Middleware chain on `/admin-panel` is identical to `/metadata`.
- `@nestjs/graphql` patch extended: `resolverSchemaScope?: 'core' |
'metadata' | 'admin'`.
- `AdminPanelResolver` class decorator swapped from
`@MetadataResolver()` to `@AdminResolver()` — no other changes.

### Frontend
- `codegen-admin.cjs` → `src/generated-admin/graphql.ts` (982 lines).
- `codegen-metadata.cjs` excludes admin paths; metadata file shrinks by
877 lines.
- `ApolloAdminProvider` / `useApolloAdminClient` follow the existing
`ApolloCoreProvider` / `useApolloCoreClient` pattern, wired inside
`AppRouterProviders` alongside the core provider.
- 37 admin consumer files migrated: imports switched to
`~/generated-admin/graphql` and `client: useApolloAdminClient()` is
passed to `useQuery` / `useMutation`.
- Three files intentionally kept on `generated-metadata` because they
consume non-admin Documents: `useHandleImpersonate.ts`,
`SettingsAdminApplicationRegistrationDangerZone.tsx`,
`SettingsAdminApplicationRegistrationGeneralToggles.tsx`.

### CI
- `ci-server.yaml` runs all three `graphql:generate` configurations and
diff-checks all three generated dirs.

## Authorization (unchanged, but audited while reviewing)

Every one of the 38 methods on `AdminPanelResolver` has a method-level
guard:
- `AdminPanelGuard` (32 methods) — requires `canAccessFullAdminPanel ===
true`
- `ServerLevelImpersonateGuard` (6 methods: user/workspace lookup + chat
thread views) — requires `canImpersonate === true`

On top of the class-level guards above. No resolver method is accessible
without these flags + `SECURITY` permission in the workspace.

## Test plan

- [ ] Dev server boots; `/graphql`, `/metadata`, `/admin-panel` all
mapped as separate GraphQL routes (confirmed locally during
development).
- [ ] `nx typecheck twenty-server` passes.
- [ ] `nx typecheck twenty-front` passes.
- [ ] `nx lint:diff-with-main twenty-server` and `twenty-front` both
clean.
- [ ] Manual smoke test: log in with a user who has
`canAccessFullAdminPanel=true`, open the admin panel at
`/settings/admin-panel`, verify each tab loads (General, Health, Config
variables, AI, Apps, Workspace details, User details, chat threads).
- [ ] Manual smoke test: log in with a user who has
`canImpersonate=false` and `canAccessFullAdminPanel=false`, hit
`/admin-panel` directly with a raw GraphQL request, confirm permission
error on every operation.
- [ ] Production deploy note: reverse proxy / ingress must route the new
`/admin-panel` path to the Nest server. If the proxy has an explicit
allowlist, infra change required before cutover.

## Follow-ups (out of scope here)

- Consider cutting over the three
`SettingsAdminApplicationRegistration*` components to admin-scope
versions of the app-registration operations so the admin page is fully
on the admin endpoint.
- The `renderGraphiQL` double-assignment in
`admin-panel.module-factory.ts` is copied from
`metadata.module-factory.ts` — worth cleaning up in both.
2026-04-19 20:55:10 +02:00
github-actions[bot]
90661cc821
i18n - translations (#19851)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-19 13:35:47 +02:00
Félix Malfait
6117a1d6c0
refactor: standardize AI acronym to Ai (PascalCase) across internal identifiers (#19837)
## Summary

The "AI" acronym was rendered inconsistently across the codebase. The
backend AI module had settled on PascalCase `Ai` (`AiAgentModule`,
`AiBillingService`, `AiChatModule`, `AiModelRegistryService`, etc.),
while frontend components, several DTOs, a few types, and shared
identifiers still used all-caps `AI` (`AIChatTab`,
`AISystemPromptPreviewDTO`, `SettingsPath.AIPrompts`, ...). CLAUDE.md
specifies PascalCase for classes; this PR normalizes everything internal
to `Ai`.

**This is a pure internal rename.** The GraphQL schema is untouched —
`@ObjectType` decorator string arguments, resolver method names (which
become Query/Mutation field names), gql template contents, and the
`generated-metadata/graphql.ts` file are preserved verbatim. The only
visible change is TypeScript identifiers and file names.

## Also folded in (adjacent cleanups)

- **`AgentModelConfigService` → `AiModelConfigService`**. Lives in
`ai-models/` and is used by multiple AI code paths, not just the Agent
entity. The "Agent" prefix was misleading.
- **`generate-text-input.dto.ts` → `generate-text.input.ts`**. The
`ai-agent/dtos/` folder already uses `<entity>.input.ts` convention for
Input classes (`create-agent.input.ts` etc.); the old path mixed
`.dto.ts` file extension with a class that has no DTO suffix. File
rename only; class stays `GenerateTextInput`.
- **Removed stale TODO** in `ai-model-config.type.ts` that asked for the
`AiModelConfig` rename that this PR performs.

## Rename methodology

Bulk rename via perl with anchored regex
`(?<!['"])(?<![A-Z.])AI([A-Z])(?=[a-z])/Ai$1/g`:

- **Lookbehind for non-uppercase** skips adjacent acronyms (`MOSAIC`,
`OIDCSSO`) and leaves `AIRBNB_ID` alone.
- **Lookbehind for non-quote** protects most string literals.
- **Lookahead for lowercase** restricts matches to PascalCase
identifiers (`AIChatTab`), leaving SCREAMING_SNAKE constants untouched.

Strict file-scope exclusions: `generated-metadata/**`, `generated/**`,
`locales/**`, `migrations/**`, `illustrations/**`, `halftone/**`, and
the two gql template files (`queries/getAISystemPromptPreview.ts`,
`mutations/uploadAIChatFile.ts`).

Post-rename reverts for identifiers where the regex was too eager:
- Backend resolver method names kept: `getAISystemPromptPreview`,
`uploadAIChatFile` (they are GraphQL field names).
- `@ObjectType('AdminAIModels')` / `('AISystemPromptPreview')` /
`('AISystemPromptSection')` kept as-is.
- Backend classes `ClientAIModelConfig` / `AdminAIModelConfig` kept
as-is (they use `@ObjectType()` with no argument, so the class name IS
the schema name).
- External-library symbols restored: `OpenAIProvider`,
`createOpenAICompatible`, `vercelAIIntegration`.

File renames use a two-step rename to work on macOS case-insensitive
filesystems: `git mv X.tsx X.tsx.tmp && git mv X.tsx.tmp renamed.tsx`.

## Diff audit

- 0 changes to migrations
- 0 changes to locale `.po` / `.ts` files
- 0 changes to `generated-metadata/graphql.ts`
- 0 changes to website illustration files (base64 blobs preserved)
- 0 renames inside user-facing translation strings (`t\`…\``,
`msg\`…\``, `<Trans>…</Trans>`)

## Test plan

- [x] `npx nx typecheck twenty-server` — PASS
- [x] `npx nx typecheck twenty-front` — PASS
- [x] `npx jest ai-model admin agent-role` — 79/79 PASS
- [x] `npx oxlint --type-aware` on 118 changed files — 0 errors
- [x] `npx prettier --check` on 118 changed files — clean
- [ ] CI
2026-04-19 13:29:35 +02:00
Avasis AI
1e27c3b621
fix: correct sSOService → ssoService camelCase typo (#19845)
## Summary

Fixes the pre-existing camelCase typo mentioned in #19839.

The injected `SSOService` property was named `sSOService` instead of the
correct camelCase `ssoService` across the auth module. This is a
straightforward mechanical rename of the property/variable name — no
logic changes.

> **Bonus: pre-existing typo to fix** — `private sSOService: SSOService`
— The variable name is a camelCase typo (`sSOService` instead of
`ssoService`). — #19839

## Changes

Renamed `sSOService` → `ssoService` in 7 files:
- `auth/guards/oidc-auth.guard.ts`
- `auth/guards/saml-auth.guard.ts`
- `auth/guards/oidc-auth.spec.ts`
- `auth/auth.resolver.ts`
- `auth/controllers/sso-auth.controller.ts`
- `auth/strategies/saml.auth.strategy.ts`
- `sso/sso.resolver.ts`

Note: The type `SSOService` (PascalCase class name) is intentionally
left unchanged — it will be addressed in the broader SSO acronym PR from
#19839.

## Test plan

- [ ] Verify `typecheck twenty-server` passes
- [ ] Verify existing auth/SSO tests pass

Co-authored-by: Abhay <abhayjnayakpro@gmail.com>
2026-04-19 13:29:06 +02:00
Abdullah.
dbf43d792c
[Website] Fix testimonials shape, diamond direction, and integrate partner application form. (#19835)
Closes the following issues.

https://github.com/twentyhq/core-team-issues/issues/2368
https://github.com/twentyhq/core-team-issues/issues/2369
https://github.com/twentyhq/core-team-issues/issues/2374
https://github.com/twentyhq/core-team-issues/issues/2375
2026-04-19 09:12:59 +00:00
Thomas des Francs
066003cb04
Hero 2.0 (#19846)
and a few fixes

---------

Co-authored-by: Abdullah. <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-19 09:01:30 +00:00
Félix Malfait
1f3defa7b3
fix(server): expose WWW-Authenticate header for browser-based MCP clients (#19836)
Some checks are pending
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
## The bug

Claude's MCP connector fails with \"Couldn't reach the MCP server\" on
every URL (\`api.twenty.com/mcp\`, \`app.twenty.com/mcp\`,
\`<workspace>.twenty.com/mcp\`, custom domains). The failure happens
**before** any OAuth flow starts — the client never even reaches the
consent screen.

## Root cause

\`POST /mcp\` unauthenticated returns:

\`\`\`
HTTP/2 401
access-control-allow-origin: *
www-authenticate: Bearer
resource_metadata=\"https://…/.well-known/oauth-protected-resource\"
content-type: application/json; charset=utf-8
(no access-control-expose-headers)
\`\`\`

The [Fetch/CORS
spec](https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name)
defines only six response headers as safelisted — \`Cache-Control\`,
\`Content-Language\`, \`Content-Type\`, \`Expires\`, \`Last-Modified\`,
\`Pragma\`. Every other header is withheld from cross-origin JS unless
the server opts it in via \`Access-Control-Expose-Headers\`.

Result: Claude's browser-side MCP client receives the 401 but
\`response.headers.get('WWW-Authenticate')\` returns \`null\`. No
\`resource_metadata\` URL, no discovery, no OAuth — the client gives up
with the generic \"can't reach server\" error.

The [MCP authorization
spec](https://modelcontextprotocol.io/specification/draft/basic/authorization)
explicitly requires this header to be exposed.

## Fix

One config change in \`main.ts\`:

\`\`\`ts
-    cors: true,
+ // Expose WWW-Authenticate so browser-based MCP clients can read the
+ // resource_metadata pointer on 401. Required by MCP authorization
spec.
+    cors: { exposedHeaders: ['WWW-Authenticate'] },
\`\`\`

NestJS's default \`cors: true\` uses the \`cors\` package defaults,
which don't set \`exposedHeaders\`. Moving to an explicit config keeps
all other defaults (origin \`*\`, standard methods) and adds the single
required expose.

## Why it's safe and generally beneficial

- \`Access-Control-Expose-Headers: WWW-Authenticate\` is sent on every
response but only has an effect when \`WWW-Authenticate\` is actually
present (i.e. 401s). It's an opt-in permission, not a header-setter.
- \`WWW-Authenticate\` itself is still only set by \`McpAuthGuard\` on
401 — this PR doesn't change where or when the header is emitted.
- Covers the entire app, not just \`/mcp\` — any future 401-returning
endpoint will behave correctly for browser clients automatically.
- No change to origin handling, methods, or credentials. All existing
API / GraphQL / REST traffic is unaffected.

## Verification

After deploy:
\`\`\`bash
curl -sI -X POST -H \"Origin: https://claude.ai\"
https://api.twenty.com/mcp \\
  | grep -iE 'access-control-expose|www-authenticate'
# Expect:
#   access-control-expose-headers: WWW-Authenticate
#   www-authenticate: Bearer resource_metadata=\"…\"
\`\`\`

Then re-try adding the MCP connector in Claude — if this was the only
blocker, OAuth should now complete.

## Related

- #19755, #19766, #19824 — prior fixes in the MCP/OAuth discovery chain
(host-aware metadata, path-aware well-known, \`TRUST_PROXY\` for
\`request.protocol\`). This PR completes the CORS side of that work.

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 21:28:59 +02:00
Félix Malfait
5223c4771d
fix(server): align OAuth discovery metadata with MCP / RFC 9728 spec (#19838)
## Summary

Three small spec-compliance fixes called out in an audit against the
[MCP authorization spec
(draft)](https://modelcontextprotocol.io/specification/draft/basic/authorization)
and RFC 9728 / RFC 9207.

### 1. Split Protected Resource Metadata by path (RFC 9728 §3.2)

> The `resource` value returned MUST be identical to the protected
resource's resource identifier value into which the well-known URI path
suffix was inserted.

Today a single handler serves both
\`/.well-known/oauth-protected-resource\` and
\`/.well-known/oauth-protected-resource/mcp\` and returns \`resource:
<origin>/mcp\` from both. That's wrong for the root form — per RFC 9728
the root URL corresponds to the **origin as resource**, and only the
\`/mcp\`-suffixed URL corresponds to \`<origin>/mcp\`.

After this PR:

| Request | `resource` field |
|---|---|
| `GET /.well-known/oauth-protected-resource` | `https://<host>` |
| `GET /.well-known/oauth-protected-resource/mcp` | `https://<host>/mcp`
|

Both still return the same `authorization_servers`, `scopes_supported`,
and `bearer_methods_supported`.

Claude's current flow happens to work because our WWW-Authenticate
points at the root form and Claude compares `resource` against what it
connected to. Strict clients probing the path-aware URL first were
rejecting us.

### 2. Advertise `authorization_response_iss_parameter_supported: true`
(RFC 9207)

Defense against OAuth mix-up attacks. Required by the [OAuth 2.1
security
BCP](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1).
Signals that clients receiving an authorization response will find the
issuer in the `iss` parameter and can validate it.

### 3. Fix `WWW-Authenticate` challenge: point at path-aware PRM URL,
add `scope` param

- Was: `Bearer
resource_metadata=\"https://<host>/.well-known/oauth-protected-resource\"`
- Now: `Bearer
resource_metadata=\"https://<host>/.well-known/oauth-protected-resource/mcp\",
scope=\"api profile\"`

After change (1), only the path-aware URL returns a PRM document whose
`resource` matches what the MCP client connected to (\`<host>/mcp\`).
Pointing clients at the right URL keeps discovery consistent.

The `scope` parameter is a SHOULD in RFC 6750 and lets clients ask for
least-privilege scopes on first authorization.

## Not in this PR (queued separately)

From the same audit:

- **Audit JWT `aud` (audience) validation** — the spec requires the
server to reject tokens whose audience doesn't match this resource. Need
a read-only code review to confirm; filing as a follow-up.
- **Audit PKCE enforcement** — we advertise
`code_challenge_methods_supported: [\"S256\"]`; need to confirm the
\`/authorize\` flow actually rejects requests missing `code_challenge`.
- **403 `insufficient_scope` challenge format** for step-up auth.
- **CIMD (Client ID Metadata Documents)** support — newer spec
alternative to DCR.

## Test plan

- [x] \`yarn jest
--testPathPatterns=\"mcp-auth.guard|oauth-discovery.controller\"\` → 4/4
passing
- [x] \`tsc --noEmit\` clean on touched files
- [ ] After deploy:
  \`\`\`bash
curl -s https://<host>/.well-known/oauth-protected-resource | jq
.resource
  # expect: \"https://<host>\"
curl -s https://<host>/.well-known/oauth-protected-resource/mcp | jq
.resource
  # expect: \"https://<host>/mcp\"
  curl -sI -X POST https://<host>/mcp | grep -i www-authenticate
# expect: Bearer resource_metadata=\"…/oauth-protected-resource/mcp\",
scope=\"api profile\"
  \`\`\`

## Related

- #19836 — CORS exposes `WWW-Authenticate` + `MCP-Protocol-Version` so
browser clients can read them. Pairs with this PR.
- #19755 / #19766 / #19824 — the earlier chain that got host-aware
discovery and \`TRUST_PROXY\` working.

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 21:28:44 +02:00
github-actions[bot]
3292f1758e
i18n - translations (#19844)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-18 21:18:30 +02:00
Félix Malfait
53d22a3b70
fix(server): require PKCE code_challenge for public OAuth clients (#19840)
## Summary

OAuth 2.1 and the MCP authorization spec mandate PKCE (S256) for public
clients — clients registered with \`token_endpoint_auth_method=none\`
(no client secret). We advertise \`code_challenge_methods_supported:
[\"S256\"]\` in \`/.well-known/oauth-authorization-server\` but our
\`/authorize\` flow accepted requests from public clients without
\`code_challenge\`.

## Why this was a soft failure today

\`oauth.service.ts:178\` already rejects token exchange when a client
presents neither \`client_secret\` nor \`code_verifier\`:

\`\`\`ts
if (!clientSecret && !storedCodeChallenge) {
return this.errorResponse('invalid_request', 'Either client_secret or
code_verifier (PKCE) is required');
}
\`\`\`

So a public client attempting to bypass PKCE would **eventually** fail —
but only after:
1. Getting a valid authorization code issued at \`/authorize\`
2. Round-tripping the user through consent
3. Trying to exchange the code at \`/token\` and finally getting
rejected

That's a wasted user interaction and a fuzzy spec boundary. This PR
rejects at \`/authorize\` instead, matching the spec's \"MUST require
PKCE for public clients\" expectation.

## Fix

Single check in \`AuthService.generateAuthorizationCode\`:

\`\`\`ts
const isPublicClient = !applicationRegistration.oAuthClientSecretHash;

if (isPublicClient && !codeChallenge) {
  throw new AuthException(
\`code_challenge is required for public clients (PKCE S256, per OAuth
2.1)\`,
    AuthExceptionCode.FORBIDDEN_EXCEPTION,
  );
}
\`\`\`

### Why \`!oAuthClientSecretHash\` is the right \"public\" predicate

- Dynamic registration (\`POST /oauth/register\`) hardcodes
\`oAuthClientSecretHash: null\` and rejects any
\`token_endpoint_auth_method != \"none\"\`
(oauth-registration.controller.ts:120-130).
- Confidential clients registered via the workspace settings UI have a
non-null bcrypt hash.
- The same field is already used as the public/confidential gate in
\`validateClient\` and \`validateClientSecret\`.

## Scope

-  Dynamic-registration clients (Claude, other MCP connectors) — MUST
now supply code_challenge. They already do; no behavior change for
conformant clients.
-  The seeded twenty-cli registration — public client, already uses
PKCE. No change.
-  Confidential clients (workspace-admin-registered OAuth apps with a
client_secret) — unaffected, they authenticate at the token endpoint.

## Related

- #19836 — CORS exposes \`WWW-Authenticate\` / \`MCP-Protocol-Version\`
- #19838 — RFC 9728 PRM split + RFC 9207 iss param + \`scope\` in
WWW-Authenticate challenge

## Test plan

- [x] \`tsc --noEmit\` clean on modified file (pre-existing
\`twenty-shared\` dist errors unrelated)
- [ ] Integration-level smoke test after deploy:
  \`\`\`bash
  # Register a dynamic client (public)
  CLIENT_ID=$(curl -s -X POST -H 'Content-Type: application/json' \\
-d
'{\"client_name\":\"pkce-test\",\"redirect_uris\":[\"http://localhost/cb\"]}'
\\
    https://<host>/oauth/register | jq -r .client_id)

# Without code_challenge → should now 4xx at /authorize (cannot easily
test outside the React UI,
  # but the GraphQL authorizeApp mutation will throw AuthException)
  \`\`\`
- [ ] Claude MCP connector still completes OAuth end-to-end (it always
sends code_challenge, so no-op)

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 21:17:22 +02:00
github-actions[bot]
1c54e79d6c
i18n - translations (#19843)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-18 21:14:49 +02:00
neo773
be9616db60
chore: remove draft email feature flag (#19842) 2026-04-18 21:12:01 +02:00
Félix Malfait
c28c20143b
refactor(server): rename Agent exception to Ai; add THREAD_NOT_FOUND / MESSAGE_NOT_FOUND codes (fixes 500s) (#19831)
## Summary

- The exception class under `ai-agent/` was serving every AI surface
(agent, chat, role, models, generate-text), so `Agent` was a misnomer.
Promoted to the `ai/` namespace; renamed `AgentException` →
`AiException`, `AgentExceptionCode` → `AiExceptionCode`, and related
interceptor / filter / handler / file names accordingly.
- Split the single `AGENT_NOT_FOUND` code into entity-specific codes.
Chat-thread lookups no longer reuse the agent identifier.
- **Fixes Sentry 500s on `GetChatMessages` / `chatThread`.** Every
"Thread not found" and "Queued message not found" throw site in ai-chat
was previously wired to `AGENT_EXECUTION_FAILED`, which maps to
`InternalServerError` (HTTP 500). They now use `THREAD_NOT_FOUND` /
`MESSAGE_NOT_FOUND`, both of which map to `NotFoundError` (HTTP 404) in
the GraphQL and REST handlers.

The underlying cause of *why* clients are asking for threads that no
longer resolve for them — per-user chat-thread create events being
broadcast workspace-wide — is addressed separately in a follow-up PR.

### Code map

- Added: `ai/ai.exception.ts`,
`ai/utils/ai-graphql-api-exception-handler.util.ts` (+ spec with new
THREAD/MESSAGE cases),
`ai/interceptors/ai-graphql-api-exception.interceptor.ts`,
`ai/filters/ai-api-exception.filter.ts`
- Deleted: `ai/ai-agent/agent.exception.ts`,
`ai/ai-agent/utils/agent-graphql-api-exception-handler.util.ts` (+
spec),
`ai/ai-agent/interceptors/agent-graphql-api-exception.interceptor.ts`,
`ai/ai-agent/filters/agent-api-exception.filter.ts`
- Updated: 21 call sites across ai-agent, ai-agent-execution,
ai-agent-role, ai-chat, ai-generate-text, ai-models, role, and
workspace-migration validators.

## Test plan

- [x] `npx nx typecheck twenty-server`
- [x] `npx jest ai-graphql-api-exception-handler` (3/3 including new
THREAD_NOT_FOUND and MESSAGE_NOT_FOUND cases)
- [x] `npx jest agent-role.service` (9/9)
- [x] `npx oxlint --type-aware` on all changed files (0 warnings/errors)
- [x] `npx prettier --check` on all changed files
- [ ] CI
2026-04-18 21:08:33 +02:00
Charles Bochet
4c94699376
Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 1.23.0-canary.1 (#19841)
## Summary
- Bumps `twenty-sdk`, `twenty-client-sdk`, and `create-twenty-app` from
`1.22.0` to `1.23.0-canary.1`.

## Test plan
- [ ] CI green

Made with [Cursor](https://cursor.com)
2026-04-18 19:41:01 +02:00
Charles Bochet
eb1ca1b9ec
perf(sdk): split twenty-sdk barrel into per-purpose subpaths to cut logic-function bundle ~700x (#19834)
## Summary

Logic-function bundles produced by the twenty-sdk CLI were ~1.18 MB even
for a one-line handler. Root cause: the SDK shipped as a single bundled
barrel (`twenty-sdk` → `dist/index.mjs`) that co-mingled server-side
definition factories with the front-component runtime, validation (zod),
and React. With no `\"sideEffects\"` declaration on the SDK package,
esbuild had to assume every module-level statement could have side
effects and refused to drop unused code.

This PR restructures the SDK so consumers' bundlers can tree-shake at
the leaf level:

- **Reorganized SDK source.** All server-side definition factories now
  live under `src/sdk/define/` (agents, application, fields,
  logic-functions, objects, page-layouts, roles, skills, views,
  navigation-menu-items, etc.). All front-component runtime
  (components, hooks, host APIs, command primitives) lives under
  `src/sdk/front-component/`. The legacy bare `src/sdk/index.ts` is
  removed; the bare `twenty-sdk` entry no longer exists.

- **Split the build configs by purpose / runtime env.** Replaced
  `vite.config.sdk.ts` with two purpose-specific configs:
  - `vite.config.define.ts` — node target, externals from package
    `dependencies`, emits to `dist/define/**`
  - `vite.config.front-component.ts` — browser/React target, emits to
    `dist/front-component/**`
  Both use `preserveModules: true` so each leaf ships as its own `.mjs`.

- **\`\"sideEffects\": false\`** on `twenty-sdk` so esbuild can drop
  unreferenced re-exports.

- **\`package.json\` exports + \`typesVersions\`** updated: dropped the
bare \`.\` entry, added \`./front-component\`, and pointed \`./define\`
  at the new per-module dist layout.

- **Migrated every internal/example/community app** to the new subpath
  imports (`twenty-sdk/define`, `twenty-sdk/front-component`,
  `twenty-sdk/ui`).

- **Added \`bundle-investigation\` internal app** that reproduces the
  bundle bloat and demonstrates the fix.

- Cleaned up dead \`twenty-sdk/dist/sdk/...\` references in the
  front-component story builder, the call-recording app, and the SDK
  tsconfig.

## Bundle size impact

Measured with esbuild using the same options as the SDK CLI
(\`packages/twenty-apps/internal/bundle-investigation\`):

| Variant | Imports | Before | After |
| ----------------------- |
------------------------------------------------------- | ---------- |
--------- |
| \`01-bare\` | \`defineLogicFunction\` from \`twenty-sdk/define\` |
1177 KB | **1.6 KB** |
| \`02-with-sdk-client\` | + \`CoreApiClient\` from
\`twenty-client-sdk/core\` | 1177 KB | **1.9 KB** |
| \`03-fetch-issues\` | + GitHub GraphQL fetch + JWT signing + 2
mutations | 1181 KB | **5.8 KB** |
| \`05-via-define-subpath\` | same as \`01\`, via the public subpath |
1177 KB | **1.7 KB** |

That's a ~735× reduction on the bare baseline. Knock-on benefits for
Lambda warm + cold starts, S3 upload size, and \`/tmp\` disk usage in
warm containers.

## Test plan

- [x] \`npx nx run twenty-sdk:build\` succeeds
- [x] \`npx nx run twenty-sdk:typecheck\` passes
- [x] \`npx nx run twenty-sdk:test:unit\` passes (31 files / 257 tests)
- [x] \`npx nx run-many -t typecheck
--projects=twenty-front,twenty-server,twenty-front-component-renderer,twenty-sdk,twenty-shared,bundle-investigation\`
passes
- [x] \`node
packages/twenty-apps/internal/bundle-investigation/scripts/build-variants.mjs\`
produces the sizes above
- [ ] CI green

Made with [Cursor](https://cursor.com)
2026-04-18 19:38:34 +02:00
Félix Malfait
fd495ee61b
fix(server): deliver user-scoped metadata events only to the owning user (#19832)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
## Summary

Fixes cross-user cache contamination for AI chat threads in multi-user
workspaces.

`agentChatThread` is the only user-scoped (`userWorkspaceId`-filtered)
entry in `METADATA_NAME_TO_ENTITY_KEY`, but its create/update events
were going through `WorkspaceEventBroadcaster`, which fans out to every
active SSE stream in the workspace. Every client therefore received
other users' threads into their local `agentChatThreads` metadata store
(which is persisted to localStorage). On a subsequent session,
`AgentChatThreadInitializationEffect` would pick the
most-recently-updated thread — potentially another user's — and fire
`GetChatMessages` against it; the server's `userWorkspaceId` filter
didn't match, producing "Thread not found" errors (now 404 thanks to
twentyhq/twenty#19831).

### The fix mirrors the RLS pattern already used for object records

`ObjectRecordEventPublisher` already reads
`streamData.authContext.userWorkspaceId` to filter per subscriber.
Metadata events had no equivalent. This PR closes that gap with a
minimal, opt-in change:

- Add optional `recipientUserWorkspaceIds?: string[]` to
`WorkspaceBroadcastEvent`. Omit → workspace-wide (unchanged for views,
objects, fields, etc.). Set → delivered only to streams whose
`authContext.userWorkspaceId` is in the list.
- `WorkspaceEventBroadcaster.broadcast` builds the payload per stream
and skips events whose recipient doesn't match.
- Both `agentChatThread` broadcast call sites in `AgentChatService` now
pass `recipientUserWorkspaceIds: [userWorkspaceId]`.

### Scope

3 files, +40/-11 LOC:
-
`subscriptions/workspace-event-broadcaster/types/workspace-broadcast-event.type.ts`
-
`subscriptions/workspace-event-broadcaster/workspace-event-broadcaster.service.ts`
- `metadata-modules/ai/ai-chat/services/agent-chat.service.ts`

### Stale cache note

Existing clients still carry poisoned localStorage from before this
lands. They self-heal on sign-out/in because
`clearAllSessionLocalStorageKeys` already drops `agentChatThreads`. If
we want to actively flush on deploy, a frontend cache-version bump can
follow in a separate PR.

### Related

- twentyhq/twenty#19831 turns the resulting "Thread not found" from 500
to 404. This PR addresses the root cause; #19831 stops the Sentry noise.

## Test plan

- [x] `npx nx typecheck twenty-server`
- [x] `npx oxlint --type-aware` on all 3 files (0 warnings/errors)
- [x] `npx prettier --check` on all 3 files
- [ ] Manual: two users in the same workspace, user A creates/sends a
chat thread — confirm user B's session does not receive the event and
their `chatThreads` sidebar is unaffected
- [ ] CI
2026-04-18 12:26:21 +02:00
Charles Bochet
70a73534c0
perf(server): reuse ESM module cache across warm Lambda invocations of logic functions (#19830)
## Summary

Lambda warm-invocations of logic functions were spending **~440 ms**
re-parsing and re-evaluating the user bundle on every call. The executor
wrote the user code to a **randomly-named** temp file and `import()`-ed
it, so each warm call resolved to a new URL and Node's ESM cache could
never reuse the previous module record.

This PR makes the executor write to a **content-hash filename**, skip
the write when the file already exists, and stop deleting it. Identical
code now reuses the same module record across warm calls in the same
container, dropping warm-invocation overhead by **~30–40%**.

## What changed

- `executor/index.mjs`: temp filename derived from `sha256(code)`, write
skipped when file exists, no `fs.rm` on cleanup.
- `lambda.driver.ts`: single structured `[lambda-timing]` log per
invocation with `totalMs / buildExecutorMs / getBuiltCodeMs /
payloadBytes / invokeSendMs / reportDurationMs / billedMs /
initDurationMs / coldStart`. Goes through the standard NestJS `Logger`.

No behavioural change for callers: same input → same output, same error
semantics.

### Caveat: module-scope state now persists across warm calls

With a stable filename, the user bundle is evaluated **once per warm
container**. Any module-scoped state or top-level side-effects in user
code are now shared across invocations of the same container, instead of
re-running on every call. This is documented in the executor and is the
intended trade-off — module scope should be treated as a per-container
cache, not as per-call isolation.

## Findings — measured impact

Same logic function (`fetch-prs`, ~12k PRs to page through), same
workspace, same Lambda config (eu-west-3, 512 MB), token cache primed.

### Warm invocations

| Phase | Before fix | After fix | Δ |
| -------------------------------------- | ------------- |
-------------- | ------------ |
| Executor `import(userBundle)` | ~440 ms | **~0 ms** | **-440 ms** |
| Lambda billed duration | ~1.5–1.7 s | **~1.0–1.1 s** | **~30–40%** |
| Server-perceived round-trip | ~1.7–2.0 s | **~1.0–1.2 s** |
**~30–40%** |

### Cold starts

Unchanged — the cache helps subsequent warm calls in the same container,
not the first one. Init Duration stays ~130–170 ms; total cold call
~2.5–3.0 s.

### Stress

Could not reproduce the previously-reported \"every ~10th call times
out\" behaviour after the fix:

- 30 sequential calls: max 1.7 s, median ~1.1 s, 0 timeouts
- 50 concurrent calls: max 9.4 s (clear cold-start cluster), median ~1.5
s, 0 timeouts

Hypothesis: the warm-import overhead was eating into the headroom
against the function timeout under bursty load; removing it pushed
everything well below the limit.

## Observability

One structured log line per invocation, sent through the standard NestJS
logger:

\`\`\`
[lambda-timing] fnId=abc123 totalMs=1187 buildExecutorMs=2
getBuiltCodeMs=3 payloadBytes=1466321 invokeSendMs=1180
reportDurationMs=992 billedMs=1000 initDurationMs=n/a coldStart=false
\`\`\`

\`coldStart=true\` whenever Lambda spun up a fresh container; on warm
calls \`buildExecutorMs\` and \`getBuiltCodeMs\` collapse to
single-digit ms, confirming the cache fix is working.

## Test plan

- [ ] CI green.
- [ ] Deploy to a Lambda-backed env, trigger a logic function several
times in a row.
- [ ] Confirm \`[lambda-timing]\` warm invocations show \`totalMs\`
~30–40% lower than before, and \`coldStart=false\` after the first call
in a container.
- [ ] Push a new version of an app; confirm the next call shows higher
\`buildExecutorMs\` (new hash, new file written) followed by warm calls
again.
- [ ] Smoke test: errors thrown by the user handler are still surfaced
correctly.

Made with [Cursor](https://cursor.com)
2026-04-18 11:30:56 +02:00
neo773
1d575f0496
fix oauth permission check (#19829)
was regressed due to https://github.com/twentyhq/twenty/pull/19441
2026-04-18 11:20:23 +02:00
github-actions[bot]
768469f2bd
chore: sync AI model catalog from models.dev (#19828)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-18 08:21:55 +02:00
Félix Malfait
b292a93376
fix(server): honor X-Forwarded-* via configurable trust proxy (#19824)
## The bug

Pasting `https://<workspace>.twenty.com/mcp` into an MCP client (Claude
connector, etc.) fails discovery. Curl shows why:

```bash
$ curl -si https://twentyfortwenty.twenty.com/.well-known/oauth-protected-resource
HTTP/2 200
...
{
  \"resource\": \"http://{workspace}.twenty.com/mcp\",
  \"authorization_servers\": [\"http://twentyfortwenty.twenty.com\"],
  ...
}
```

The response advertises `http://` even though the request came in on
`https://`. RFC 9728 / RFC 8707 require the client to validate that the
advertised `resource` matches the URL it connected to, so strict MCP
clients reject the mismatch and OAuth never starts.

## Why request.protocol returns \"http\"

Per [Express docs](https://expressjs.com/en/guide/behind-proxies.html),
`request.protocol` returns the socket-level protocol unless
`app.set('trust proxy', ...)` is configured. In our deployment:

```
client -- https --> Cloudflare -- https --> ingress-nginx -- http --> NestJS pod
```

TLS is terminated at the edge. The upstream TCP connection into the pod
is plain HTTP, and nginx sets `X-Forwarded-Proto: https` for the pod to
read. Without a `trust proxy` setting, Express ignores
`X-Forwarded-Proto` and `request.protocol === 'http'`.

`main.ts` currently has no `app.set('trust proxy', ...)` call anywhere.

## Why this only surfaced now

`grep -rn request.protocol` finds three pre-existing call sites —
`RestApiMetadataService`, `OpenApiService`, `RouteTriggerService`. All
three wrap it in `getServerUrl({ serverUrlEnv: SERVER_URL,
serverUrlFallback: \`${request.protocol}://${request.get('host')}\` })`,
which returns `SERVER_URL` whenever it's non-empty. In production
`SERVER_URL` is always set (e.g. \`api.twenty.com\`), so the
\`request.protocol\` branch is effectively dead code there.

#19755 introduced the first call site that uses `request.protocol`
unconditionally — the OAuth discovery controller has to echo the request
host, because the whole point is supporting multiple paste-able origins
(workspace subdomains, custom domains, etc.). That's why this is the
first \"wrong protocol\" bug anyone has seen in our app.

## The fix

One line in `main.ts`:

```ts
app.set('trust proxy', twentyConfigService.get('TRUST_PROXY'));
```

Backed by a new `TRUST_PROXY` env var with a default. `request.protocol`
then honors `X-Forwarded-Proto`, `request.ip` honors `X-Forwarded-For`,
etc. OAuth discovery URLs come out on the right scheme, and any future
`request.protocol` callers Just Work.

## Why this needs to be configurable (not hardcoded)

Twenty is open-source and deployed in at least three distinct
topologies:

1. **Kubernetes with ingress** (us, enterprise self-hosters) — TLS
terminated upstream, needs `trust proxy` **on**.
2. **Self-host behind a user-supplied reverse proxy** (Caddy, Traefik,
nginx — our [recommended
setup](https://twenty.com/developers/section/self-hosting)) — same as
above, needs `trust proxy` **on**.
3. **Self-host with NestJS exposed directly to the internet** — no
upstream proxy, needs `trust proxy` **off** (otherwise any curl with
`X-Forwarded-For: 1.2.3.4` spoofs `request.ip`, poisoning rate-limiters
and audit logs).

There is no single static value that's correct for all three. Express
makes this a setting for exactly this reason — we follow suit.

## Why the default is `'loopback, linklocal, uniquelocal'`

Shorthand for loopback (127/8, ::1), link-local (169.254/16, fe80::/10),
and unique-local (10/8, 172.16/12, 192.168/16, fc00::/7). In practical
terms: **trust peers coming from private networks; don't trust the
public internet**.

This default is correct for shapes 1 and 2 (cloud, proxied self-host)
because the ingress/proxy peer is always a private-network IP in every
sane deployment.

For shape 3 (directly exposed), the default is still safe because public
clients have public IPs, which are not in any of those ranges — so
`X-Forwarded-For` from an attacker on the internet is ignored. The only
way to be bitten is the exotic case where a public client reaches NestJS
through a private-network hop that isn't a proxy (e.g. a NAT appliance
that forwards to the pod on a private IP and blindly appends headers).
Narrow attack surface, and an operator running that kind of setup is
expected to configure `TRUST_PROXY=false` explicitly.

\"Safer than the naïve `true`, more useful than `false`\" — this matches
what Rails, Django, and many other frameworks recommend for
Kubernetes-style deployments.

## Why an env var instead of hardcoded

- Rejecting hardcoded `true`: would expose shape-3 self-hosters to IP
spoofing without a way to opt out.
- Rejecting hardcoded `false`: would leave cloud + shape-2 self-hosters
broken, same bug as today.
- Accepting string-typed env (not boolean): Express's `trust proxy`
accepts booleans, hop counts (`1`, `2`), IP ranges (`'10.0.0.0/8'`), and
named CIDRs (`'loopback'`). A boolean would hide that flexibility;
operators occasionally need the richer values. The string maps 1:1 onto
what Express accepts.

## Deployment matrix

| Deployment | Default works? | Override needed? |
|---|---|---|
| Cloud (us, K8s + nginx ingress + Cloudflare) | ✓ | — |
| Self-host behind reverse proxy (recommended) | ✓ | — |
| Self-host exposed directly on public IP | ✓ (public IPs not in private
ranges) | Optional: `TRUST_PROXY=false` for strictness |
| Local dev (direct, no proxy) | ✓ (no `X-Forwarded-*` headers arrive) |
— |
| Exotic: multi-hop through non-sanitizing private-network middlebox |
Risky | `TRUST_PROXY=false` |

## Related

- Blocks MCP connector OAuth on `<ws>.twenty.com` / custom domains.
After deploy: `curl -s
https://<ws>.twenty.com/.well-known/oauth-protected-resource | jq
.resource` should return `https://...` (not `http://...`).
- Fixes latent issue in `RestApiMetadataService`, `OpenApiService`,
`RouteTriggerService` fallback paths (pre-existing but dead in
production because `SERVER_URL` is always set — no behavior change
there).

## Test plan

- [x] `tsc --noEmit` clean
- [ ] After deploy: `curl -s
https://twentyfortwenty.twenty.com/.well-known/oauth-protected-resource`
returns `https://` URLs
- [ ] After deploy: MCP connector in Claude successfully completes OAuth
against `https://<ws>.twenty.com/mcp`
- [ ] No change in `request.ip` logging behavior on cloud (nginx-ingress
peer is already private-network, was already being trusted implicitly by
every framework layer that wasn't `request.protocol`)

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 06:03:30 +02:00
Charles Bochet
4fa2c400c0
fix(server): skip standard page layout widgets referencing missing field metadatas during 1.23 backfill (#19825)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
## Summary

The 1.23 backfill command (`upgrade:1-23:backfill-record-page-layouts`)
creates standard page layout widgets from `STANDARD_PAGE_LAYOUTS`. Some
widgets reference field metadatas via `universalConfiguration` (e.g. the
`opportunity.owner` FIELD widget pointing at universal identifier
`20202020-be7e-4d1e-8e19-3d5c7c4b9f2a`).

If a workspace's matching field metadata does not exist or has a
different universal identifier (e.g. older workspaces created before
standard universal identifiers were backfilled), the runner throws

```
Field metadata not found for universal identifier: 20202020-be7e-4d1e-8e19-3d5c7c4b9f2a
```

and the entire migration for that workspace aborts. This was the
underlying cause behind the `Migration action 'create' for
'pageLayoutWidget' failed` error surfaced by #19823.
2026-04-17 23:59:13 +02:00
Charles Bochet
e878d646ed
chore(server): bump logic-function executor lambda memory to 512MB (#19826)
## Summary

The executor lambda for user logic functions is created in
`LambdaDriver` without a `MemorySize` parameter, so AWS Lambda falls
back to its 128 MB default. That cap is too tight for non-trivial logic
functions — large upstream GraphQL responses, JSON parsing of paginated
batches, and chained Twenty Core API mutations push the process over the
limit and trigger an OOM SIGKILL surfaced to the user as:

```
Runtime exited with error: signal: killed
```

This bumps the executor lambda memory to **512 MB** (matching the
existing `BUILDER_LAMBDA_MEMORY_MB`). The change is applied on both:

- The `CreateFunctionCommand` path used when a logic function is first
deployed.
- The `UpdateFunctionConfigurationCommand` path used when the deps/SDK
layer wiring is refreshed — so existing functions get reconfigured on
next deploy without any additional manual action.

## Why 512 MB

Lambda compute is allocated proportionally to memory. 512 MB:
- Matches the builder lambda already in this file
(`BUILDER_LAMBDA_MEMORY_MB`).
- Is comfortably above the 128 MB default that the existing OOMs are
hitting.
- Stays well below the higher tiers, keeping the per-invocation cost
increase modest.


Made with [Cursor](https://cursor.com)
2026-04-17 23:59:05 +02:00
Charles Bochet
fb5a1988b1
fix(server): log inner errors of WorkspaceMigrationRunnerException in workspace iterator (#19823)
## Summary

When a workspace migration action fails during workspace iteration (e.g.
during upgrade commands), only the wrapper message was logged:

```
[WorkspaceIteratorService] Error in workspace 7914ba64-...: Migration action 'create' for 'pageLayoutWidget' failed
```

The underlying error (transpilation/metadata/workspace schema) and its
stack were swallowed, making production debugging painful.

This PR adds a follow-up log entry for each inner error attached to a
`WorkspaceMigrationRunnerException`, including its message and stack
trace. The runner exception itself is untouched — it already exposes
structured `errors` (`actionTranspilation`, `metadata`,
`workspaceSchema`).

After this change, logs look like:

```
[WorkspaceIteratorService] Error in workspace 7914ba64-...: Migration action 'create' for 'pageLayoutWidget' failed
[WorkspaceIteratorService] Caused by actionTranspilation in workspace 7914ba64-...: <real reason>
    at ...
```

## Test plan

- [ ] Trigger a failing workspace migration (e.g. backfill record page
layouts) on a workspace and confirm the underlying cause + stack now
appear in logs.

Made with [Cursor](https://cursor.com)
2026-04-17 22:31:35 +02:00
Charles Bochet
3eeaebb0cc
fix(server): make workspace:seed:dev --light actually seed only one workspace (#19822)
## Summary

The `--light` flag of `workspace:seed:dev` was supposed to seed a single
workspace for thin dev containers, but it was only filtering the rich
workspaces (Apple, YCombinator) — the `Empty3`/`Empty4` fixtures
introduced in #19559 for upgrade-sequence integration tests were always
seeded.

So `--light` actually produced **3** workspaces:
- Apple
- Empty3
- Empty4

In single-workspace mode (`IS_MULTIWORKSPACE_ENABLED=false`, the default
for the `twenty-app-dev` container),
[`WorkspaceDomainsService.getDefaultWorkspace`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service.ts)
returns the most recently created workspace — Empty4 — which has no
users. The prefilled `tim@apple.dev` therefore cannot sign in, which
breaks flows that depend on the default workspace such as `yarn twenty
remote add --local`'s OAuth handshake against the dev container.

This PR makes `--light` actually skip the empty fixtures so the dev
container ends up with a single workspace (Apple). The default (no flag)
invocation, used by `database:reset` for integration tests, still seeds
all four workspaces, so
`upgrade-sequence-runner-integration-test.util.ts` keeps working
unchanged.
2026-04-17 22:08:01 +02:00
Nathan Nguyen
59e4ed715a
fix(server): normalize empty composite phone sub-fields to NULL (#19775)
Fixed using Opus 4.7, I wanted to test this model out and in this repo I
know you guys care about quality, pls let me know if this is good code.
It looks good to me

Fixes #19740.

## Summary

PostgreSQL UNIQUE indexes treat two `''` values as duplicates but two
`NULL`s as distinct. `validateAndInferPhoneInput` was persisting blank
`primaryPhoneNumber` as `''` instead of `NULL`, so a second record with
an empty unique phone failed with a constraint violation. The sibling
composite transforms (`transformEmailsValue`, `removeEmptyLinks`,
`transformTextField`) already canonicalize null-equivalent values;
phones was the outlier.

- Empty-string phone sub-fields now normalize to `null`. `undefined` is
preserved so partial updates leave columns the user did not touch alone.
- `PhonesFieldGraphQLInput` drops the aspirational `CountryCode` brand
on input. GraphQL delivers raw strings at the boundary; branding happens
during validation.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-17 19:36:43 +00:00
github-actions[bot]
ab85946102
i18n - translations (#19821)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-17 21:37:54 +02:00
martmull
38a03abc06
Fix app design 5 (#19820)
small fixes
2026-04-17 19:22:17 +00:00
Weiko
13b32a22b6
Add page layout tab icon picker (#19818)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
Adds the ability to change the icon of a record page layout tab from the
side panel in tab edit mode, and sets a default icon for newly-created
record page tabs (no default for dashboards).

<img width="916" height="312" alt="Screenshot 2026-04-17 at 19 55 51"
src="https://github.com/user-attachments/assets/d9f57e89-d40d-483e-b508-5d7318df1ef5"
/>
2026-04-17 18:19:46 +00:00
github-actions[bot]
4a5702328a
i18n - translations (#19819)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-17 20:14:22 +02:00
neo773
9307c718cf
Add twenty-managed Docker target with AWS CLI for EKS deployments (#19816)
Separate build target so self-hosters have slimmer image but managed
infra gets aws cli for automation
2026-04-17 17:54:10 +00:00
Weiko
b320de966c
Add reset page layout in record page layout edit mode tab (#19800)
## Context
Adds a burger-menu dropdown on the layout customization bar exposing a
"Reset record page layout" action, so users can reset a record page
layout straight from the edit bar (previously only available in object
settings).

<img width="1512" height="849" alt="Screenshot 2026-04-17 at 18 03 19"
src="https://github.com/user-attachments/assets/145a77b8-6234-4987-ae31-38eccaa0548d"
/>
2026-04-17 17:46:42 +00:00
github-actions[bot]
b8f8892b67
i18n - translations (#19817)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-17 19:49:00 +02:00
Weiko
df9c4e26b5
Disable reset to default when custom tab or widget (#19814)
## Context
"Reset to default" action is rejected by the backend for custom entities
because there is no "default" concept for them

## Implementation
Grey out the Reset to default action on record page-layout tabs and
widgets when the entity either has no applicationId yet (unsaved draft —
previously slipped through the existing check), or belongs to the
workspace custom application.

<img width="926" height="370" alt="Screenshot 2026-04-17 at 18 50 31"
src="https://github.com/user-attachments/assets/c7c163f4-17a6-4b69-a66d-90f9085d27a2"
/>
2026-04-17 17:30:57 +00:00
Raphaël Bosi
619ea13649
Twenty for twenty app (#19804)
## Twenty for Twenty: Resend module

Introduces `packages/twenty-apps/internal/twenty-for-twenty`, the
official internal Twenty app, with a first module integrating
[Resend](https://resend.com).

### Breakdown

**Resend module** (`src/modules/resend/`)
- Two app variables: `RESEND_API_KEY` and `RESEND_WEBHOOK_SECRET`.
- **Objects**: `resendContact`, `resendSegment`, `resendTemplate`,
`resendBroadcast`, `resendEmail`, with relations between them and to
standard `person`.
- **Inbound sync (Resend → Twenty)**:
- Cron-driven logic function `sync-resend-data` (every 5 min) pulling
all entities through paginated, rate-limit-aware utilities
(`sync-contacts`, `sync-segments`, `sync-templates`, `sync-broadcasts`,
`sync-emails`).
- Webhook endpoint (`resend-webhook`) verifying signatures and handling
`contact.*` and `email.*` events in real time.
- `find-or-create-person` auto-links Resend contacts to Twenty people by
email.
- **Outbound sync (Twenty → Resend)**: DB-event logic functions for
`contact.created/updated/deleted` and `segment.created/deleted`, with a
`lastSyncedFromResend` field for loop prevention.
- **UI**: views, page layouts, navigation menu items, and front
components (`HtmlPreview`, `RecordHtmlViewer`) to preview email/template
HTML in record pages; `sync-resend-data` command exposed as a front
component.

### Setup

See the new README for install steps, webhook configuration, and local
testing with the Resend CLI.
2026-04-17 17:29:09 +00:00
Abdullah.
ce2a0bfbe5
fix: socket.io allows an unbounded number of binary attachments (#19812)
Resolves [Dependabot Alert
683](https://github.com/twentyhq/twenty/security/dependabot/683).
2026-04-17 17:17:56 +00:00
Weiko
a18840f3cd
Fix deactivated tabs not visible in new tab action (#19811)
## Context
On custom objects, clicking "+ New Tab" on a record page layout never
exposed deactivated tabs for reactivation, even though isActive: false
tabs were correctly returned by the API. Standard objects worked fine.

## Fix
isReactivatableTab gated reactivation on tab.applicationId ===
objectMetadata.applicationId. For custom objects these two ids are
intentionally different.
This check was unnecessary after all, we simply want to check if a tab
is inactive (only non-custom entities can be de-activated) 👍

<img width="694" height="551" alt="Screenshot 2026-04-17 at 18 14 28"
src="https://github.com/user-attachments/assets/42485cb2-8be5-4a55-a311-479ed3226908"
/>
2026-04-17 17:10:14 +00:00
Thomas des Francs
6095798434
Add SVG export and refine halftone studio controls (#19813)
## Summary
- Added image-mode SVG export and clipboard copy support for the
halftone generator.
- Reworked the export panel UX into separate `Download` and `Copy`
sections with format-only buttons.
- Simplified the SVG output to reduce redundant segments while
preserving the rendered result.
- Updated related halftone canvas, state, exporter, and illustration
code to support the new flow.

## Testing
- `yarn nx typecheck twenty-website-new`
- `yarn nx build twenty-website-new`
2026-04-17 16:53:38 +00:00
Weiko
0cb50f8a9d
Fix indexFieldMetadata select missing workspaceId (#19806) 2026-04-17 17:34:27 +02:00
github-actions[bot]
47a742fd01
i18n - translations (#19805)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-17 17:09:25 +02:00
martmull
120ced44a9
Fix app design 4 (#19803)
## Before

<img width="1512" height="584" alt="image"
src="https://github.com/user-attachments/assets/2a05d0c7-4bba-438f-9b05-4abd159530ba"
/>
<img width="1512" height="908" alt="image"
src="https://github.com/user-attachments/assets/a36da096-505d-4f25-84bc-a0feca436d53"
/>


## After

<img width="1503" height="574" alt="image"
src="https://github.com/user-attachments/assets/e039b92f-057a-4ed7-869a-a248f446eb2b"
/>
<img width="1512" height="904" alt="image"
src="https://github.com/user-attachments/assets/065767b5-8a70-4ea7-a520-5b2ccbdcffa3"
/>

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-04-17 14:52:41 +00:00
Weiko
3268a86f4b
Skip backfill record page layouts for missing standard objects (#19799) 2026-04-17 13:09:11 +00:00
neo773
68746e22a0
Send Email Tool: Don't persist message on SMTP only connections (#19756)
Previously this blocked users who only had SMTP configured to send
outbound emails, this fixes it by making messageChannel and persist
layer conditional
2026-04-17 12:56:38 +00:00
Charles Bochet
4ed6fcd19e
chore: move TABLE_WIDGET view type migration to 1.23 fast instance command (#19797)
## Summary

- Relocates `AddTableWidgetViewTypeFastInstanceCommand` from `1-22/` to
`1-23/` and bumps its `@RegisteredInstanceCommand` version from `1.22.0`
to `1.23.0`. The original timestamp `1775752190522` is preserved so the
command slots chronologically into the existing 1.23 sequence;
auto-discovered via `@RegisteredInstanceCommand`, no module wiring
change needed.
- Same pattern as #19792 (move
`pageLayoutWidget.conditionalAvailabilityExpression` to 1.23).
2026-04-17 12:44:35 +00:00
Weiko
e70269b9d3
Fix: aggregate Calculate not updating in dashboard Table widgets (#19796)
## Context
Picking an aggregate option (Count, Sum, Percentage Not Empty, etc.) in
a dashboard Table widget footer did nothing visually — the value never
appeared or updated

## Fix
RecordTableWidget was missing RecordIndexTableContainerEffect, which
reactively syncs currentView.viewFields[].aggregateOperation from the
Apollo cache into the viewFieldAggregateOperationState jotai atom that
the footer reads

<img width="612" height="261" alt="Screenshot 2026-04-17 at 14 17 47"
src="https://github.com/user-attachments/assets/b4409b0e-82a6-4614-bc09-653be738134a"
/>
2026-04-17 12:34:11 +00:00
github-actions[bot]
f94ee2d495
i18n - translations (#19795)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-17 14:15:46 +02:00
Paul Rastoin
bb464b2ffb
Forbid other app role extension (#19783)
# Introduction
Even though this would not possible through API at the moment, from
neither API metadata or manifest ( as manifest `permissionsFlag`
declarations etc are done from within a declared role )
Prevent any app to create permissions entities over another app role
from the validation engine itself

## `isEditable`
We might wanna deprecate this column at some point from the entity it
self as now the grain would rather be `what app owns that role ?`

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-17 12:00:37 +00:00
Abdul Rahman
a93f23a150
Fix AI chat dropzone persisting when dragging file out without dropping (#19794)
Issue link:
https://discord.com/channels/1130383047699738754/1494248499351519232

### Before


https://github.com/user-attachments/assets/ec4ba8cc-b9e7-4b77-8b8f-b254e11edb19



### After



https://github.com/user-attachments/assets/2905536b-6a31-41e8-9f25-8768fd224b6a
2026-04-17 11:43:02 +00:00
Charles Bochet
beeb8b7406
chore: move pageLayoutWidget.conditionalAvailabilityExpression migration to 1.23 fast instance command (#19792)
## Summary

- Replaces the standalone TypeORM migration
`1775654781000-addConditionalAvailabilityExpressionToPageLayoutWidget.ts`
with a registered fast instance command under
`packages/twenty-server/src/database/commands/upgrade-version-command/1-23/`,
so the `pageLayoutWidget.conditionalAvailabilityExpression` column is
created through the unified upgrade pipeline.
- Uses `ADD COLUMN IF NOT EXISTS` / `DROP COLUMN IF EXISTS` so the new
instance command is a safe no-op for environments that already applied
the previous TypeORM migration.
- Keeps the original timestamp `1775654781000` so the command slots
chronologically into the existing 1.23 sequence; auto-discovered via
`@RegisteredInstanceCommand`, no module wiring needed.

## Context

Reported error when creating a new workspace on `main`:

> column PageLayoutWidgetEntity.conditionalAvailabilityExpression does
not exist

Aligns this column addition with the rest of the 1.23 schema changes
that already use the instance-command pattern.
2026-04-17 11:40:05 +00:00
Weiko
5cd8b7899d
shouldIncludeRecordPageLayouts deprecation (#19774)
## Context
Deprecating shouldIncludeRecordPageLayouts in preparation for page
layout release.

See new workspace with standard page layout from standard app
<img width="570" height="682" alt="Screenshot 2026-04-16 at 18 35 23"
src="https://github.com/user-attachments/assets/bf7fa621-d40d-4c29-8d96-537c58b3eb40"
/>
2026-04-17 11:32:10 +00:00
Charles Bochet
76ea0f37ed
Surface structured validation errors during application install (#19787)
## Summary
- Add `WorkspaceMigrationGraphqlApiExceptionInterceptor` to
`MarketplaceResolver` and `ApplicationInstallResolver` so validation
failures during app install return `METADATA_VALIDATION_FAILED` with
structured `extensions.errors` instead of generic
`INTERNAL_SERVER_ERROR`
- Update SDK `installTarballApp()` to pass the full GraphQL error object
(including extensions) through the install flow
- Add `formatInstallValidationErrors` utility to format structured
validation errors for CLI output
- Add integration test verifying structured error responses for invalid
navigation menu items and view fields

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 11:29:33 +00:00
Weiko
7aa60fd20b Revert "Fix"
This reverts commit 70603a1af6.
2026-04-17 13:15:57 +02:00
Weiko
70603a1af6 Fix 2026-04-17 13:15:04 +02:00
Marie
b31f84fbb8
fix(server): workspace member permissions and profile onboarding (#19786)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
## Summary

Aligns **workspace member** editing and **onboarding** with how the
product is actually used: profile and other “settings” fields go through
**`updateWorkspaceMemberSettings`**, while **`/graphql`** record APIs
follow **object-level** permissions for the `workspaceMember` object.

## Product behaviour

### Completing “Create profile” onboarding

Users who must create a profile (empty name at sign-up) get
`ONBOARDING_CREATE_PROFILE_PENDING` set. The onboarding UI saves the
name with **`updateWorkspaceMemberSettings`**, not with a workspace
record **`updateOne`**.

**Before:** The server only cleared the pending flag on
**`workspaceMember.updateOne`**, so the flag could stay set and
onboarding appeared stuck.

**After:** Clearing the profile step runs when
**`updateWorkspaceMemberSettings`** persists an update that includes a
**name** (same rules as before: non-empty name parts). Onboarding can
advance normally after **Continue** on Create profile.

### Two ways to change workspace member data

| Path | Typical use | Who can change what |
|------|----------------|---------------------|
| **`updateWorkspaceMemberSettings`** (metadata API) | Standard member
fields the app treats as “my profile / preferences” (name,
avatar-related settings, locale, time zone, etc.) | **Always** your
**own** workspace member. Changing **another** member still requires
**Workspace members** in role settings (`WORKSPACE_MEMBERS`). Custom
fields are **not** allowed on this endpoint (unchanged). |
| **`/graphql`** record mutations on **`workspaceMember`** | Custom
fields, integrations, anything that goes through the generic record API
| **`WorkspaceMember`** is special-cased in permissions: **read** stays
**on** for everyone, but **update / create / delete** require
**`WORKSPACE_MEMBERS`**, including updating **your own** row via
`/graphql`. So a **Member** without that permission cannot fix their
name through **`updateWorkspaceMember`**; they use **Settings** /
**`updateWorkspaceMemberSettings`** instead. |

This matches **`WorkspaceRolesPermissionsCacheService`**: for the
workspace member object, `canReadObjectRecords` is always true;
`canUpdateObjectRecords` (and delete-related flags) follow
**`WORKSPACE_MEMBERS`**.

### Hooks and delete side-effects

- Removed **`workspaceMember.updateOne`** pre-query hook and
**`WorkspaceMemberPreQueryHookService`**: they duplicated the same rules
the permission cache already enforces for `/graphql`.
- **`WorkspaceMember.deleteOne`** pre-hook still tells users to remove
members via the dedicated flow; the post-hook only runs the
**`deleteUserWorkspace`** side-effect when a member row is actually
removed—**no** extra settings-permission check there, since only callers
that already passed **object** delete permission can remove the row.

## Tests

- **`workspace-members.integration-spec.ts`**: clarifies and extends
coverage so **`/graphql`** **`updateOne`** is denied for **own** record
on a **standard** name field and on a **custom** field when the role
lacks **`WORKSPACE_MEMBERS`**.

## Implementation notes

- **`OnboardingService.completeOnboardingProfileStepIfNameProvided`**
centralises the “clear profile pending if name present” logic;
**`UserResolver.updateWorkspaceMemberSettings`** calls it after save,
using the typed update payload’s **`name`** (no cast).
- **`UserWorkspaceService.updateUserWorkspaceLocaleForUserWorkspace`**:
drops a redundant **`coreEntityCacheService.invalidate`**;
**`updateWorkspaceMemberSettings`** still invalidates the user-workspace
cache after the mutation.
2026-04-17 09:58:34 +00:00
github-actions[bot]
ba1195d92e
i18n - translations (#19784)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-17 09:34:28 +02:00
Abdul Rahman
fcba0ca30a
Add search to add column dropdown (#19763)
https://github.com/user-attachments/assets/4a64fff0-6495-4651-934b-43f4ad0dc966
2026-04-17 07:19:07 +00:00
github-actions[bot]
d7453303b8
chore: sync AI model catalog from models.dev (#19782)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-17 08:32:09 +02:00
Paul Rastoin
75235f4621
Validate universalIdentifier uniqueness among application and its dependencies (#19767)
# Introduction
Gracefully validating that when creating an entity its
`universalIdentifier` is available within the all application metadata
maps context ( current app + twenty standard, currently the only managed
dependencies )
2026-04-16 16:59:12 +00:00
Paul Rastoin
bf410ae438
upgrade:status command (#19584)
## Introduction
Introducing a new command in order to determine the curent twenty
instance and workspaces status as it's not stored in database but a
derivation of each current curors


## `upgrade:status` all healthy
<img width="1376" height="1202" alt="image"
src="https://github.com/user-attachments/assets/e90d6987-07d2-4b6b-b573-105249aca325"
/>

## `upgrade:status` Nearly use cases
<img width="1442" height="1304" alt="image"
src="https://github.com/user-attachments/assets/c336cb9d-eb9d-4c7d-9392-ec1ef54a7326"
/>

## `upgrade:status --failed-only`
<img width="1442" height="940" alt="image"
src="https://github.com/user-attachments/assets/93a3dfdb-0d2f-4a01-b185-118e5cf0a078"
/>

## `upgrade:status -w aa8fdcb1-8ee1-4012-98af-44a97caa7411 -w
20202020-1c25-4d02-bf25-6aeccf7ea419 -w
20202020-1c25-4d02-bf25-6aeccf7ea412`
<img width="1486" height="928" alt="image"
src="https://github.com/user-attachments/assets/ec1b1abc-46e8-4e36-9799-ab3a4b85e410"
/>
2026-04-16 16:05:02 +00:00
Etienne
aecbc89a3f
Fix slow db query issue (#19770)
https://github.com/twentyhq/twenty/pull/19586#discussion_r3074136617
2026-04-16 15:37:58 +00:00
Thomas Trompette
446a3923f2
Add workspace id in job logs (#19764)
Cannot currently investigate spikes
2026-04-16 15:28:37 +00:00
Félix Malfait
4f4f723ed0
Fix MCP discovery: path-aware well-known URL and protocol version (#19766)
## Summary

Adding `https://api.twenty.com/mcp` as an MCP server in Claude fails
with `Couldn't reach the MCP server` before OAuth can start. Two
independent bugs cause this:

1. **Missing path-aware well-known route.** The latest MCP spec
instructs clients to probe `/.well-known/oauth-protected-resource/mcp`
before `/.well-known/oauth-protected-resource`. Only the root path was
registered, so the path-aware request fell through to
`ServeStaticModule` and returned the SPA's `index.html` with HTTP 200.
Strict clients (Claude.ai) tried to parse it as JSON and gave up. Fixed
by registering both paths on the same handler.
2. **Stale protocol version.** Server advertised `2024-11-05`, which
predates Streamable HTTP. We've implemented Streamable HTTP (SSE
response format was added in #19528), so bumped to `2025-06-18`.

Reproduction before the fix:

```
$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" https://api.twenty.com/.well-known/oauth-protected-resource/mcp
200 text/html; charset=UTF-8
```

After the fix this returns `application/json` with the RFC 9728 metadata
document.

Note: this is separate from #19755 (host-aware resource URL for
multi-host deployments).

## Test plan

- [x] `npx jest oauth-discovery.controller` — 2/2 tests pass, including
one asserting both routes are registered
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [ ] After deploy, `curl
https://api.twenty.com/.well-known/oauth-protected-resource/mcp` returns
JSON (not HTML)
- [ ] Adding `https://api.twenty.com/mcp` in Claude reaches the OAuth
authorization screen

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 17:32:45 +02:00
Charles Bochet
4103efcb84
fix: replace slow deep-equal with fastDeepEqual to resolve CPU bottleneck (#19771)
## Summary

- Replaced the `deep-equal` npm package with the existing
`fastDeepEqual` from `twenty-shared/utils` across 5 files in the server
and shared packages
- `deep-equal` was causing severe CPU overhead in the record update hot
path (`executeMany` → `formatTwentyOrmEventToDatabaseBatchEvent` →
`objectRecordChangedValues` → `deepEqual`, called **per field per
record**)
- `fastDeepEqual` is ~100x faster for plain JSON database records since
it skips unnecessary prototype chain inspection and edge-case handling
- Removed the now-unnecessary `LARGE_JSON_FIELDS` branching in
`objectRecordChangedValues` since all fields now use the fast
implementation
2026-04-16 17:23:50 +02:00
Félix Malfait
d3df58046c
chore(server): drop api-host branch in OAuth discovery (#19768)
## Summary

Follow-up to #19755. Simplifies `OAuthDiscoveryController` by dropping
the `authorization_endpoint → frontend base URL` branch that was there
to make `api.twenty.com/mcp` paste-able in MCP clients.

We've decided not to support pasting `api.twenty.com/mcp` — users can
paste `app.twenty.com/mcp`, `<workspace>.twenty.com/mcp`, or a custom
domain, all of which serve both frontend and API. On those hosts,
`authorization_endpoint` was already pointed at the same host as
`issuer`, which is what we want.

## Change

- Remove `isApiHost` helper and the `authorizeBase` branch — use
`issuer` for `authorization_endpoint`.
- Drop now-unused `TwentyConfigService` and `DomainServerConfigService`
injections.
- Drop duplicate `DomainServerConfigModule` import from
`application-oauth.module.ts` (the module is no longer needed).

Net diff: +1 / -22 across 2 files.

## Breaking change

MCP clients configured with `https://api.twenty.com/mcp` will stop
working. They should be reconfigured with the host matching the
workspace they're connecting to (`<workspace>.twenty.com/mcp`,
`app.twenty.com/mcp`, or a custom domain).

## Test plan

- [x] `yarn jest --testPathPatterns="mcp-auth.guard"` → 2/2 passing
(unchanged)
- [x] `tsc --noEmit` clean on modified files
- [ ] Manual verification on staging: `app.twenty.com/mcp` and
`<workspace>.twenty.com/mcp` OAuth flow still works end-to-end

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 17:01:55 +02:00
Félix Malfait
cb6953abe3
fix(server): make OAuth discovery and MCP auth metadata host-aware (#19755)
## Summary

OAuth discovery metadata (RFC 9728 protected-resource, RFC 8414
authorization-server) and the MCP `WWW-Authenticate` header were
hardcoded to `SERVER_URL`. This breaks MCP clients that paste any URL
other than `api.twenty.com/mcp` — the metadata declares `resource:
https://api.twenty.com/mcp`, which doesn't match the URL the client
connected to, so the client rejects it and the OAuth flow never starts.

Reproduced with Claude's MCP integration: pasting
`<workspace>.twenty.com/mcp`, `app.twenty.com/mcp`, or a custom domain
returned *"Couldn't reach the MCP server"* because discovery returned a
resource URL for a different host.

Related memory: MCP clients POST to the URL the user entered, not the
discovered resource URL — so every paste-able hostname has to advertise
`resource` for that same hostname.

## What the server now does

`WorkspaceDomainsService.getValidatedRequestBaseUrl(req)` resolves the
canonical base URL for the host the request came in on, validated
against the set of hosts we actually serve:

- `SERVER_URL` (e.g. `api.twenty.com`) — API host
- default base URL (e.g. `app.twenty.com`) — the `DEFAULT_SUBDOMAIN`
base
- `FRONTEND_URL` bare host
- any `<workspace>.twenty.com` subdomain (DB lookup)
- any workspace `customDomain` where `isCustomDomainEnabled = true`
- any registered `publicDomain`

An unrecognized / spoofed Host falls back to
`DomainServerConfigService.getBaseUrl()`. **We never reflect arbitrary
Host values into the response.**

Callers updated:

- `OAuthDiscoveryController.getProtectedResourceMetadata` — echoes the
validated host into `resource` and `authorization_servers`.
- `OAuthDiscoveryController.getAuthorizationServerMetadata` — uses the
validated host for `issuer` and `*_endpoint`, **except**
`authorization_endpoint`: when the request came in via `SERVER_URL`
(API-only, no `/authorize` route), we keep that one pointed at the
default frontend base URL.
- `McpAuthGuard` — sets `WWW-Authenticate: Bearer
resource_metadata=\"<validatedBase>/.well-known/oauth-protected-resource\"`
on 401s, so the MCP client's follow-up discovery fetch lands on the same
host it started on.

## Security

- Workspace identity is already bound to the JWT via per-workspace
signing secrets (`jwtWrapperService.generateAppSecret(tokenType,
workspaceId)`). Host-aware discovery does not weaken that.
- Custom domains are only accepted once `isCustomDomainEnabled = true`
(i.e. after DNS verification), so an attacker can't register a
custom-domain mapping on a workspace and have discovery reflect it
before it's been proven.
- Unknown / spoofed Hosts fall through to the default base URL.

## Drive-by

Fixed a duplicate `DomainServerConfigModule` import in
`application-oauth.module.ts` while adding `WorkspaceDomainsModule`.

## Companion infra change required for custom domains

Customer custom domains (`crm.acme.com/mcp`) also require an
ingress-level fix to exclude `/mcp`, `/oauth`, and `/.well-known` from
the `/s\$uri` rewrite applied when `X-Twenty-Public-Domain: true`.
Shipping that in a twenty-infra PR (will cross-link here).

## Test plan

- [x] 14 new tests in
`WorkspaceDomainsService.getValidatedRequestBaseUrl` covering: missing
Host, SERVER_URL, base URL, FRONTEND_URL, workspace subdomain, unknown
subdomain fallback, enabled custom domain, disabled custom domain,
public domain, completely unrecognized host, lowercase coercion,
malformed Host, single-workspace mode fallback, DB throwing → fallback
- [x] New `oauth-discovery.controller.spec.ts` covering both endpoints
across api / app / workspace-subdomain / custom-domain hosts, plus
`cli_client_id` propagation
- [x] Rewrote `mcp-auth.guard.spec.ts` to cover `WWW-Authenticate` for
all four host types (api, workspace subdomain, custom domain, spoofed
fallback)
- [x] `yarn jest
--testPathPatterns=\"workspace-domains.service|oauth-discovery.controller|mcp-auth.guard\"`
→ 41/41 passing
- [x] `tsc --noEmit` clean on all modified files
- [ ] Manual verification against staging: connect Claude to
`api.twenty.com/mcp`, `app.twenty.com/mcp`,
`<workspace>.twenty.com/mcp`, and a custom domain and confirm OAuth flow
completes on each

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 16:47:42 +02:00
github-actions[bot]
9bc803d0c7
i18n - translations (#19765)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-16 16:17:27 +02:00
Thomas Trompette
cd31d9e0de
Add test tab to tool step (#19760)
So we can use those as variables.
When the function input is updated, invalidate the input using an
effect.

<img width="770" height="635" alt="Capture d’écran 2026-04-16 à 14 45
41"
src="https://github.com/user-attachments/assets/cbf0b3e4-baad-424d-8f08-06eb1028abdb"
/>
2026-04-16 14:00:21 +00:00
github-actions[bot]
c0fef0be08
i18n - translations (#19762)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-16 15:14:16 +02:00
Abdul Rahman
270069c3e3
Add search to Fields dropdown (#19750)
Issue link:
https://discord.com/channels/1130383047699738754/1489198502998315100


https://github.com/user-attachments/assets/7d0a859e-c33e-4f9e-bdb3-5867fc0dd80f
2026-04-16 12:59:01 +00:00
Paul Rastoin
2413af0ba9
[run-instance-commands] Preserve fast slow sequentiality (#19757)
# Introduction
The command was wrongly running all fast and then all slow ignoring
instance commands segment
Leading to 
```ts
1.23.0_AddGlobalObjectContextToCommandMenuItemAvailabilityTypeFastInstanceCommand_1776090711153 executed successfully
[Nest] 32679  - 04/16/2026, 1:21:07 PM     LOG [InstanceCommandRunnerService] 1.23.0_DropWorkspaceVersionColumnFastInstanceCommand_1785000000000 executed successfully
[Nest] 32679  - 04/16/2026, 1:21:07 PM     LOG [InstanceCommandRunnerService] 1.22.0_BackfillWorkspaceIdOnIndirectEntitiesSlowInstanceCommand_1775758621018 executed successfully
[Nest] 32679  - 04/16/2026, 1:21:07 PM     LOG [RunInstanceCommandsCommand] Instance commands completed
```

No prod/self host impact as 1.23 hasn't been released yet
2026-04-16 12:14:47 +00:00
Raphaël Bosi
2b5b8a8b13
Link command menu items to specific page layout (#19706)
- Add a `pageLayoutId` foreign key to `CommandMenuItem`, allowing
command menu items to be scoped to a specific page layout instead of
being globally available
- Filter command menu items by the current page layout on the frontend.
Items with a `pageLayoutId` only appear when viewing that layout, while
items without one remain globally visible
- Create an effect to track the current page layout ID
- Include a seed example: a "Show Notification" command pinned to the
Star history standalone page layout

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-16 12:07:36 +00:00
github-actions[bot]
7af82fb6a4
i18n - translations (#19758)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-16 13:48:47 +02:00
Félix Malfait
9beb1ca326
Support Select All for workflow manual triggers (#19734)
## Summary
- Move record fetching and payload building from the enrichment hook
into `TriggerWorkflowVersionEngineCommand`, following the same
component-based pattern as `DeleteRecordsCommand` and
`RestoreRecordsCommand`
- The enrichment hook now only stores workflow metadata (`trigger`,
`availabilityType`, `availabilityObjectMetadataId`); the component uses
`useLazyFetchAllRecords` for exclusion mode (Select All) with full
pagination
- `buildTriggerWorkflowVersionPayloads` is now a pure function accepting
`selectedRecords: ObjectRecord[]` instead of reading from the Jotai
store

Fixes the issue introduced by #19718 which blocked Select All with a
warning toast instead of implementing it.

## Test plan
- [ ] Select individual records → run workflow trigger from command menu
→ works as before
- [ ] Click Select All → run workflow trigger from command menu →
fetches all matching records and runs the workflow
- [ ] Select All with some records deselected → correctly excludes those
records
- [ ] Global workflows (no object context) → run without payload as
before
- [ ] Bulk record triggers → payload wraps records in `{namePlural:
[records]}`

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 11:33:03 +00:00
Lazare Rossillon
d54092b0e2
fix: add missing LayoutRenderingProvider in SettingsApplicationCustomTab (#19679)
## Summary

- `SettingsApplicationCustomTab` renders `FrontComponentRenderer` which
calls `useFrontComponentExecutionContext` →
`useLayoutRenderingContext()`, but the settings page never provided a
`LayoutRenderingProvider`
- Every other render site (side panel, command menu, record pages) wraps
`FrontComponentRenderer` with this provider — it was just missed here
- Opening the "Custom" tab in Settings → Applications crashes with:
`LayoutRenderingContext Context not found`
- Fix: wrap with `LayoutRenderingProvider` using `DASHBOARD` layout type
and no target record, matching the pattern used in
`SidePanelFrontComponentPage`

## Test plan

- [ ] Open Settings → Applications → any app with a custom settings tab
- [ ] Click the "Custom" tab — should render the front component without
crashing

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-16 10:01:28 +00:00
github-actions[bot]
60701c2cf9
i18n - translations (#19754)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-16 12:03:48 +02:00
martmull
381f3ba7d9
Fix app design 1/2 (#19735)
comply with
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=96977-349627&m=dev

## After
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 40
37"
src="https://github.com/user-attachments/assets/6d80191a-79a9-4f0f-aa4f-0e447fff4f6d"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 40
22"
src="https://github.com/user-attachments/assets/4f763272-027e-4246-b455-7d46babf7d8c"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39
11"
src="https://github.com/user-attachments/assets/b9b35e18-8068-447e-821d-5ec28bb5bd16"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39
05"
src="https://github.com/user-attachments/assets/57d9318a-902f-4fd7-a2a3-5795ebe0b9dc"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39
02"
src="https://github.com/user-attachments/assets/78a33fa8-6bdd-484e-a82d-bd0f7592a623"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38
58"
src="https://github.com/user-attachments/assets/f7987aed-c6e1-4032-a611-86817655137d"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38
55"
src="https://github.com/user-attachments/assets/d1c451ab-1d2d-41e4-a059-cf4303ecabe7"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38
48"
src="https://github.com/user-attachments/assets/593cae36-2320-443f-a955-93b211a6ee3f"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 37
40"
src="https://github.com/user-attachments/assets/c9f602b1-8de3-4e82-a3a6-344594a0c153"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 37
34"
src="https://github.com/user-attachments/assets/b54ddddf-5dda-46c8-ace3-cffe6015825a"
/>

## before

<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42
18"
src="https://github.com/user-attachments/assets/c0976a0a-0124-48ec-8e7c-78627cea7063"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42
16"
src="https://github.com/user-attachments/assets/d2db926c-4040-411d-9091-8b60e7c519e6"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42
13"
src="https://github.com/user-attachments/assets/2d69f2ff-f26e-4249-91a3-2cf3d261e840"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42
07"
src="https://github.com/user-attachments/assets/1028aabc-77ac-4c51-a8c3-9a194faba87f"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42
01"
src="https://github.com/user-attachments/assets/1caa9f5e-3eaa-433c-9d3b-e0f094f16e8e"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41
56"
src="https://github.com/user-attachments/assets/f42b6976-3a8f-4591-9283-bda79bdb424b"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41
53"
src="https://github.com/user-attachments/assets/93d00df8-0091-4dfa-9ac0-f6f376be5962"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41
43"
src="https://github.com/user-attachments/assets/9deae7e5-39c1-4518-a463-6d79bc5bf132"
/>
<img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41
37"
src="https://github.com/user-attachments/assets/3e21b521-c47d-482c-ad41-66abfe973772"
/>
2026-04-16 09:47:44 +00:00
neo773
64470baa1e
fix compute folders to update util (#19749)
This regressed with the migration work

Type checker should've caught it, but didn't because of `Partial`
changed it to `Pick` instead to avoid future cases

/closes https://github.com/twentyhq/twenty/issues/19745
2026-04-16 09:09:15 +00:00
Paul Rastoin
f5e8c05267
Add logs before and after instance slow data migration (#19753)
As it can take sometime, would result in not seeing any logs until
2026-04-16 09:08:37 +00:00
Raphaël Bosi
48c540eb6f
Add event forwarding stories to the front component renderer (#19721)
Add Storybook stories and example components to test event forwarding
through the front component renderer: form events (text input, checkbox,
focus/blur, submit), keyboard events (key/code/modifiers), and host API
calls (navigate, snackbar, progress, close panel)
2026-04-16 08:55:49 +00:00
Paul Rastoin
5583254f58
Remove workspace-migrations deadcode (#19752)
Was a previous twenty version upgrade command util
2026-04-16 08:52:58 +00:00
Abdullah.
97832af778
[Website] Resolve animations breaking due to renderer context being lost. (#19747)
Resolves the following two issues. The reason we had these issues was
because the maxLimit of renderers was reached and the browser started
losing context before restoring it. Now, we make sure the context that
is lost belongs to visuals that are not currently in the viewport and
when those visuals come back into viewport, they're loaded again.

https://github.com/twentyhq/core-team-issues/issues/2372

https://github.com/twentyhq/core-team-issues/issues/2373
2026-04-16 08:50:56 +00:00
Abdul Rahman
7ce3e2b065
Fix spacing between workspace domain cards (#19742)
https://discord.com/channels/1130383047699738754/1492113564138344468
2026-04-16 08:43:54 +00:00
Abdul Rahman
a49d8386aa
Fix calendar event "Not shared" content alignment and background (#19743)
Issue link:
https://discord.com/channels/1130383047699738754/1491712714848866424

<img width="1287" height="419" alt="Screenshot 2026-04-16 at 9 50 34 AM"
src="https://github.com/user-attachments/assets/ba2143df-7957-4aee-8994-80c0392b6086"
/>
2026-04-16 08:38:09 +00:00
Charles Bochet
e420ee8746
Release v1.22.0 for twenty-sdk, twenty-client-sdk, and create-twenty-app (#19751)
## Summary
- Bump `twenty-sdk` from `1.22.0-canary.6` to `1.22.0`
- Bump `twenty-client-sdk` from `1.22.0-canary.6` to `1.22.0`
- Bump `create-twenty-app` from `1.22.0-canary.6` to `1.22.0`
2026-04-16 08:29:50 +00:00
Thomas Trompette
7adab8884b
Add isUnique update in query + invalidate cache on rollback (#19746)
Fix isUnique not sent in field update mutation: Added isUnique and
settings to the Pick type in useUpdateOneFieldMetadataItem, which
previously silently dropped these properties from the GraphQL payload.

Invalidate cache on failed migration rollback: Added invalidateCache()
call in the migration runner's catch block so that a failed migration
(e.g., unique index creation failing due to duplicate data) regenerates
the Redis hash, preventing stale metadata from persisting in frontend
localStorage indefinitely. Was
2026-04-16 07:44:59 +00:00
Thomas des Francs
6101a4f113
Update website hero to use shared local avatars and logos (#19736)
## Summary
- import shared company logos and people avatars into
`packages/twenty-website-new/public/images/shared`
- expand the shared asset registry with the new local logo and avatar
paths
- update the home hero data to use local shared assets for matched
people and company icons
- replace synthetic or mismatched hero avatars with better-fitting named
or anonymous local assets
- prefer local shared company logos in hero visual components before
falling back to remote icons

## Testing
- Not run (not requested)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-16 06:58:48 +00:00
Thomas des Francs
da8fe7f6f7
Homepage 3 cards finished (#19732)
& various fixes
2026-04-16 06:42:25 +00:00
github-actions[bot]
cddc47b61f
i18n - translations (#19731)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-15 19:08:24 +02:00
Charles Bochet
446c39d1c0
Fix silent failures in logic function route trigger execution (#19698)
## Summary

Route-triggered logic functions were returning empty 500 responses with
zero server-side logging when the Lambda build chain failed. This PR
makes those failures observable and returns meaningful HTTP responses to
API clients.

- **Observability** — Log errors (with stack traces) at each layer of
the execution chain: `LambdaDriver` (deps-layer fetch, SDK-layer fetch,
invocation), `LogicFunctionExecutorService`, and `RouteTriggerService`.
- **Typed exceptions** — Replace raw `throw error` sites with
`LogicFunctionException` carrying an appropriate code and
`userFriendlyMessage` (new codes: `LOGIC_FUNCTION_EXECUTION_FAILED`,
`LOGIC_FUNCTION_LAYER_BUILD_FAILED`).
- **Correct HTTP semantics** — `RouteTriggerService` maps inner
exception codes to the right `RouteTriggerExceptionCode` so
`LOGIC_FUNCTION_NOT_FOUND` returns 404 and `RATE_LIMIT_EXCEEDED` returns
429 (new code + filter case) instead of a generic 500.
- **User-facing messages** — Forward the inner
`CustomException.userFriendlyMessage` when wrapping into
`RouteTriggerException`, without leaking raw internal error text into
the public exception message.
- **Infra** — Bump Lambda ephemeral storage from 2048 to 4096 MB to
prevent `ENOSPC` errors during yarn install layer builds (root cause of
the original silent failures).
2026-04-15 16:49:43 +00:00
Abdul Rahman
5eda10760c
Fix navbar folder opening lag by deferring navigation until expand animation completes (#19686)
### Before


https://github.com/user-attachments/assets/7d57aa55-8d4c-43e1-8835-f40206e5e453



### After


https://github.com/user-attachments/assets/2457c8ee-fcb9-41ae-a8f7-08f8c7b4a233

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-04-15 16:31:33 +00:00
Aryan Ghugare
b9605a3003
Refactor SnackBar duration handling and progress bar visibility logic (#19712)
## Summary

Fixes error snackbars disappearing too quickly by **disabling
auto-dismiss for error variants by default**. Error snackbars now remain
visible until the user closes them.

Closes #19694

## What changed

- Error snackbars no longer default to a 6s timeout (they only
auto-dismiss if an explicit `duration` is provided).
- Non-error snackbars keep the existing default auto-dismiss behavior
(6s).
- Progress bar animation/visibility is tied to auto-dismiss (no progress
bar when there’s no duration).

**Files**
-
packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx

## How to test

1. Trigger a long error snackbar (example: spreadsheet/CSV import error
with a long message).
2. Confirm the snackbar **stays visible** until clicking **Close**.
3. Trigger a success/info snackbar and confirm it **still
auto-dismisses** after ~6s.

## Notes

- Call sites that explicitly pass `options.duration` for error snackbars
will continue to auto-dismiss (intentional).

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-15 16:22:28 +00:00
Weiko
56d6e13b5d
Fix fields widget flash during reset (#19726)
Summary

- Remove the evictViewMetadataForViewIds step from the page-layout reset
flow. It synchronously cleared viewFields/viewFieldGroups rows from the
metadata store, leaving a window where
useFieldsWidgetGroups saw a view with no fields and fell back to
buildDefaultFieldsWidgetGroups, briefly rendering a synthetic "General"
+ "Other" layout before the real reset defaults
arrived.
- invalidateMetadataStore() alone is sufficient: it marks the
collections stale and triggers MinimalMetadataLoadEffect to refetch,
which replaces current atomically. The UI now
transitions old-layout → new-default with no synthetic flash.
- Simplified refreshPageLayoutAfterReset to no longer take a
collectAffectedViewIds callback, and updated both tab/widget reset call
sites plus ObjectLayout.tsx accordingly.
- Deleted the now-unused evictViewMetadataForViewIds and
collectViewIdsFromWidgets utils.
2026-04-15 16:08:24 +00:00
github-actions[bot]
725171bfd3
i18n - translations (#19729)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-15 18:11:26 +02:00
Raphaël Bosi
3b85747d3f
Go back to the original command K button (#19727)
## Before


https://github.com/user-attachments/assets/868e5293-8843-44c8-8011-b7130e97fa95


## After


https://github.com/user-attachments/assets/89cf46cc-1fb1-4e78-bfef-53e1d04e8ebf
2026-04-15 15:53:26 +00:00
Paul Rastoin
a4cc7fb9c5
[Upgrade] Fix workspace creation cursor (#19701)
## Summary

### Problem

The upgrade migration system required new workspaces to always start
from a workspace command, which was too rigid. When the system was
mid-upgrade within an instance command (IC) segment, workspace creation
would fail or produce inconsistent state.

### Solution

#### Workspace-scoped instance command rows

Instance commands now write upgrade migration rows for **all
active/suspended workspaces** alongside the global row. This means every
workspace has a complete migration history, including instance command
records.

- `InstanceCommandRunnerService` reloads `activeOrSuspendedWorkspaceIds`
immediately before writing records (both success and failure paths) to
mitigate race conditions with concurrent workspace creation.
- `recordUpgradeMigration` in `UpgradeMigrationService` accepts a
discriminated union over `status`, handles `error: unknown` formatting
internally, and writes global + workspace rows in batch.

#### Flexible initial cursor for new workspaces

`getInitialCursorForNewWorkspace` now accepts the last **attempted**
(not just completed) instance command with its status:

- If the IC is `completed` and the next step is a workspace segment →
cursor is set to the last WC of that segment (existing behavior).
- If the IC is `failed` or not the last of its segment → cursor is set
to that IC itself, preserving its status.

This allows workspaces to be created at any point during the upgrade
lifecycle, including mid-IC-segment and after IC failure.

#### Relaxed workspace segment validation

`validateWorkspaceCursorsAreInWorkspaceSegment` accepts workspaces whose
cursor is:
1. Within the current workspace segment, OR
2. At the immediately preceding instance command with `completed` status
(handles the `-w` single-workspace upgrade scenario).

Workspaces with cursors in a previous segment, ahead of the current
segment, or at a preceding IC with `failed` status are rejected.

### Test plan
created empty workspaces to allow testing upgrade with several active
workspaces
2026-04-15 15:41:10 +00:00
Raphaël Bosi
0f8152f536
Fix command menu item edit record selection dropdown icons (#19725)
## Before
<img width="816" height="126" alt="CleanShot 2026-04-15 at 17 20 11@2x"
src="https://github.com/user-attachments/assets/647d6aa6-1000-41d9-9351-c1489bc095c6"
/>


## After
<img width="808" height="120" alt="CleanShot 2026-04-15 at 17 19 05@2x"
src="https://github.com/user-attachments/assets/e7685370-b6eb-4720-a5dd-401dfae8dc6b"
/>
2026-04-15 15:31:59 +00:00
Charles Bochet
a328c127f3
Fix standalone page migration failing on navigationMenuItem enum type change (#19724)
## Summary
- The `AddStandalonePageFastInstanceCommand` migration was failing with:
`default for column "type" cannot be cast automatically to type
core."navigationMenuItem_type_enum"`
- The migration was missing `DROP DEFAULT` / `SET DEFAULT` around the
`ALTER COLUMN TYPE` for `navigationMenuItem.type`. PostgreSQL cannot
automatically cast the existing default (`'VIEW'::old_enum`) to the new
enum type.
- The same 3-step pattern (DROP DEFAULT → ALTER TYPE → SET DEFAULT) was
already correctly applied for `pageLayout.type` in the same migration —
this fix brings `navigationMenuItem.type` in line.
2026-04-15 17:25:55 +02:00
Baptiste Devessier
8cb803cedf
Various bug fixes Record page layouts (#19719)
Fixes:

- Can't add multiple widgets in a row
- Ensure newly created is always focused
2026-04-15 15:12:57 +00:00
Weiko
5e83ad43de
Update backfill page layout command (#19687) 2026-04-15 14:55:42 +00:00
Raphaël Bosi
7ba5fe32f8
Add new html tags to the remote elements (#19723)
- Add 72 missing HTML and SVG elements to the remote-dom component
registry (48 HTML + 24 SVG), bringing the total from 47 to 119 supported
elements
- HTML additions include semantic inline text (b, i, u, s, mark, sub,
sup, kbd, etc.), description lists, ruby annotations, structural
elements (figure, details, dialog), and form utilities (fieldset,
progress, meter, optgroup)
- SVG additions include containers (svg, g, defs), shapes (path, circle,
rect, line, polygon), text (text, tspan), gradients (linearGradient,
radialGradient, stop), and utilities (clipPath, mask, foreignObject,
marker)
- Add htmlTag override to support SVG elements with camelCase names
(e.g. clipPath, foreignObject) while keeping custom element tags
lowercase per the Web Components spec
2026-04-15 14:53:23 +00:00
github-actions[bot]
7601dbc218
i18n - translations (#19722)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-15 16:12:03 +02:00
Thomas Trompette
8a968d1e31
Hide workflow manual trigger from command menu on "Select All" (#19718)
https://github.com/user-attachments/assets/e19c6d23-03c1-49c8-8b83-5c8f5f0f1135
2026-04-15 13:55:38 +00:00
github-actions[bot]
0c4a194c7a
i18n - translations (#19720)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-15 15:50:21 +02:00
Etienne
94b8e34362
Object view widget - Introduce new TABLE_WIDGET view type (#19545)
closes
https://discord.com/channels/1130383047699738754/1491549365263667230/1491804729397743666
2026-04-15 13:30:37 +00:00
Marie
2fccd194f3
[Billing for self host] End dummy enterprise key validity (#19560)
<img width="1504" height="755" alt="Screenshot 2026-04-10 at 16 40 07"
src="https://github.com/user-attachments/assets/68a12e40-a077-48df-9e18-885493520a32"
/>


Re-using hasValidEnterpriseKey to avoid breaking changes. 
This will be entirely removed in the next versions.
2026-04-15 13:26:38 +00:00
Abdul Rahman
762fb6fd64
Fix active navigation item disambiguation (#19664)
## Summary

- Introduce a `activeNavigationMenuItemState` Jotai atom (persisted via
localStorage) to disambiguate active navigation items when multiple
items share the same URL
- Add active item evaluation for record show pages with three scenarios:
1. Navigating from a nav item → parent stays active + dedicated RECORD
item also active
  2. Clicking a dedicated RECORD nav item → only that item active
3. Navigating via search/direct link → OBJECT nav item fallback, or
Opened section if none exists
- Extract shared active logic into `isNavigationMenuItemActive` utility
to eliminate duplication between orphan items and folder items
- Support multiple simultaneously active items within folders via
`Set<number>` instead of a single index

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-04-15 13:00:47 +00:00
Raphaël Bosi
7956ecc7b2
Invert pinned and dots icon buttons in command menu items edit mode (#19717)
## Before
<img width="804" height="158" alt="CleanShot 2026-04-15 at 14 22 22@2x"
src="https://github.com/user-attachments/assets/65c4d4f1-6f17-47e2-b964-f3b7132d2f18"
/>

## After
<img width="804" height="168" alt="CleanShot 2026-04-15 at 14 21 38@2x"
src="https://github.com/user-attachments/assets/2f1d4fff-5f07-4df0-833e-129ad85391a9"
/>
2026-04-15 12:38:52 +00:00
Weiko
e12b55a951
Fix duplicate Fields widget (#19696)
## Context
Tab duplication was broken after the view creation logic was deferred
and moved to the FE.

- Duplicating a tab or a FIELDS widget now produces a fully independent
copy: new view, new view field groups, new view fields — all with fresh
IDs — while preserving any unsaved edits
  from the source widget.
- Removed the backend auto-seed of default view fields / view field
groups in ViewService.createOne for FIELDS_WIDGET views. The frontend
always sends the complete layout via
upsertFieldsWidget, so the auto-seed was both redundant and the source
of potential bugs.
- Extracted a shared useDuplicateFieldsWidgetForPageLayout hook used by
both tab and widget duplication paths, plus a small
useCloneViewInMetadataStore helper that clones the FlatView
in the metadata store and returns the copied flat view fields/groups for
the caller.
2026-04-15 12:29:44 +00:00
Raphaël Bosi
2df32f7003
Hide fallback command menu items in edit mode (#19700)
Hide fallback command menu items in edit mode
2026-04-15 09:38:54 +00:00
Thomas Trompette
46ee72160d
Fix infinite recursion in iterator loop traversal when If/Else branch loops back to enclosing iterator (#19714)
Fix a stack overflow (Maximum call stack size exceeded) in
getAllStepIdsInLoop caused by an If/Else branch inside an iterator loop
pointing back to the enclosing iterator. The traversal incorrectly
treated the enclosing iterator as a nested iterator, calling
getAllStepIdsInLoop recursively with fresh visited sets, causing
infinite recursion.

Add the enclosing iterator's own ID to the skip condition in
traverseSteps so back-edges from If/Else branches are handled the same
way as back-edges from regular nextStepIds.

<img width="1054" height="723" alt="Capture d’écran 2026-04-15 à 11 00
42"
src="https://github.com/user-attachments/assets/aee1477b-5059-4552-809e-7c8a34a9ec4a"
/>
2026-04-15 09:23:25 +00:00
github-actions[bot]
f953aea3c5
i18n - translations (#19715)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-15 11:27:10 +02:00
Baptiste Devessier
c805a351ab
Reactivate disabled full tab widgets (#19702)
https://github.com/user-attachments/assets/14a48c7d-e731-4a15-883f-c53beb4943de
2026-04-15 09:11:55 +00:00
Thomas des Francs
3e48be4c31
Update home card visuals and partner marketing assets (#19711)
## Summary
- replace static home card imagery with code-driven visuals for the
familiar interface, fast path, and live data cards
- refine the live data card interaction details, including hover states,
animated cursors, filter chips, table styling, and inline tag editing
- add shared company logos, people avatars, and updated partner
testimonial assets to support the new marketing visuals
- refresh related home, partner, case study, signoff, testimonials, and
design-system content/components to align with the updated marketing
presentation

## Testing
- `npx tsc -p tsconfig.json --noEmit` in `packages/twenty-website-new`

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-15 06:35:46 +00:00
github-actions[bot]
a9ea1c6eed
i18n - docs translations (#19710)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 22:35:59 +02:00
github-actions[bot]
7a721ef5dd
i18n - docs translations (#19709)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 20:45:33 +02:00
github-actions[bot]
77e5b06a50
i18n - translations (#19707)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 19:00:22 +02:00
Marie
bc28e1557c
Introduce updateWorkspaceMemberSettings and clarify product (#19441)
## Summary

Introduces a dedicated **metadata** mutation to update **standard
(non-custom)** workspace member settings, moves profile-related UI to
use it, and aligns **workspace member** record permissions with the rest
of the CRM so users cannot escalate visibility via RLS by editing their
own member record.

## Product behaviour

### Profile and appearance (standard fields)

- Users can still update **their own** standard workspace member fields
that the product exposes in **Settings / Profile** (e.g. name, locale,
color scheme, avatar flow) via the new
**`updateWorkspaceMemberSettings`** mutation.
- The mutation returns a **boolean**; the app **merges** the updated
fields into local state so the UI stays in sync without refetching the
full workspace member record.
- **Locale** changes also keep **`userWorkspace`** in sync when a locale
is present in the payload (including from the workspace `updateOne` path
when applicable).

### Custom fields on workspace members

- The dedicated metadata mutation **rejects** any **custom** workspace
member field (and unknown keys). Those updates must go through the
normal **object** `updateOne` pipeline, which is subject to **object-
and field-level** permissions like other records. But since we don't
have object- and field-level permission configuration for system objects
yet, this permission is derived from Workspace member settings
permission.
- **Workspace member** is no longer exempt from ORM permission
validation for updates merely because it is a **system** object. Users
who **do not** have workspace member access (e.g. no **Workspace
members** settings permission and no equivalent broad settings access on
the role) **cannot** use `updateOne` on `workspaceMember` to change
**custom** (or other) fields on their own row—even though that row is
used for RLS predicates.
- This closes a path where someone could widen what they can see by
writing to fields that drive row-level rules.

### Who can change another member

- Updating **another** user’s workspace member still requires
**Workspace members** (or equivalent) settings permission, consistent
with admin tooling.
2026-04-14 16:29:00 +00:00
github-actions[bot]
42f452311b
i18n - docs translations (#19705)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 18:44:30 +02:00
github-actions[bot]
59a222e0f0
i18n - translations (#19703)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 18:38:55 +02:00
Félix Malfait
307b6c94de
Align GraphQL error handling for billing and AI chat (#19690)
## What changed

This refactor fixes AI chat error surfacing by aligning both the backend
and frontend with the existing GraphQL error architecture instead of
adding AI-local error translation.

On the backend:
- add a dedicated GraphQL billing exception path
- register billing GraphQL handling globally for GraphQL requests
- reuse the existing AI GraphQL interceptor path for agent/chat
exceptions
- keep billing status classification shared between REST and GraphQL
- remove the earlier attempt to preserve `CustomException` metadata in
the global GraphQL fallback

On the frontend:
- keep the original Apollo GraphQL error object in AI chat state
- reuse shared Apollo/GraphQL helpers for user-facing messages and
error-type checks
- delete AI-specific error extraction helpers that duplicated generic
GraphQL parsing
- replace a few direct `extensions.subCode` call sites with a shared
predicate

## Why it changed

The original bug was that `BillingException` and AI exceptions thrown
from chat were not being translated into GraphQL errors with the
expected `extensions.subCode` and `extensions.userFriendlyMessage`, so
the AI chat UI had nothing structured to inspect.

An intermediate fix worked mechanically but pushed `CustomException`
handling into the global GraphQL fallback, which blurred the intended
layering. This PR moves the behavior back to explicit GraphQL edges.

## Root cause

`AgentChatResolver` could throw `BillingException` and `AgentException`,
but:
- billing had a REST exception filter and no shared GraphQL equivalent
- AI chat was not consistently using the same GraphQL exception
translation path as the sibling AI resolver
- the frontend chat UI had drifted into AI-specific error parsing
instead of consuming the same structured Apollo errors as the rest of
the app

## Impact

- `BILLING_CREDITS_EXHAUSTED` is now preserved through GraphQL and can
render the existing credits-exhausted UI in chat
- `API_KEY_NOT_CONFIGURED` is preserved through the AI GraphQL path
- AI chat now follows the same general GraphQL error consumption pattern
as the rest of the frontend
- billing GraphQL handling is less dependent on individual resolver
authors remembering to add a filter

## Validation

- `yarn jest --config packages/twenty-server/jest.config.mjs
packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/billing-graphql-api-exception-handler.util.spec.ts
packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/__tests__/agent-graphql-api-exception-handler.util.spec.ts`
- `yarn jest --config packages/twenty-front/jest.config.mjs
packages/twenty-front/src/utils/__tests__/is-graphql-error-of-type.util.test.ts`
- `npx oxlint --type-aware ...` on touched backend/frontend files
- `npx prettier --check ...` on touched backend/frontend files

## Follow-up ideas

- consolidate frontend GraphQL error helpers further so more existing
direct `extensions.subCode` checks move to shared utilities
- consider whether common GraphQL exception filter registration should
live in a more explicit GraphQL-specific module instead of
`CoreEngineModule`
- add an end-to-end test for a real `sendChatMessage` GraphQL failure
path in AI chat

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 18:31:59 +02:00
martmull
3463ee5dc4
Switch default remote to lastly added remote (#19697)
as title
2026-04-14 16:03:05 +00:00
github-actions[bot]
9cf6f42313
i18n - translations (#19699)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 18:08:57 +02:00
Raphaël Bosi
a88d1f4442
Introduce standalone page (#19675)
Add support for standalone pages: a new `PageLayout` type
(`STANDALONE_PAGE`) that can be rendered independently at
`/page/:pageLayoutId`, not tied to any record or object context.

- New `STANDALONE_PAGE` page layout type
- New `PAGE_LAYOUT` navigation menu item type: adds a `pageLayoutId`
foreign key to `NavigationMenuItemEntity`, allowing sidebar items to
link directly to standalone pages
- New `GLOBAL_OBJECT_CONTEXT` command menu availability type: separates
object-context-dependent commands (Create Record, Import, Export, See
Deleted, Create View, Hide Deleted) from truly global ones, so
standalone pages only show relevant commands
- Frontend routing & rendering: adds a `/page/:pageLayoutId` route with
its own page component, header, and command menu
- Widget rendering refactor
- Instance commands: two fast 1.22 migrations: `pageLayoutId` column +
`STANDALONE_PAGE` enum, and `GLOBAL_OBJECT_CONTEXT` availability type
enum
- Workspace command: backfills existing command menu items from `GLOBAL`
to `GLOBAL_OBJECT_CONTEXT` where appropriate
- Dev seeds: adds a sample "Star History" standalone page with an iframe
widget for local development
2026-04-14 15:52:45 +00:00
martmull
43249d80e8
Add missing doc (#19693)
follow up of the @charlesBochet presentation
as title
2026-04-14 15:30:50 +00:00
github-actions[bot]
ed9dd3c275
i18n - translations (#19692)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 14:56:36 +02:00
martmull
b3354ab6e7
Fix multi-workspace-registration (#19685)
## Before

<img width="1512" height="915" alt="image"
src="https://github.com/user-attachments/assets/5cb05f76-b672-404e-b31d-ca455802f97a"
/>


## After

<img width="1512" height="726" alt="image"
src="https://github.com/user-attachments/assets/58229c4c-3ac6-4428-9c4d-3586a2b9ee36"
/>
2026-04-14 12:41:19 +00:00
Paul Rastoin
762de40c3d
Improve upgrade registry logging for pre-release bundles (#19689)
```ts
Registered 3 fast instance command(s), 0 slow instance command(s), and 14 workspace command(s) for 1.21.0
Registered 5 fast instance command(s), 1 slow instance command(s), and 3 workspace command(s) for 1.22.0
Registered 1 fast instance command(s), 0 slow instance command(s), and 1 workspace command(s) for 1.23.0 (pre-release)
```
2026-04-14 12:33:47 +00:00
github-actions[bot]
573ecea753
i18n - translations (#19691)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 14:39:07 +02:00
Thomas Trompette
c8a3de6c65
Test workflow with webhook expected body (#19688)
As title. Currently payload is always undefined when testing
2026-04-14 12:21:48 +00:00
Weiko
edba7fe085
Reset to default page layout (#19682)
- Add resetPageLayoutToDefault GraphQL mutation that resets an entire
page layout (all tabs, widgets, view field groups, and view fields) to
their default state in a single operation
- Add a "Reset to default" button in the settings Layout tab
(/settings/objects/:object#layout) with a confirmation modal


<img width="673" height="426" alt="Screenshot 2026-04-14 at 13 13 53"
src="https://github.com/user-attachments/assets/002d33c9-9ea1-49f2-bef6-179ce034c126"
/>
2026-04-14 12:19:48 +00:00
Paul Rastoin
ef328755bb
Bump current version to 1.23.0 (#19683) 2026-04-14 12:04:44 +00:00
Weiko
1e42be5a44
Expend field widget field supported types (#19684)
## Context
Expending field widget supported types to all field types. They will all
by default fallback to "FIELD" displaymode which is the inline display
mode (same as the one used in FIELDS widget) and can be extended to
other displayMode such as CARD or EDITOR in the future if needed. Most
of the work was already done in the previous PR and was unnecessary
filter out until this was properly tested

<img width="951" height="757" alt="Screenshot 2026-04-14 at 13 24 24"
src="https://github.com/user-attachments/assets/a08a1855-21ea-4e75-8032-4f970b3ff50d"
/>
<img width="915" height="595" alt="Screenshot 2026-04-14 at 13 23 34"
src="https://github.com/user-attachments/assets/8b420c1f-0496-4c1c-91b8-b266c40b3772"
/>
2026-04-14 11:51:01 +00:00
Jeevan Mohan Pawar
b194b67ac4
fix(address): populate street line from place details (#19326)
## Summary
- extract and expose `street` from Google place details (`street_number`
+ `route`) on the server DTO
- request and type `street` in front-end geo-map place details query
- use `placeData.street` as the preferred value for `addressStreet1` in
address autofill
- add regression coverage for query fields and street-line precedence
behavior

## Why
Address autocomplete selection currently writes full place text
(including city/state/postcode/country) into `addressStreet1`,
duplicating values already mapped to dedicated fields.

Fixes #18860

---------

Signed-off-by: jeevan6996 <jeevanpawar5890@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-14 11:44:36 +00:00
Thomas des Francs
eaf54ae02f
website new fixes (#19678) 2026-04-14 11:28:40 +00:00
martmull
fb4d037b93
Upgrade self hosting application (#19680)
as title, installed on
https://twentyfortwenty.twenty.com/objects/selfHostingUsers?viewId=20069db0-5137-4b2f-9b20-1797572b8eb8
2026-04-14 11:21:18 +00:00
github-actions[bot]
edc47bd458
i18n - docs translations (#19677)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 12:45:12 +02:00
Weiko
47bdcb11d8
Move is active to fe (#19649)
## Context
Moving isActive filtering to the frontend for page layout tabs and
widgets, hiding inactive entities from the UI while keeping them in
state for future reactivation

Next we will implement deactivated standard tab re-activation during tab
creation (cc @Devessier)
<img width="234" height="303" alt="📋 Menu (Slots)"
src="https://github.com/user-attachments/assets/17a25ac6-55e2-4778-b7f0-e7554ed69704"
/>
2026-04-14 10:18:09 +00:00
Baptiste Devessier
b817bdca02
Rpl various fixes (#19668) 2026-04-14 09:46:15 +00:00
Etienne
9c07ecd363
Fix view filter/sort deletion (#19567)
fixes https://github.com/twentyhq/twenty/issues/19543

+ bonus bug : when deleting an advanced filter, it triggers a destroy
which cascade-deletes associated view filters. Then, view filters
deletion throws.
2026-04-14 09:43:52 +00:00
Raphaël Bosi
eb13378760
Fix Quick Lead command menu item not appearing (#19635)
- Refactored prefillWorkflowCommandMenuItems and
prefillFrontComponentCommandMenuItems to use
validateBuildAndRunWorkspaceMigration instead of raw TypeORM
createQueryBuilder inserts
- This ensures the flat entity cache is properly updated when seeding
command menu items, fixing the Quick Lead item not appearing after
workspace creation
- Moved command menu item prefill calls outside the transaction since
they now go through the migration pipeline
2026-04-14 09:30:31 +00:00
Paul Rastoin
3e699c4458
Fix upgrade commands discovery outside of cli (#19671)
# Introduction
We were allowing the sequence to be empty in the worker context that was
facing an edge case importing the UpgradeModule through the
WorkspaceModule god module, no commands were discovered and it was
throwing as the sequence must have at least one workspace commands to
allow a workspace creation

Though the issue was also applicable to the twenty-server `AppModule`
too that was not discovering any commands

## Integration tests were passing
The integration test were importing the `CommandModule` at the nest
testing app creating leading to asymmetric testing context
It was a requirement for a legacy commands import and global assignation

## Fix
The `UpgradeModule` now import both `WorkspaceCommandsProviderModule`
and `InstanceCommandProviderModule` which ships the commands directly in
the module
We could consider moving the commands into the `engine/upgrade` folder

## Concern
Bootstrap could become more and more long to load at both server and
worker start
When this becomes a problem we will have to only import the latest
workspace command or whatever
For the moment this is not worth it the risk to import not the latest
workspace command
2026-04-14 09:20:33 +00:00
Etienne
f738961127
Add gql operationName metadata in sentry (#19564) 2026-04-14 08:55:54 +00:00
Amlan Kumar Nandy
49aac04b84
fix: edit button not coming up on avatar right after image upload (#19596)
## Summary

After uploading an image/file to the empty avatar field in the person
tab, the edit icon next to the field would not appear until the browser
was refreshed or another field was clicked.

### Root cause

- When user clicks over the avatar field,
`recordFieldListCellEditModePosition` is set to `globalIndex`
- That is fine when a avatar already exists. But when there is no avatar
already set, the native file picker is opened with no `onClose` handler
attached.
- So after the file upload is completed,
`recordFieldListCellEditModePosition` is never reset to null.
- `FieldsWidgetCellEditModePortal` stays anchored to the avatar file
element
- When the user hovers over the same field again, its hover portal tries
to compete to anchor for the same element
- So, `RecordInlineCellDisplayMode ` (the edit button) doesn't render

### Fix

- Pass the `onClose` function through `openFieldInput` to
`openFilesFieldInput`
- `onClose` resets `recordFieldListCellEditModePosition` back to null,
when the upload completes.

## Before


https://github.com/user-attachments/assets/ac9318e9-5471-434c-8af3-5c20d0112460

## After


https://github.com/user-attachments/assets/0d064a7f-95ad-4b92-a9ee-d9570f360972

Fixes #19595

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-14 08:54:14 +00:00
github-actions[bot]
40c6c63bf5
i18n - docs translations (#19672)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-14 10:54:27 +02:00
oniani1
d583984bf0
fix(api-key): batch role resolution with DataLoader to fix N+1 (#19590)
## Summary

The `role` @ResolveField on `ApiKeyResolver` calls `getRolesByApiKeys`
with a single-element array per API key. When a query returns N API
keys, this produces N separate DB queries to resolve their roles.

This adds an `apiKeyRoleLoader` to the existing DataLoader
infrastructure. All API key IDs in a single GraphQL request are
collected and resolved in one batched query.

- Before: N queries (one per API key)
- After: 1 query (batched via DataLoader)

## Changes

- `dataloader.service.ts` - new `createApiKeyRoleLoader` method,
delegates to `ApiKeyRoleService.getRolesByApiKeys`
- `dataloader.interface.ts` - `apiKeyRoleLoader` added to `IDataloaders`
- `dataloader.module.ts` - import `ApiKeyModule` so `ApiKeyRoleService`
is available
- `api-key.resolver.ts` - `role()` now uses
`context.loaders.apiKeyRoleLoader.load()` instead of calling the service
directly

## Test plan

- [ ] Verify `apiKeys { id role { label } }` query returns the same
results as before
- [ ] Confirm only 1 role_target query fires regardless of how many API
keys are returned

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-14 08:43:31 +00:00
Paul Rastoin
714f149b0c
Move backfill page layout to 1.23 (#19670) 2026-04-14 08:39:24 +00:00
martmull
194f0963dc
Remove 'twenty-app' keyword by default (#19669)
as title
2026-04-14 08:26:45 +00:00
srijita2506
5fa3094800
test: fix failing useColorScheme test and remove FIXME (#19593)
Summary
This PR fixes a bug in the `useColorScheme` test suite and removes a
lingering `FIXME` comment where the color scheme was unexpectedly
unsetting during state updates.

Root Cause
Previously, the Jotai state was being initialized *inside* the
`renderHook` callback using `useSetAtomState`. When the `setColorScheme`
function was called, it triggered a hook re-render, which caused the
callback to execute again and overwrite the new state with the hardcoded
`'System'` initial state.

The Fix
- Removed the state initialization from inside the render cycle.
- Bootstrapped the state on a fresh store using `resetJotaiStore()` and
`store.set()` *before* rendering the hook.
- Updated the mock `workspaceMember` to correctly use the
`CurrentWorkspaceMember` type.
- Removed the `FIXME` comment and successfully asserted that the color
scheme updates to `'Dark'`.

Testing
Ran tests locally to confirm the fix works as expected:
`corepack yarn jest --config packages/twenty-front/jest.config.mjs
--testPathPattern=useColorScheme.test.tsx`

---------

Co-authored-by: Srabani Ghorai <subhojit04ghorai@gmail.com>
2026-04-14 06:40:40 +00:00
Thomas des Francs
87f8e5ca19
few website updates (#19663)
## Summary
- refresh pricing page content, plan cards, CTA styling, and Salesforce
comparison visuals
- update partner and hero/testimonial visuals, including pulled
carousel-compatible partner testimonial data
- improve halftone export and illustration mounting flows, plus related
button and hydration fixes
- add updated website illustration and pricing assets

## Testing
- Not run (not requested)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-14 06:23:20 +00:00
Hussain Arslan
e041125426
fix: return 404 for deleted workspace webhook race (#19439)
Handle late TwentyORM workspace-not-found exceptions in the workflow
webhook REST exception filter so deleted workspaces return a 404 instead
of surfacing as internal errors.

Also add a focused regression spec covering the deleted-workspace ORM
codes and the existing workflow-trigger status mappings.

Closes #15544

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-13 22:38:36 +00:00
Thomas Trompette
d88fb2bd65
Clean event creation exception (#19561)
https://twenty-v7.sentry.io/issues/7351816489/?environment=prod&project=4507072499810304&query=is%3Aunresolved&referrer=issue-stream

Those are expected error that should not reach sentry. These happen when
stream TTL expires or user session ends

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-13 22:11:57 +00:00
Abdullah.
cdc7339da1
Fix testimonials background, faq clickability and some case-studies page edits. (#19657)
As title. Also copied release-notes related files from `twenty-website`
to `twenty-website-new`.
2026-04-13 21:19:41 +00:00
Félix Malfait
69d228d8a1
Deprecate IS_RECORD_TABLE_WIDGET_ENABLED feature flag (#19662)
## Summary
- Removes the `IS_RECORD_TABLE_WIDGET_ENABLED` feature flag, making the
record table widget unconditionally available in dashboard widget type
selection
- The flag was already seeded as `true` for all new workspaces and only
gated UI visibility in one component
(`SidePanelPageLayoutDashboardWidgetTypeSelect`)
- Cleans up the flag from `FeatureFlagKey` enum, dev seeder, and test
mocks

## Analysis
The flag only controlled whether the "View" (Record Table) widget option
appeared in the dashboard widget type selector. The entire record table
widget infrastructure (rendering, creation hooks, GraphQL types,
`RECORD_TABLE` enum in `WidgetType`) is independent of the flag and
fully implemented. No backend logic depends on this flag.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-13 21:13:15 +00:00
Félix Malfait
455022f652
Add ClickHouse-backed metered credit cap enforcement (#19586)
## Summary
Implements a ClickHouse-backed polling system to enforce metered-credit
caps for workflow executions, replacing reliance on Stripe billing
alerts. The system re-evaluates tier caps against live pricing on every
poll cycle, allowing price/tier changes to propagate immediately without
recreating Stripe alert objects.

## Key Changes

- **BillingUsageCapService**: New service that queries ClickHouse for
current-period credit usage and evaluates whether a subscription has
reached its metered-credit allowance (tier cap + credit balance)
  - `isClickHouseEnabled()`: Checks if ClickHouse is configured
- `getCurrentPeriodCreditsUsed()`: Sums creditsUsedMicro from usageEvent
table for a workspace within a billing period
- `evaluateCap()`: Determines if usage has reached the allowance by
reading live pricing from the subscription

- **EnforceUsageCapJob**: Cron job that polls all active subscriptions
and updates `hasReachedCurrentPeriodCap` on metered items
  - Runs every 2 minutes to keep cap enforcement in sync with live usage
- Supports shadow mode (log-only) via
`BILLING_USAGE_CAP_CLICKHOUSE_ENABLED` flag for safe rollout
- Continues processing after per-subscription errors with detailed
logging

- **EnforceUsageCapCronCommand**: CLI command to register the
enforcement cron job

- **MeteredCreditService**: Extracted
`extractMeteredPricingInfoFromSubscription()` as a pure function for
callers that already hold the subscription with pricing loaded, avoiding
redundant DB queries

- **Configuration**: Added `BILLING_USAGE_CAP_CLICKHOUSE_ENABLED` flag
to control enforcement mode (active vs. shadow)

- **Constants**: Added `METERED_OPERATION_TYPES` to define which
operation types count toward the metered product's credit cap

## Implementation Details

- The service queries ClickHouse for the sum of `creditsUsedMicro` in
the current billing period, matching Stripe meter semantics
- Pricing is re-read on every evaluation, so tier changes propagate
within one poll cycle without Stripe alert recreation
- The cron job only updates the database when the cap state actually
changes (no-op if already in the correct state)
- Shadow mode allows safe validation before enabling enforcement;
transitions are logged but not persisted
- Comprehensive test coverage for both the service and cron job,
including error handling and state transitions

https://claude.ai/code/session_01VksTSrYLXJVCPVBQhQdBTe

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-13 21:17:41 +02:00
Charles Bochet
fa354b4c1c
Remove orphaned workspaceId column from BillingSubscriptionItemEntity (#19660)
## Summary
- Removes the `workspaceId` column, `@Index()`, and `@ManyToOne`
workspace relation from `BillingSubscriptionItemEntity`
- The entity defined these fields but they don't exist in the actual
database table, causing `column X.workspaceId does not exist` errors at
runtime
- The workspace relationship is already accessible through the parent
`BillingSubscriptionEntity`
2026-04-13 17:53:33 +00:00
martmull
0894f2004b
Remove app record if first install fails (#19659)
If application install fails, and if app was created, uninstall app

fixes
https://discord.com/channels/1130383047699738754/1491822937462804590
2026-04-13 17:39:59 +00:00
github-actions[bot]
665db83bb5
i18n - translations (#19661)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-13 19:45:10 +02:00
martmull
64a7725ac7
Add banner for not vetted apps (#19655)
https://github.com/user-attachments/assets/4038e21a-d5d9-4b93-8589-7a4baf35ef5b

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-04-13 17:28:50 +00:00
github-actions[bot]
76d3e4ad2e
i18n - translations (#19656)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-13 18:30:08 +02:00
Raphaël Bosi
fb772c7695
Sync command menu with main context store (#19650)
## PR description

- The command menu in the side panel now reads directly from
`MAIN_CONTEXT_STORE_INSTANCE_ID` instead of snapshotting the main
context store into a separate side-panel instance when opening. This
keeps the command menu always in sync with the current page state
(selection, filters, view, etc.).
- Removed the broadening/reset-to-selection feature (Backspace to clear
context, "Reset to" button) since the command menu no longer maintains
its own copy of the context.

## Video QA


https://github.com/user-attachments/assets/5d5bc664-b6d4-431d-a271-6ce23d8a4ae0
2026-04-13 16:08:15 +00:00
Paul Rastoin
123d6241d7
Fix AddPermissionFlagRoleIdIndexFastInstanceCommand (#19654)
# Introduction
Index seemed to be missing in production only, as it's blocking the
whole migration release on other env that already implements the index
2026-04-13 18:14:31 +02:00
neo773
d2a99ef72d
Fix VariablePicker and Fullscreen Icon overlap in FormAdvancedTextFieldInput (#19614)
fixes 

<img width="659" height="386" alt="image"
src="https://github.com/user-attachments/assets/c9755574-6830-464d-8abf-7741188f84dd"
/>
2026-04-13 16:01:50 +00:00
Abdul Rahman
96959b43ba
Fix: Filter out deactivated objects from navigation sidebar (#19620) 2026-04-13 15:54:30 +00:00
Abdul Rahman
3f495124a5
Fix navbar folder not opening on page refresh when it has an active child item (#19619)
### Before


https://github.com/user-attachments/assets/b76e6184-0299-4240-a1f7-8651b69885ec

### After


https://github.com/user-attachments/assets/e7e9061b-98a1-4781-b882-eef87b83597a
2026-04-13 15:53:28 +00:00
Thomas Trompette
353d1e89d5
Fix merge with null value + reset data virtualization before init load (#19633)
**Merge records fix:**

selectPriorityFieldValue throws when merging records if the priority
record has no value for a field (e.g., null/empty) but 2+ other records
do. The recordsWithValues array is pre-filtered to only records with
non-empty values, so the priority record isn't in the list. The fix:
instead of throwing, fall back to null since this is the priority record
actual value


**Duplicated IDs fix**


https://github.com/user-attachments/assets/bd6d7d08-d079-49a5-aad4-740b59a3c246


When applying a filter that reduces the record count, the virtualized
table's record ID array keeps stale entries from the previous larger
result set. loadRecordsToVirtualRows clones the old array (e.g., 60
entries) and only overwrites the first N positions (e.g., 9) with the
new filtered results, leaving positions 9-59 with old IDs. If any old ID
matches a new one, it appears twice in the selection, causing "-> 2
selected" for a single click and a duplicate ID in the merge mutation
payload. The fix: clear the record IDs array in
useTriggerInitialRecordTableDataLoad before repopulating it with fresh
data.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-13 15:50:58 +00:00
Paul Rastoin
87f5c0083f
Prevent cross version upgrade mismatch in 1.22 (#19627)
## Introduction
As the new upgrade sequence engine is released in `1.22` it requires all
workspaces to be in `1.21.0` which mean they will have a cursor on the
sequence

As if if someone upgrades from `1.20` to `1.22` no `upgradeMigration`
will exist and throw a pretty basic `Could not find any cursor, database
might not been initialized correctly`

Here we allow a meaningful error
2026-04-13 14:53:12 +00:00
neo773
7dfc556250
refactor messaging jobs (#19626)
Cleans up the code quality by migrating from Raw SQL to TypeORM
entities. The previous implementation was necessary to do cross‑schema
table joins but since we've migrated to the core schema we don't need it
anymore.

- Also extracted `toIsoStringOrNull` to a utility it was duplicated
several times
- Moved `isThrottled` logic from job handler to cron enqueuer
2026-04-13 14:39:52 +00:00
github-actions[bot]
9f6855e7dd
i18n - translations (#19652)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-13 16:54:50 +02:00
github-actions[bot]
33c74c4d28
i18n - docs translations (#19651)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-13 16:48:44 +02:00
Paul Rastoin
ce2723d6cf
Move view field label identifier deletion validation into the cross entity validation (#19642)
## Introduction
In the same validate build and run we should be able to delete a view
field targetting a label identifier and at the same create one that
repoints to it again without failing any validation

Leading for this valdiation rule to be moved in the cross entity
validation steps
2026-04-13 14:38:27 +00:00
Thomas des Francs
12233e6c47
few fixes (#19648)
## Summary
- refresh the partner hero visual and testimonial presentation,
including the partner-specific carousel and illustration assets
- switch the testimonials top notch to the masked rendering approach
used elsewhere for more precise shape control
- extend halftone studio/export support and related geometry/state
handling used by the updated partner visuals
- include supporting website UI adjustments across navigation, pricing,
plans, and Salesforce-related sections

## Testing
- Not run (not requested)
2026-04-13 14:36:03 +00:00
Abdullah.
84b325876d
More website updates. (#19624)
This PR introduces more updates to the website, such as real
testimonials, case studies, copy of pricing plans table. It also adds
modals for "Talk to Us" and "Become a Partner".
2026-04-13 14:02:21 +00:00
neo773
88c0b24d9e
Add per-workspace error handling to CronTriggerCronJob (#19640)
fix sonarly 19618
2026-04-13 13:59:54 +00:00
Thomas des Francs
87bb2f94bb
Fixes on website (#19625)
## Summary
- fix the halftone studio image-switch behavior so image mode uses a
sane default preview distance instead of rendering nearly off-screen
- add shared preview-distance handling for shape and image modes, and
tune the default 3D idle auto-rotate speed
- update halftone controls/export plumbing to support the latest studio
settings changes
- refresh website UI/content in pricing, Salesforce, menu, and
billing-related sections

## Testing
- Ran targeted Jest tests for halftone state and footprint logic
- Ran TypeScript check for `packages/twenty-website-new`
- Broader app-level/manual testing not run
2026-04-13 13:55:38 +00:00
github-actions[bot]
c67602f2e8
i18n - translations (#19647)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-13 16:00:27 +02:00
github-actions[bot]
e34ca3817c
i18n - translations (#19646)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-13 15:53:02 +02:00
martmull
227d24512e
Fix permission flag deletion validator (#19636)
As title
2026-04-13 13:41:28 +00:00
Raphaël Bosi
1382790c80
Fix side panel close button title (#19638)
## Before
<img width="830" height="1486" alt="CleanShot 2026-04-13 at 15 07 19@2x"
src="https://github.com/user-attachments/assets/757a52bb-f06e-4f04-950a-087cbdec0653"
/>


## After
<img width="838" height="1478" alt="CleanShot 2026-04-13 at 15 06 56@2x"
src="https://github.com/user-attachments/assets/ed9d99ab-fbda-4548-b09a-e7825d86b5dd"
/>
2026-04-13 13:39:51 +00:00
Paul Rastoin
5e7e5dd466
Colliding subject field fix on messageThread command (#19637) 2026-04-13 13:33:46 +00:00
martmull
6182918a66
Document isAuthRequired: true instead of false (#19641) 2026-04-13 13:32:23 +00:00
Baptiste Devessier
6829fc315a
Implement full tab widget frontend (#19568)
https://github.com/user-attachments/assets/488e0145-ff49-4205-8fd3-0eb4f614c054
2026-04-13 13:24:20 +00:00
github-actions[bot]
8425afd930
i18n - translations (#19645)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-13 15:37:00 +02:00
github-actions[bot]
64e031b9dd
i18n - translations (#19643)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-13 15:30:32 +02:00
martmull
cd10f3cbd9
Disable permission tab when empty (#19630)
as title

fixes
https://discord.com/channels/1130383047699738754/1488475331072491590
2026-04-13 13:18:49 +00:00
martmull
bf5cc68f25
Rename standard and custom apps (#19631)
as title
no migration for existing apps, changes only apply on new workspaces
2026-04-13 13:13:59 +00:00
martmull
8bf9b12ace
Fix installed app setting tab (#19629)
## Before

<img width="773" height="523" alt="image"
src="https://github.com/user-attachments/assets/e62d39b6-a82a-4b87-a983-70d63842c739"
/>


## After

<img width="780" height="519" alt="image"
src="https://github.com/user-attachments/assets/cec80e28-1298-443f-8e12-5f3b4dd8bf06"
/>
2026-04-13 13:13:53 +00:00
martmull
2ac93bd803
Fix design (#19628)
## Before
<img width="614" height="348" alt="image"
src="https://github.com/user-attachments/assets/0a87b8cb-efe0-42ab-ad50-98a3635796f6"
/>

## After

<img width="622" height="411" alt="image"
src="https://github.com/user-attachments/assets/4f3ab135-dbe9-4384-97c2-4d20a13b3d9c"
/>
2026-04-13 13:13:45 +00:00
Charles Bochet
884b06936e
Switch app test infra to globalSetup with appDevOnce (#19623)
## Summary

- Replace per-file `setupFiles` + manual
`appBuild`/`appDeploy`/`appInstall` with vitest `globalSetup` that runs
`appDevOnce` once for the entire suite and `appUninstall` in teardown
- Add `fileParallelism: false` to prevent shared-state collisions
between test files
- Replace `app-install.integration-test.ts` with
`schema.integration-test.ts` that verifies app installation, custom
object schema (fields/relations), and CRUD
- Add reusable test helpers (`client.ts`, `metadata.ts`, `mutations.ts`)
- Applied to both `create-twenty-app` template and `postcard` example
2026-04-13 13:03:52 +00:00
Baptiste Devessier
63666547fb
Fix e2e (#19639) 2026-04-13 15:13:11 +02:00
Paul Rastoin
21142d98fe
Implement cross version upgrade (#19559)
# Introduction
Refactoring the upgrade engine to handle cross version upgrade,
completely getting rid of the semver `version` at db and runtime level
It remains a visual a listing indicator for or CD process but also
during devenv in order to prepare next release
Will write a release process runbook documentation on how to handle
upgrade step patch, command insertion etc as it needs to be cascaded
across all the involved supported version

**The upgrade sequence model:**

The sequence is a flat, ordered array of upgrade steps
(`UpgradeStep[]`), built from the registry by chaining all versions in
order, each version contributing its fast-instance → slow-instance →
workspace commands sorted by timestamp. Version is metadata for logging,
not used in the algorithm.

**Segments:**

The sequence naturally splits into alternating segments of contiguous
instance steps and contiguous workspace steps. The runner processes
segments in order:

- **Instance segment:** Run sequentially from the instance cursor. Each
step runs once globally.
- **Workspace segment:** Each workspace independently walks from its own
cursor through the end of the segment. Workspaces are independent within
a segment — they can be at different positions.
- **Synchronization (workspace → instance):** The runner blocks before
entering an instance segment. All active/suspended workspaces must have
completed the last workspace step of the preceding workspace segment. If
any workspace failed, abort. This is the only explicit synchronization
point.
- Instance → workspace ordering is implicit — the runner processes
segments sequentially, so the instance segment naturally completes
before the workspace segment begins.


full docs
https://gist.github.com/prastoin/e62106d455fd72d6b6ebada8351e5492

## Version constants & type-level deprecation

Version management is split into three atomic constants:
`TWENTY_PREVIOUS_VERSIONS`, `TWENTY_CURRENT_VERSION`, and
`TWENTY_NEXT_VERSIONS`. Two derived constants compose them:
`CROSS_UPGRADE_SUPPORTED_VERSIONS` (previous + current — what the engine
runs) and `ALL_TWENTY_VERSIONS` (the full ordered tuple including next).
The registry service validates at module init that no version is
duplicated across constants and that at least one previous version
exists.

A `DeprecatedSinceVersion<RemoveAtVersion, T>` type utility resolves to
`T` while `TWENTY_CURRENT_VERSION` is below `RemoveAtVersion`, and to
`never` once it reaches it — turning deprecation into a compile-time
guarantee via `IndexOf` and `IsGreaterOrEqual` generics in
`twenty-shared`.

### `workspace.version` column deprecation

The column is replaced by cursor-based state inference from
`UpgradeMigration` records, but cannot be dropped in 1.22: workspaces
activated during 1.21 predate the cursor system and need their initial
cursor backfilled first (`backfillWorkspaceCreatedIn1_21_0Cursors`).
This backfill itself depends on a new `isInitial` column on
`UpgradeMigration`, bootstrapped via a targeted TypeORM migration before
the upgrade sequence runs.

Both functions and the entity field are typed with
`DeprecatedSinceVersion<'1.23.0', ...>`. When `TWENTY_CURRENT_VERSION`
reaches `1.23.0`, compile errors force their removal — and the
pre-declared `DropWorkspaceVersionColumnFastInstanceCommand` takes over
to drop the column.

## What's next
- ci cross version upgrade ( wip )
- banner asking to contact twenty administrator if workspace is outdated
- upgrade healthcheck cli 

## New unit/integ test pattern
Create a dedicated `createNestApp` that consumes a real database in
order not to have to mack any database interaction to the
`upgradeMigrations` allowing full coverage of the whole
`upgradeRunnerService.run` core logic
2026-04-13 11:42:27 +02:00
Paul Rastoin
7091561489
Fix command FixMessageThreadViewAndLabelIdentifierCommand (#19622)
## Introduction
View standard backfill seed has been introduced during a partial patch,
a window exists where some workspaces got created without the view and
wasn't included in the upgrade process

The fix isn't covering all the view that has been missed during that
time, only the one required for the command to pass
2026-04-13 11:19:57 +02:00
github-actions[bot]
de401d96fe
i18n - translations (#19621)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-13 09:54:25 +02:00
Félix Malfait
09806d7d8c
Add admin panel workspace detail page with chat viewer (#19579)
## Overview
Adds comprehensive admin panel functionality for viewing workspace
details and AI chat threads.

## Changes

### Frontend
- **New Routes**: Added `AdminPanelWorkspaceDetail` and
`AdminPanelWorkspaceChatThread` pages with lazy loading
- **New Queries**: 
  - `getAdminWorkspaceChatThreads` - fetch chat threads for a workspace
  - `getAdminChatThreadMessages` - fetch messages for a specific thread
  - `workspaceLookupAdminPanel` - lookup workspace info and users
- **New Components**:
- `SettingsAdminWorkspaceDetail` - displays workspace info and chat
sessions tabs
- `SettingsAdminWorkspaceChatThread` - renders chat conversation with
message bubbles
- **Navigation**: Updated AI admin panel to link to workspace detail
pages
- **Settings Paths**: Added `AdminPanelWorkspaceDetail` and
`AdminPanelWorkspaceChatThread` paths

### Backend
- **New DTOs**:
  - `AdminWorkspaceChatThreadDTO` - workspace chat thread data
  - `AdminChatThreadMessagesDTO` - thread with messages
  - `AdminChatMessageDTO` - individual message with parts
- **New Resolvers**: Added three queries to `AdminPanelResolver`
- **New Service Methods**:
  - `workspaceLookup()` - fetch workspace info
  - `getWorkspaceChatThreads()` - list chat threads
  - `getChatThreadMessages()` - fetch thread messages with validation
- **Module Updates**: Added entity imports for workspace, user, AI chat,
and feature flag data

### Security
- Added `allowImpersonation` check before accessing chat data
- Validates workspace ownership and access permissions

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:47:34 +02:00
Sanskar Shukla
150c326369
fix: prevent image upload panel from being clipped in side panel (#19613)
fixes: #19571

## What's happening
BlockNote's `.bn-panel` (the Upload/Embed popup for images, videos,
audio) has a hardcoded `width: 500px` in the upstream
`blocknoteStyles.css`. When you open it inside the side panel (~350px
wide), it overflows past the container's `overflow: hidden` and gets
clipped.
## Why percentage-based fixes don't work here
I initially tried `max-width: 100%` on `.bn-panel` but it doesn't work
the panel sits inside a Floating UI popover that's absolutely positioned
with no explicit width. Its width comes from its content (the 500px
panel), so `max-width: 100%` just resolves to that same 500px. It's a
circular reference.
## The fix
```css
& .bn-mantine {
  container-type: inline-size;
}
& .bn-mantine .bn-panel {
  width: min(500px, 100cqi);
  box-sizing: border-box;
}
```
container-type: inline-size on .bn-mantine lets us reference the
editor's actual width using cqi units. min(500px, 100cqi) keeps the
panel at 500px in wide containers (main note view) and shrinks it to fit
in narrow ones (side panel).

I scoped container-type to .bn-mantine (BlockNote's own root wrapper)
rather than the outer StyledEditor div, so the containment context stays
within BlockNote's own tree.

#Before
<img width="373" height="530" alt="image"
src="https://github.com/user-attachments/assets/63fb407c-79d1-4faa-afb4-15a98fe4ba22"
/>

#After
<img width="332" height="597" alt="image"
src="https://github.com/user-attachments/assets/f7e91c28-ad35-44a5-9450-0b6d52714977"
/>
<img width="599" height="579" alt="image"
src="https://github.com/user-attachments/assets/20fd457b-2d06-4799-9164-f993ef37d833"
/>


Tested
Side panel: panel fits correctly, matches "Add image" button width 
Full-width editor: panel stays at 500px 
Slash menu, formatting toolbar, drag handles: all unaffected

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-04-13 09:08:28 +02:00
BugIsGod
c73cdd0844
fix: prevent image upload panel from being clipped in side panel (#19572)
fixes:   #19571 
## Summary:
- BlockNote's file upload/embed panel (.bn-panel) has a hardcoded width:
500px that overflows the side panel's overflow: hidden containers,
causing the Upload/Embed UI to be cut off
<img width="636" height="364" alt="image"
src="https://github.com/user-attachments/assets/4ec501c6-409c-4a86-a5b3-2c5f104c94f1"
/>

## Before
<img width="464" height="660" alt="image"
src="https://github.com/user-attachments/assets/3364e20e-6194-4c5d-b16f-9541cc11bfd7"
/>

## After
**Same width with "Add Image" button** ( same in the video,audio upload
<img width="444" height="665" alt="image"
src="https://github.com/user-attachments/assets/369a6974-a986-44ab-a38a-a717a8f978c0"
/>
2026-04-13 09:01:07 +02:00
github-actions[bot]
4e46aa32bb
i18n - translations (#19612)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-12 20:33:07 +02:00
Weiko
9e385e44f9
Fields widget draft view (#19562)
- Defer backend view creation for FIELDS widgets to prevent orphan views
when users cancel or leave before saving. Views are now created
optimistically in Jotai state (client-side UUID) and only persisted to
the database when the page layout is saved.
- Align the frontend's default field groups fallback with the backend
logic: split into General/Other groups, hide relation fields by default,
and filter invisible fields consistently across all code paths.
2026-04-12 20:26:47 +02:00
Charles Bochet
63806b24fe
Export field settings types from SDK public API (#19611)
## Summary

- Follows up on #19610 which exported view/page-layout types but missed
field settings types
- Adds re-exports for `DateDisplayFormat`, `NumberDataType`, and
`FieldMetadataSettingsOnClickAction` from the SDK public API
- These enums are referenced in the return type of `defineObject` (via
field settings types like `FieldMetadataSettingsDate`,
`FieldMetadataSettingsNumber`, `FieldMetadataSettingsMultiItem`) but
were not publicly exported
- This causes TS4082 ("private name") errors for SDK consumers that have
`declaration: true` in their tsconfig, specifically when using
`defineObject` with fields that have settings (e.g. SELECT, DATE, NUMBER
fields)
2026-04-12 19:35:37 +02:00
Charles Bochet
7540bb064f
Re-export missing types from SDK public API (#19610)
## Summary

- Fixes TS4082 errors for SDK consumers with `declaration: true` in
their tsconfig
- The `define*` functions (e.g. `defineView`, `definePageLayout`,
`defineLogicFunction`) return `ValidationResult<T>` where `T` references
types from `twenty-shared` that were not re-exported from the SDK's
public barrel
- TypeScript cannot generate `.d.ts` files when the return type
references "private names" — types that exist in the package but aren't
publicly exported

### Added re-exports

**Enums** (used in `ViewManifest`, `HttpRouteTriggerSettings`):
- `ViewType`, `ViewFilterOperand`, `ViewFilterGroupLogicalOperator`,
`ViewOpenRecordIn`, `ViewVisibility`
- `HTTPMethod`

**Types** (used in `PageLayoutWidgetManifest`, `LogicFunctionConfig`):
- `GridPosition`, `PageLayoutWidgetConditionalDisplay`
- `InputJsonSchema`
2026-04-12 18:33:14 +02:00
Charles Bochet
ad1a4ecca0
Add isUnique support for application-defined fields (#19609)
## Summary

- Adds `isUnique?: boolean` to `RegularFieldManifest` in
`twenty-shared`, allowing SDK applications to declare unique constraints
on fields
- Updates the manifest-to-flat-field converter to read `isUnique` from
the manifest instead of hardcoding `false`
- Generates corresponding unique index metadata in
`computeApplicationManifestAllUniversalFlatEntityMaps` when a field has
`isUnique: true`, matching the behavior of the `CreateFieldInput` path
- Adds SDK-side validation rejecting `isUnique` on RELATION,
MORPH_RELATION, and FILES field types
- Adds integration test verifying manifest sync creates a unique index
for `isUnique` fields
- Adds SDK unit tests for `isUnique` validation on unsupported field
types

## Test plan

- [x] SDK unit tests: `defineField` accepts `isUnique: true` on TEXT,
rejects on RELATION and FILES
- [ ] Integration test: manifest sync with `isUnique: true` creates the
unique index in DB
- [ ] Verify `isUnique` defaults to `false` when not specified (backward
compatible)
- [ ] Verify standalone manifest fields (not nested in objects) also
generate unique indexes correctly
2026-04-12 17:03:09 +02:00
Thomas des Francs
d9d3648baa
Halftone studio v3 (glass effect) (#19598)
## Summary
- refine `/halftone` controls for material selection and slower 3D
rotation tuning
- switch the default halftone material to solid for new sessions
- include related halftone rendering, export, and glass material updates
across the website app

## Testing
- `yarn nx run twenty-website-new:typecheck`
- `yarn prettier
packages/twenty-website-new/src/app/halftone/_components/controls/AnimationsTab.tsx
--check`
- `yarn prettier
packages/twenty-website-new/src/app/halftone/_components/controls/controls-ui.tsx
packages/twenty-website-new/src/app/halftone/_components/controls/DesignTab.tsx
--check`
- `yarn prettier
packages/twenty-website-new/src/app/halftone/_lib/state.ts --check`
2026-04-12 14:40:52 +00:00
Thomas des Francs
fda5aba9ec
small fixes on pricing (#19603)
## Summary
- update pricing card styling to use a 4px border radius
- make pricing card CTAs fill the available card width
- reduce spacing below the pricing cards section
- fix pricing copy casing and punctuation, including the tailor-made
plan banner
- make engagement band headings support a configurable size and default
them to the smaller variant

## Testing
- Not run (not requested)
2026-04-12 14:39:34 +00:00
Charles Bochet
4264741281
Clear stale SDK config on uninstall and invalid client (#19608)
## Summary
- After a successful `uninstall`, clear all app registration config
(`appRegistrationId`, `appRegistrationClientId`, `appAccessToken`,
`appRefreshToken`) so the next `dev` run doesn't hit "app not found"
errors from leftover credentials
- On app re-registration (`ensureAppRegistration`), clear stale
`appAccessToken`/`appRefreshToken` that belonged to the previous
registration
- On OAuth token refresh failure, only clear config when the server
returns `invalid_client` (registration was deleted), not on transient
failures like network issues
- Extract a shared `parse-server-error` utility for consistently parsing
both GraphQL error codes (`extensions.code`/`subCode`) and OAuth error
responses (`error`/`error_description`)
2026-04-12 16:29:29 +02:00
Charles Bochet
6e259d3ded
Inline twenty-shared types in SDK declarations (#19605)
## Summary
- `twenty-shared` is private and never published to npm, so SDK
consumers couldn't resolve type imports like `from
'twenty-shared/types'` in the generated `.d.ts` files
- Replace `vite.config.sdk.ts` with `rollup-plugin-dts` which compiles
`src/sdk/index.ts` directly into a self-contained `dist/sdk/index.d.ts`
with all `twenty-shared` types inlined
- The JS output from `vite.config.sdk.ts` (`dist/sdk/*.js`) was unused —
the main export already maps to `dist/index.mjs` from the node Vite
config

## Changes
- **Deleted** `vite.config.sdk.ts` — its preserved-module JS output
wasn't referenced by any `package.json` export
- **Added** `rollup.config.dts.mjs` — uses `rollup-plugin-dts` to
compile SDK types from source with `twenty-shared` inlined (~850ms)
- **Updated** `project.json` — build/dev/build:sdk targets now use
rollup instead of the removed vite config
- **Updated** `tsconfig.json` — removed `vite.config.sdk.ts` from
include

## Test plan
- [ ] Run `npx nx build twenty-sdk` and verify `dist/sdk/index.d.ts`
contains no `twenty-shared` references
- [ ] Verify `dist/index.mjs` and `dist/index.cjs` are still produced
correctly
- [ ] Verify CLI (`dist/cli.cjs`) still works
- [ ] Verify `npx nx build:sdk twenty-sdk` works standalone


Made with [Cursor](https://cursor.com)
2026-04-12 15:45:31 +02:00
Charles Bochet
3ae63f0574
Fix syncApplication failing when navigation menu item child is listed before folder in manifest (#19599)
## Summary

- Fix `syncApplication` crashing with `ENTITY_NOT_FOUND` when a
navigation menu item child (with `folderUniversalIdentifier`) appears
before its folder in the manifest's `navigationMenuItems` array
- Add a generic topological sort in
`WorkspaceEntityMigrationBuilderService.validateAndBuild()` that detects
self-referential FKs from `ALL_MANY_TO_ONE_METADATA_RELATIONS` and
ensures parents are created before children
- This also covers other self-referential entities:
`viewFilterGroup.parentViewFilterGroup`,
`fieldMetadata.relationTargetFieldMetadata`, and
`rowLevelPermissionPredicateGroup.parentRowLevelPermissionPredicateGroup`

## Root cause

The builder iterated `createdFlatEntityMaps.byUniversalIdentifier` in
insertion order (from the manifest). Validation passed because it
checked both optimistic maps and remaining-to-create maps. But the
runner processed create actions sequentially, so
`resolveUniversalRelationIdentifiersToIds` threw when the folder hadn't
been created yet.
2026-04-12 12:35:38 +02:00
Charles Bochet
c8f5ecb2b6
Add APPLICATION_LOG_DRIVER=CONSOLE to twenty-app-dev container (#19600)
## Summary
- Enable application log console output in the `twenty-app-dev` Docker
image by adding `APPLICATION_LOG_DRIVER=CONSOLE` to the Dockerfile ENV
block
- Rename `APPLICATION_LOG_DRIVER_TYPE` to `APPLICATION_LOG_DRIVER` and
`ApplicationLogDriverType` to `ApplicationLogDriver` for consistency
with all other driver config variables (`EMAIL_DRIVER`, `LOGGER_DRIVER`,
`CAPTCHA_DRIVER`, etc.)
- Add `APPLICATION_LOG_DRIVER` to `.env.example`
2026-04-12 11:55:38 +02:00
Félix Malfait
55b1624210
Convert AI chat state atoms to component family states (#19585)
## Summary
Refactored AI chat state management to use component family states
instead of global atoms. This change enables proper state isolation per
chat thread, preventing state leakage between different chat instances.

## Key Changes

- **Converted global atoms to component family states:**
  - `agentChatErrorState` → `agentChatErrorComponentFamilyState`
- `agentChatIsStreamingState` →
`agentChatIsStreamingComponentFamilyState`
- `agentChatFirstLiveSeqState` →
`agentChatFirstLiveSeqComponentFamilyState`
- `agentChatHandleEventCallbackState` →
`agentChatHandleEventCallbackComponentFamilyState`
  - `agentChatUsageState` → `agentChatUsageComponentFamilyState`
- `currentAIChatThreadTitleState` →
`currentAIChatThreadTitleComponentFamilyState`

- **Updated state access patterns:**
- Introduced `useAtomComponentFamilyStateCallbackState` hook to get
family state callbacks
- Changed from direct atom access to family key-based access using `{
threadId }` as the family key
- Updated all components and hooks to use the new family state patterns

- **Removed instance ID dependency:**
  - Eliminated `AGENT_CHAT_INSTANCE_ID` constant usage
- Simplified state access by using thread ID directly as the family key

- **Updated affected components and hooks:**
- `useAgentChatSubscription`: Now creates family state atoms per thread
- `AgentChatMessagesFetchEffect`: Uses family state callbacks for event
handling
- `AgentChatThreadInitializationEffect`: Sets family state values per
thread
  - `useAIChatThreadClick`: Updates family states when switching threads
- `AIChatErrorUnderMessageList`, `AIChatLastMessageWithStreamingState`,
`AIChatEmptyState`, `AIChatStandaloneError`, `SendMessageButton`,
`AIChatContextUsageButton`, `SidePanelAskAIInfo`: Updated to use family
state values with thread ID

## Implementation Details

- All new component family states use
`AgentChatComponentInstanceContext` for proper component-level isolation
- Family key structure: `{ threadId: string | null }`
- Removed `disposed` flag logic as it's no longer needed with proper
state isolation
- Updated dependency arrays in hooks to include family callback
functions

https://claude.ai/code/session_01D8syiJtCXu5bEHSr5KYkCt

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 07:39:12 +02:00
github-actions[bot]
c268a50e4e
i18n - translations (#19591)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-11 22:12:58 +02:00
Félix Malfait
4c57352450
Improve sensitive config variable masking and editing UX (#19578)
## Summary
This PR enhances the handling of sensitive configuration variables by
improving masking logic and user experience when editing them. It adds
metadata-aware masking for dynamically marked sensitive variables and
clears sensitive values when entering edit mode.

## Key Changes

- **Backend masking improvements**: Updated `maskSensitiveValue()` to
accept metadata parameter, enabling masking of variables marked as
sensitive via metadata (not just predefined masking config). Sensitive
non-string values are masked as `********`.

- **Edit mode UX**: When editing a sensitive variable, the value field
is now cleared on entering edit mode to prevent exposing masked values
and ensure users intentionally provide new secret values.

- **Form state tracking**: Enhanced `useConfigVariableForm()` hook to
accept an `isEditing` parameter and properly track value changes for
sensitive variables during edit operations.

- **Input placeholder**: Updated placeholder text for sensitive variable
inputs to show `Enter a new secret value` instead of the generic
database storage message, providing clearer intent to users.

## Implementation Details

- The `maskSensitiveValue()` method now checks both predefined masking
configurations and runtime metadata to determine if a value should be
masked
- Sensitive string values use LAST_N_CHARS strategy (4 characters),
while non-string values are masked uniformly
- The form's `hasValueChanged` flag is set to true when editing
sensitive variables to ensure proper validation and submission handling

https://claude.ai/code/session_01JiTckmuVJMQWpJ7TUGwsqb

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-11 22:07:05 +02:00
Charles Bochet
b37ef3e7da
Add app-path input to deploy and install composite actions (#19589)
Supports monorepo layouts where the app isn't at the repo root. Defaults
to '.' for backward compatibility.

Made-with: Cursor
2026-04-11 17:45:42 +02:00
Charles Bochet
4d877d072d
Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 1.22.0-canary.3 (#19587)
## Summary
- Bump `twenty-sdk`, `twenty-client-sdk`, and `create-twenty-app` from
`1.22.0-canary.2` to `1.22.0-canary.3` for publishing.

## Test plan
- Version-only change, no code modifications.

Made with [Cursor](https://cursor.com)
2026-04-11 16:04:46 +02:00
Charles Bochet
baf2fc4cc9
Fix sdk-e2e-test: ensure DB is ready before server starts (#19583)
## Summary

- The `sdk-e2e-test` CI job has been failing since at least April 9th
because the nx dependency graph runs `database:reset` and
`start:ci-if-needed` in **parallel** — neither depends on the other. The
server starts before the DB tables are created, crashes with `relation
"core.keyValuePair" does not exist`, and `wait-on` times out after 10
minutes.
- Replace the `nx-affected` orchestration for e2e with explicit
**sequential** CI steps: build → create DB → reset DB → start server →
wait for health → run tests.
- Add server log dump on failure for easier future debugging.

## Root cause

In `packages/twenty-sdk/project.json`, the `test:e2e` target has:

```json
"dependsOn": [
  "build",
  { "target": "database:reset", "projects": "twenty-server" },
  { "target": "start:ci-if-needed", "projects": "twenty-server" }
]
```

Since `database:reset` and `start:ci-if-needed` don't depend on each
other, nx can (and does) run them concurrently. `start:ci-if-needed`
fires `nohup nest start &` and immediately completes. The server process
tries to query `core.keyValuePair` before `database:reset` creates it →
crash → wait-on timeout → job failure.
2026-04-11 16:02:12 +02:00
Félix Malfait
3b55026452
Improve NullCheckEnum filter descriptions with usage examples (#19581)
## Summary
Enhanced the documentation and user guidance for null/empty value
filters across all field types by updating the `NullCheckEnum`
descriptions in the field filter Zod schemas. The descriptions now
provide clear, actionable guidance on how to use the "NULL" and
"NOT_NULL" filter options.

## Changes
- Updated 20+ field type filter descriptions to replace generic "Is null
or not null" text with more descriptive guidance
- New descriptions follow the pattern: "Check for missing or empty
[field type]. Use "NULL" to find records with no [value], "NOT_NULL" for
records with a [value]"
- Applied consistent messaging across all field types including:
  - Basic types: UUID, text, number, boolean, date
  - Complex types: select, multi-select, rating
- Composite types: amount, currency, first name, last name, street,
city, country, email, phone, link URL
  - Relationship and JSON fields
- Specialized descriptions for composite fields (e.g., "Check for
missing or empty amount" for currency amount field)

## Benefits
- Improved API documentation clarity for developers using field filters
- Reduced ambiguity about NULL vs NOT_NULL filter behavior
- Better discoverability of filtering capabilities through schema
descriptions
- Consistent messaging across all field types for improved user
experience

https://claude.ai/code/session_0139Nb5bZykacNE69GQsFSrr

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-11 14:50:27 +02:00
Charles Bochet
53065f241f
Exchange clientSecret for tokens after app registration + bump canary (#19582)
## Summary

- **Fix `createApplicationRegistration` flow**: The server's
`createApplicationRegistration` mutation returns a `clientSecret`, not
`accessToken`/`refreshToken` directly. The SDK now correctly requests
`clientSecret` and immediately performs an OAuth `client_credentials`
exchange to obtain `appAccessToken` and `appRefreshToken`, then stores
them in config.
- **New `exchangeCredentialsForTokens` helper**: Shared by both `dev`
and `dev --once` flows. Takes `clientId` + `clientSecret`, calls
`/oauth/token` with `client_credentials` grant, and persists the
resulting tokens.
- **Bump `twenty-sdk`, `twenty-client-sdk`, `create-twenty-app` to
`1.22.0-canary.2`**

## Context

The `1.22.0-canary.1` SDK release expected
`createApplicationRegistration` to return `accessToken`/`refreshToken`
directly, but the `v1.22.0` server returns `clientSecret`. This caused
`yarn twenty dev` and `yarn twenty dev --once` to fail with "No
registration found" errors.
2026-04-11 12:26:18 +02:00
Thomas des Francs
300be990b0
halftone v2 (#19573)
## Summary
- Add Playwright inspection scripts for problem canvas export, chain
logging, and metrics capture
- Save updated markdown snapshots and screenshot artifacts for homepage
and problem section debugging
- Include generated browser profile data used during the investigation

## Testing
- Not run (not requested)
2026-04-11 10:00:24 +00:00
Charles Bochet
0a76db94bc
Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 1.22.0-canary.1 (#19580)
## Summary
- Bumps `twenty-sdk`, `twenty-client-sdk`, and `create-twenty-app`
package versions from `0.9.0` to `1.22.0-canary.1` for the 1.22 canary
release.

## Test plan
- [ ] Verify packages build successfully (`npx nx build twenty-sdk`,
`npx nx build twenty-client-sdk`, `npx nx build create-twenty-app`)
- [ ] Verify `create-twenty-app` scaffolds new apps with the correct SDK
version


Made with [Cursor](https://cursor.com)
2026-04-11 11:34:55 +02:00
Charles Bochet
c26c0b9d71
Use app's own OAuth credentials for CoreApiClient generation (#19563)
## Summary

- **SDK (`dev` & `dev --once`)**: After app registration, the CLI now
obtains an `APPLICATION_ACCESS` token via `client_credentials` grant
using the app's own `clientId`/`clientSecret`, and uses that token for
CoreApiClient schema introspection — instead of the user's
`config.accessToken` which returns the full unscoped schema.
- **Config**: `oauthClientSecret` is now persisted alongside
`oauthClientId` in `~/.twenty/config.json` when creating a new app
registration, so subsequent `dev`/`dev --once` runs can obtain fresh app
tokens without re-registration.
- **CI action**: `spawn-twenty-app-dev-test` now outputs a proper
`API_KEY` JWT (signed with the seeded dev workspace secret) instead of
the previous hardcoded `ACCESS` token — giving consumers a real API key
rather than a user session token.

## Motivation

When developing Twenty apps, `yarn twenty dev` was using the CLI user's
OAuth token for GraphQL schema introspection during CoreApiClient
generation. This token (type `ACCESS`) has no `applicationId` claim, so
the server returns the **full workspace schema** — including all objects
— rather than the scoped schema the app should see at runtime (filtered
by `applicationId`).

This caused a discrepancy: the generated CoreApiClient contained fields
the app couldn't actually query at runtime with its `APPLICATION_ACCESS`
token.

By switching to `client_credentials` grant, the SDK now introspects with
the same token type the app will use in production, ensuring the
generated client accurately reflects the app's runtime capabilities.
2026-04-11 11:24:28 +02:00
github-actions[bot]
f52d66b960
i18n - translations (#19570)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-10 19:19:17 +02:00
Félix Malfait
65b2baca7a
Remove IS_USAGE_ANALYTICS_ENABLED feature flag (#19566)
## Summary
This PR removes the `IS_USAGE_ANALYTICS_ENABLED` feature flag and makes
usage analytics features universally available. The feature flag guard
has been removed from the usage analytics resolver and all conditional
rendering based on this flag has been eliminated.

## Key Changes
- **Removed feature flag dependency**: Deleted
`IS_USAGE_ANALYTICS_ENABLED` from the `FeatureFlagKey` enum in
`twenty-shared`
- **Updated AI Usage tab**: Simplified `SettingsAIUsageTab` to remove
enterprise access checks and feature flag conditionals, now only checks
if ClickHouse is configured
- **Updated Usage Analytics section**: Removed feature flag guard from
`SettingsUsageAnalyticsSection` and added loading/empty state handling
- **Updated AI settings navigation**: Made the Usage tab always visible
in the AI settings tabs, removing conditional rendering based on feature
flag
- **Updated Billing Credits section**: Removed feature flag check before
showing the "View usage" button
- **Updated Settings routes**: Removed `SettingsProtectedRouteWrapper`
with feature flag requirement from usage routes
- **Updated GraphQL resolver**: Removed `@RequireFeatureFlag` decorator
and `FeatureFlagGuard` from the `getUsageAnalytics` query
- **Updated dev seeder**: Removed the feature flag seed entry for
`IS_USAGE_ANALYTICS_ENABLED`

## Implementation Details
- Usage analytics now gracefully handles loading states with
`UsageSectionSkeleton`
- Empty state messaging is shown when no usage data is available yet
- ClickHouse configuration remains the only requirement for usage
analytics functionality
- All enterprise-specific gating for AI usage analytics has been removed
in favor of ClickHouse availability checks

https://claude.ai/code/session_01MRFVXtquL3wS7qmQkDU3AT

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-10 19:12:33 +02:00
github-actions[bot]
66d9f92e60
i18n - translations (#19569)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-10 18:16:40 +02:00
Abdul Rahman
8dd172ba00
Fix: Select next sidebar menu item after removing current item (#19505) 2026-04-10 16:00:59 +00:00
Abdul Rahman
83653a463c
fix: side panel close animation cleanup not firing due to invalid CSS transition (#19556)
## Summary

Fixes the side panel close animation cleanup
(`sidePanelCloseAnimationCompleteCleanup`) never running during the
close transition.

This eventually fixes the issue where in navbar edit mode, clicking on a
nav item sometimes doesn't get selected, the side panel opens but shows
`Select a navigation item to edit` instead of the selected item's
settings. The root cause was stale `isSidePanelClosing` state from a
previous close that wasn't cleaned up, causing the next open to run
cleanup synchronously (without emitting the close event), which
interfered with the navigation item selection flow.

### Root cause

The CSS `transition` on `StyledSidePanelWrapper` used
`${themeCssVariables.animation.duration.normal}s`, which Linaria
converts to a CSS custom property value like `width
var(--t-animation-duration-normal)s`. After CSS variable substitution,
`0.3` and `s` become two separate tokens, not a valid `<time>` dimension
(`0.3s`). The browser rejects the entire transition declaration, falls
back to `all 0s`, and the width change happens instantly with no
`transitionend` event.

### Fix

- Use `calc(var(--t-animation-duration-normal) * 1s)` to properly
construct the `<time>` value (consistent with ~30 other usages in the
codebase).
- Filter `handleTransitionEnd` to only process `width` transitions on
the wrapper element itself, ignoring bubbled child `background-color`
transitions.


## Before


https://github.com/user-attachments/assets/496ccbff-dc96-487d-a994-451dbf2a5165




## After


https://github.com/user-attachments/assets/78255240-1d7d-42cd-9b56-25627613883a
2026-04-10 15:58:25 +00:00
Félix Malfait
8c4a6cd663
Replace AGENT_CHAT_UNKNOWN_THREAD_ID with null for thread state (#19552)
## Summary
This PR refactors the AI chat thread state management to use `null`
instead of a sentinel string value (`AGENT_CHAT_UNKNOWN_THREAD_ID`) to
represent an uninitialized or new chat thread. This improves type safety
and makes the code more idiomatic by using `null` to represent the
absence of a value.

## Key Changes
- **Removed sentinel constant**: Deleted `AGENT_CHAT_UNKNOWN_THREAD_ID`
constant and replaced all usages with `null`
- **Updated state types**: Changed `currentAIChatThreadState`,
`agentChatLastDiffSyncedThreadState`, and
`agentChatDisplayedThreadState` to use `string | null` type with `null`
as default value
- **Updated component family states**: Modified message-related state
families to accept `threadId: string | null` instead of `threadId:
string`
- **Refined null checks**: Added explicit `null` checks in:
- `AgentChatMessagesFetchEffect`: Updated `isNewThread` logic to check
for `null` first
- `useAIChatThreadClick` and `useSwitchToNewAIChat`: Added guards to
only save drafts when `currentAIChatThread !== null`
- `AgentChatThreadInitializationEffect`: Added null check before UUID
validation
- `useEnsureAgentChatThreadIdForSend`: Added null check before comparing
with draft key
- **Updated fallback logic**: Used nullish coalescing operator (`??`) in
`AIChatTab` and `useAIChatEditor` to default to
`AGENT_CHAT_NEW_THREAD_DRAFT_KEY` when thread is null
- **Enhanced refetch safety**: Added early return in
`handleRefetchMessages` to prevent refetching when in new thread state

## Implementation Details
- The change maintains backward compatibility by treating `null` the
same way the code previously treated `AGENT_CHAT_UNKNOWN_THREAD_ID`
- All draft saving operations now safely check for null before
attempting to store drafts
- The nullish coalescing pattern (`currentAIChatThread ??
AGENT_CHAT_NEW_THREAD_DRAFT_KEY`) ensures proper fallback behavior when
accessing draft storage

https://claude.ai/code/session_01Pz8KCygSNgBPYsndbMq8f7

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-10 16:34:03 +02:00
Raphaël Bosi
f99c05f0e8
Fix merge command being available in exclusion mode (#19546)
Fixes
https://twenty-v7.sentry.io/issues/7398810663/?project=4507072563183616

## Bug description

The mergeMultipleRecords command's conditionalAvailabilityExpression
used numberOfSelectedRecords >= 2, which evaluates correctly in both
selection and exclusion (Select All) modes. However, when the command
executes, buildHeadlessCommandContextApi returns an empty
selectedRecords array in exclusion mode because it can't synchronously
resolve record IDs from an "all minus excluded" set. This caused
MergeMultipleRecordsCommand to throw on the empty array guard.

## Fix

Prepended not isSelectAll and to the merge command's conditional
expression, preventing it from appearing when records are selected via
Select All.
2026-04-10 14:12:46 +00:00
Paul Rastoin
c99db7d9c6
Fix server-validation ci pending instance command detection (#19558)
https://github.com/twentyhq/twenty/actions/runs/24246052659/job/70792870185
2026-04-10 13:53:56 +00:00
Paul Rastoin
5a22cc88c5
database:reset depends on database:init that runs slow instance commands (#19557)
```ts
Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] LogicFunctionModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] ApplicationUpgradeModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] WorkspaceModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] IteratorActionModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] DelayActionModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] RestApiCoreModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] RecordCRUDActionModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] WorkflowTriggerModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] TimelineMessagingModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] WorkflowVersionModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] UserModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] WorkflowToolsModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] ApplicationOAuthModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] BillingWebhookModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] AiAgentExecutionModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] AiAgentActionModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] ToolProviderModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] AiAgentMonitorModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] WorkflowExecutorModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] WorkflowRunnerModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] McpModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] AiChatModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] WorkflowApiModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [InstanceLoader] AuthModule dependencies initialized
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [DatabaseConfigDriver] [INIT] Loading initial config variables from database
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [DatabaseConfigDriver] [INIT] Config variables loaded: 1 values found in DB, 83 falling to env vars/defaults
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [UpgradeCommandRegistryService] Registered 3 fast instance, 0 slow instance, and 14 workspace command(s) for 1.21.0
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [UpgradeCommandRegistryService] Registered 4 fast instance, 1 slow instance, and 3 workspace command(s) for 1.22.0
[Nest] 46347  - 04/10/2026, 3:37:43 PM     LOG [GenerateInstanceCommandCommand] Generating fast instance command for version 1.22.0...
[Nest] 46347  - 04/10/2026, 3:37:43 PM    WARN [GenerateInstanceCommandCommand] No changes in database schema were found - cannot generate a migration.

———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

 NX   Successfully ran target database:migrate:generate for project twenty-server and 8 tasks it depends on (11s)

      With additional flags:
        --name=martmull

```
2026-04-10 15:44:16 +02:00
Paul Rastoin
9a403b84f3
database:init:prod triggers instance slow command too (#19555)
When creating a db from scrath we still need to run the slow instance
command so the associated queries are still applied to the empty db

Please note that by default if there's no workspace the instance slow
runner will skip the runDataMigration part
2026-04-10 13:19:19 +00:00
Paul Rastoin
a82d078906
Lambda build update instead of delete existing logic function while building (#19116)
# Introduction
Avoid having a time window where the logic function is unavailable while
being built, by implementing an update flow instead of delete and create
everytime

fixes https://twenty-v7.sentry.io/issues/7371110591/

**Note: executor code propagation (pre-existing limitation)**

The Lambda executor shim
(`logic-function-drivers/constants/executor/index.mjs`) is only deployed
when a Lambda is first created. If this file is edited, the change won't
propagate to existing Lambdas — neither before nor after this PR. A
follow-up could compare the deployed `CodeSha256` against a local
checksum to detect drift and trigger a code update via
`UpdateFunctionCodeCommand`.

Should implem as code versioning or checksum diffing
2026-04-10 12:56:08 +00:00
Yassir S.
61720470c5
fix: expand kanban column drop zone to full height (#18897)
## Summary
- Added `flex: 1` to `StyledColumnContainer` in `RecordBoardColumns.tsx`
- The column wrapper wasn't stretching vertically, so the droppable area
only covered the top of each column where cards existed
- Now the entire column height is a valid drop target

## Test plan
- [ ] Open a board/kanban view
- [ ] Drag a card from one column
- [ ] Drop it in the lower/empty area of another column
- [ ] Verify the drop registers correctly

Fixes #18842
2026-04-10 12:54:45 +00:00
Thomas Trompette
31baf52528
Workflow - Avoid billing skipped steps (#19547)
As title
2026-04-10 12:49:14 +00:00
Etienne
217957f2a1
Optim - Increase connection idle timeout (#19553)
<img width="1486" height="55" alt="Screenshot 2026-04-10 at 14 20 19"
src="https://github.com/user-attachments/assets/29bbd120-e183-4ea3-a6c5-5cd876a81ef5"
/>
<img width="1490" height="55" alt="Screenshot 2026-04-10 at 14 20 14"
src="https://github.com/user-attachments/assets/0455b6c7-032a-4cd7-8741-fd7b73e537e5"
/>
<img width="1482" height="57" alt="Screenshot 2026-04-10 at 14 20 07"
src="https://github.com/user-attachments/assets/18699de8-4e1c-44b8-bcb9-871ed1e6e6d0"
/>
On last 7 days
2026-04-10 12:43:19 +00:00
Félix Malfait
4e6cf185fa
Fix error handling in stream agent chat job (#19550)
## Summary
Improved error handling in the StreamAgentChatJob to properly propagate
and reject errors that occur during the agent chat stream processing.

## Key Changes
- Added error re-throw in the catch block to ensure errors are not
silently swallowed
- Introduced `streamError` variable to capture errors from the stream's
`onError` callback
- Modified the promise resolution logic to reject with the captured
stream error instead of silently resolving on success, ensuring proper
error propagation to callers

## Implementation Details
The changes ensure that errors occurring during stream processing are
properly captured and propagated:
1. Stream errors are now captured in the `onError` callback and stored
in the `streamError` variable
2. After the stream completes and the message is persisted to the
database, the promise is rejected if a stream error occurred
3. The outer catch block now re-throws errors to prevent silent failures

This fix prevents scenarios where stream processing errors would be
masked by a successful database persistence operation.

https://claude.ai/code/session_016DQodL32HYv6CR1cMASNHZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-10 13:14:19 +02:00
Baptiste Devessier
001b7285e6
Support junction relations in Field widget (#19518)
## Resolved relations in picker



https://github.com/user-attachments/assets/bf2281a4-825b-4638-9cb9-7f1be987b8b6
2026-04-10 11:03:33 +00:00
github-actions[bot]
ec7d19a3af
i18n - translations (#19551)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-10 12:53:20 +02:00
neo773
ed07afa774
Workspace export command follow-up (#19549)
Did QA with a new workspace as well as the seeded YC workspace.
2026-04-10 12:48:04 +02:00
Charles Bochet
b284c8323c
Remove Favorite and FavoriteFolder from workspace schema (#19536)
## Summary

- Removes all workspace schema definitions for `Favorite` and
`FavoriteFolder` entities, which have been fully migrated to
`NavigationMenuItems`
- Deletes 26 standalone files including workspace entities, NestJS
modules, services, listeners, jobs, standard application builders (field
metadata, views, view fields, view field groups, indexes, page layouts),
mocks, and integration tests
- Cleans up ~40 modified files: removes `favorites` relation from 10
workspace entities and their field metadata utils, removes entries from
all builder maps, shared constants (`STANDARD_OBJECTS`,
`CoreObjectNameSingular`, `DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS`), SDK
default relations, AI tool filtering, and standard object icons
2026-04-10 12:47:06 +02:00
Abdul Rahman
34a903b4fa
Object icon visual parity (#19374)
## Summary

Aligns **object metadata** icons with the **tinted tile** look
everywhere we show a workspace object, and **retires** the
navigation-only `NavigationMenuItemStyleIcon` wrapper in favor of
**shared** UI primitives under `@/ui/display` and `@/object-metadata`.

## What changed

### Global tinted icon building blocks (`@/ui/display`)

- **`TintedIconTile`** / **`StyledTintedIconTileContainer`** support
optional **`size`** and **`stroke`**, and grow the tile when **`size`**
is set so layouts match previous `theme.icon` usage.
- Shared helpers and constants for theme color parsing and tinted
backgrounds/borders/icon color (e.g.
**`getTintedIconTileStyleFromColor`**, **`parseThemeColor`**,
**`getColorFromTheme`**, related constants).

### Object metadata icon (`@/object-metadata`)

- **`ObjectMetadataIcon`** composes **`TintedIconTile`** with
**`getObjectColorWithFallback`**, forwards optional **`size`** /
**`stroke`** for **visual parity** with old `getIcon` + explicit sizing.
- **`getSelectOptionIconFromObjectMetadataItem`** returns an
**`IconComponent`** for selects/menus that expect a component, not a
React node.

### Navigation module cleanup

- **Removed** **`NavigationMenuItemStyleIcon`**; call sites use
**`ObjectMetadataIcon`**, **`TintedIconTile`**, and/or the shared
**`getTintedIconTileStyleFromColor`** pipeline so the same treatment is
**not** tied to the navigation package.
- **`NavigationMenuItemIcon`**, view/link overlays, DnD handle, and
sidebar editor flows updated to use the shared pattern where they render
object (or tinted) icons.

### Product surfaces updated (non-exhaustive)

- **Settings:** role object picker/rows, data model
tables/graph/overview, object preview summary, webhooks entity list,
morph relation multiselect.
- **Workflows:** create/update/delete/upsert/find records, triggers,
filters, variables dropdowns, AI agent object rows, object dropdowns.
- **Shell:** side panel object filter / data sources / folder chrome
where object icons appear.
- **Records:** index header icon, show breadcrumb styling.
- **Activity:** timeline event icon when linked object metadata applies.
- **`NavigationDrawerItem`:** tinted branch aligned with shared
**`TintedIconTile`** behavior.

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-04-10 10:14:20 +00:00
martmull
aed81a54a2
Upgrade cli tool version in technical apps (#19542)
as title
2026-04-10 09:43:13 +00:00
Paul Rastoin
847e7124d7
Upgrade command internal doc (#19541)
Open to discussion not sure on where to store such documentation
2026-04-10 09:43:06 +00:00
Weiko
186bc02533
Skip email/calendar tab creation for custom object record page layouts (#19544)
## Context
Emails and calendars can only be associated with specific objects
(companies, people...) and we were adding those tabs for all custom
objects. I'm removing these from the default config
2026-04-10 09:41:49 +00:00
Thomas des Francs
e03ac126ab
halftone generator v1 + new 3d shapes effect start (#19539)
## Summary
- refresh the website halftone studio with new rendering controls, UI
updates, and export/runtime plumbing
- improve halftone output quality by opening highlights, grouping
shadows, and reducing visible row artifacts
- update home/problem illustration assets, 3D model references, and
related illustration components to use the new visuals
- adjust footer and layout-related website wiring to support the updated
illustration experience

## Testing
- Not run (not requested)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-10 09:37:13 +00:00
github-actions[bot]
63c407e2f7
i18n - translations (#19548)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-10 11:44:04 +02:00
Raphaël Bosi
4b3a46d953
Refactor command menu items deprecated code (#19508)
- Removes the intermediate `CommandMenuItemConfig` /
`CommandConfigContext` / `CommandMenuItemDisplay` abstraction layers,
replacing them with a single `CommandMenuItemRenderer` that renders
directly from the command menu items from the backend
- Eliminates the server-items/ subdirectory by moving its contents
(hooks/, contexts/, states/, display/, edit/) up into the parent
command-menu-item/ module, removing an unnecessary nesting level.
2026-04-10 09:28:27 +00:00
nitin
f13e7e01fe
[AI] Add group_by_* database tools and centralize groupBy validation (#19406)
closes
https://discord.com/channels/1130383047699738754/1488990242873806868



https://github.com/user-attachments/assets/2b2bbfba-3fa6-4114-9a26-96a61599d748

<img width="729" height="1283" alt="CleanShot 2026-04-07 at 20 43 06"
src="https://github.com/user-attachments/assets/815efb97-81a0-44ea-8d79-b3ce7d5b00b6"
/>


<img width="708" height="1266" alt="CleanShot 2026-04-07 at 20 40 13"
src="https://github.com/user-attachments/assets/692366bc-b629-4d9f-b6b8-ab670d5ad046"
/>

<img width="665" height="3524" alt="CleanShot 2026-04-07 at 20 42 00"
src="https://github.com/user-attachments/assets/5e844e0f-7835-47a8-9d20-a5baddc0992d"
/>
2026-04-10 08:45:18 +00:00
martmull
43ce396152
Upgrade cli tool version (#19538)
0.9.0
2026-04-10 10:19:00 +02:00
martmull
2ea62317d8
Pre-sync only pre-install-logic-function (#19534)
as title
2026-04-10 07:47:18 +00:00
github-actions[bot]
f8c35a95b5
i18n - translations (#19537)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-10 10:09:01 +02:00
Félix Malfait
67e7f05a68
feat: email attachments and open-in-app click action (#19485)
## Changes

### Email Attachments
- Added `EmailAttachmentsField` component for uploading and managing
email attachments
- New `useUploadEmailAttachment` hook for handling file uploads with
size validation
- New `UPLOAD_EMAIL_ATTACHMENT_FILE` mutation for backend file
persistence
- Integrated attachments into email composer with file validation
- Added `EmailRecipientLimits` constant to enforce max recipients (100)
on frontend

### Open in App Click Action
- New `useOpenEmailInAppOrFallback` hook to open emails in the in-app
composer
- Email fields now default to "Open in app" action instead of "Open as
link"
- New `SettingsDataModelFieldOnClickActionForm` support for
`OPEN_IN_APP` action
- Email secondary table cell button now offers in-app composer as
alternative action
- `AttachmentChip` component moved from advanced-text-editor to file
module for reuse

### Refactoring & New Utilities
- Extracted `useComposeEmailForTargetRecord` hook for consistent email
composer opening
- New `useResolveDefaultEmailRecipient` hook to resolve recipient based
on record type
- New `getPrimaryEmailFromRecord` utility for safe email field access
- New `EmptyInboxPlaceholder` component with CTA button
- Simplified `ComposeEmailButton` using new hooks
- Enhanced `ComposeEmailCommand` to support bulk Person selections
- Updated `useSendEmail` to accept and forward attachments
- Recipient count validation with warning in composer footer

### Backend
- New `FileEmailAttachmentModule` with resolver and service
- New `file-email-attachment.command` for record selection menu items
- Updated `SendEmailInput` GraphQL type to include `files` field
- Email tool constants and exceptions updated

### Type Updates
- Added `SendEmailAttachmentInput` GraphQL type
- Added `FileFolder.EMAIL_ATTACHMENT` to file folder interface

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-10 10:01:50 +02:00
Félix Malfait
cbefd42c4c
Add SSE streaming support on POST /mcp (Phase 2) (#19528)
## Summary

- Add SSE (`text/event-stream`) as an alternative response format on
`POST /mcp` per the MCP streamable-http spec
- When clients send `Accept: text/event-stream`, the server responds
with SSE wire format; otherwise returns JSON as before (fully backwards
compatible)
- Emit a `notifications/progress` SSE event before tool execution to
signal long-running operations
- No sessions, no GET SSE, no protocol version bump — this is
transport-level only (Phase 2)

## Changes

- **New**: `write-sse-event.util.ts` — writes correctly formatted SSE
events to Express Response
- **New**: `mcp-progress-notification.const.ts` — constants for progress
notification method and token prefix
- **Modified**: `mcp-core.controller.ts` — checks `Accept` header,
branches into SSE vs JSON response path
- **Modified**: `mcp-protocol.service.ts` — passes optional `sseWriter`
callback to tool executor
- **Modified**: `mcp-tool-executor.service.ts` — emits progress
notification via `sseWriter` before tool execution
- **Tests**: Unit tests for SSE utility, controller SSE/JSON paths, tool
executor progress notifications, and integration tests for SSE streaming

## Test plan

- [x] Unit tests: `writeSseEvent` utility produces correct SSE wire
format
- [x] Unit tests: Controller returns SSE headers and writes events when
`Accept: text/event-stream`
- [x] Unit tests: Controller returns JSON when `Accept:
application/json` only
- [x] Unit tests: Notifications (no `id`) return 202 regardless of
Accept header
- [x] Unit tests: Tool executor emits progress notification via
sseWriter
- [x] Unit tests: Tool executor works without sseWriter (backwards
compatible)
- [x] Integration tests: SSE response for ping, JSON fallback, progress
notification before tool call
- [x] All 503 test suites pass, typecheck clean

https://claude.ai/code/session_01QrqjBUXePJkPMd6gBAoWaR

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-10 09:56:17 +02:00
Charles Bochet
f6423f5925
Remove DataSourceService and clean up datasource migration logic (#19532)
## Summary

- **Drop the `objectMetadata.dataSourceId` foreign key and index** via a
1-22 fast instance command — column kept nullable for data preservation
- **Delete `DataSourceService`, `DataSourceModule`, and
`DataSourceException`** — all code now uses `workspace.databaseSchema`
directly
- **Remove `IS_DATASOURCE_MIGRATED` feature flag** from default flags
and all branching logic
- **Simplify workspace/object creation pipelines** —
`WorkspaceManagerService`, `DevSeederService`, and the object creation
action handler no longer route through `DataSourceService`
- **Keep `DataSourceEntity` and the `dataSource` table** for historical
data — entity stripped of all ORM relations
2026-04-10 07:34:05 +00:00
github-actions[bot]
4fd721a3c9
chore: sync AI model catalog from models.dev (#19533)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-10 08:31:48 +02:00
github-actions[bot]
20f28d6593
i18n - docs translations (#19529)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-10 00:29:03 +02:00
Sri Hari Haran Sharma
0fa7beba44
fix mcp streamable-http method handling (#19496)
## Summary

Fix MCP `/mcp` transport handling for clients using `streamable-http`.

## Changes

- add explicit `GET /mcp` and `DELETE /mcp` handlers
- return `405 Method Not Allowed` with `Allow: POST`
- keep `POST /mcp` protected by MCP auth guards
- mark `GET` and `DELETE` as intentionally public with
`PublicEndpointGuard` + `NoPermissionGuard`
- update the advertised MCP protocol version to `2025-03-26`
- add unit and integration coverage for the new behavior

## Why

The frontend advertises the MCP server as `streamable-http`, but the
backend only effectively handled `POST /mcp`. Some MCP clients probe
`GET /mcp` during connection setup, so unsupported methods need explicit
method-level responses instead of falling through or being blocked
before the handler.

## Validation

- verified locally:
  - `GET /mcp` -> `405`
  - `DELETE /mcp` -> `405`
  - unauthenticated `POST /mcp` -> `401`
- passed controller unit tests
- added integration assertions for `GET` and `DELETE`

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-04-09 20:38:28 +00:00
github-actions[bot]
fb950cf312
i18n - translations (#19527)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 22:37:55 +02:00
Félix Malfait
2bb939b4b5
Add file attachment support to agent chat messaging (#19517)
## Summary
This PR adds support for attaching files to agent chat messages. Users
can now upload files when sending messages to the AI agent, and these
files are properly processed, stored, and made available to the agent
with signed URLs.

## Key Changes

- **File attachment input**: Added `fileIds` parameter to the
`sendChatMessage` GraphQL mutation to accept file IDs from the client
- **File processing**: Implemented `buildFilePartsFromIds()` method to
convert file IDs into file UI parts with signed URLs
- **Message composition**: Updated user messages to include both text
and file parts when files are attached
- **File URL signing**: Integrated `FileUrlService` to generate signed
URLs for files in the AgentChat folder, ensuring secure access
- **Message persistence**: Files are now included in the message parts
stored in the database and retrieved when loading conversation history
- **File metadata mapping**: Enhanced `mapDBPartToUIMessagePart()` to
properly extract MIME types from file entities and include file IDs

## Implementation Details

- Files are fetched from the database using the provided file IDs and
workspace context
- Each file is converted to an `ExtendedFileUIPart` with proper metadata
(filename, MIME type, signed URL, and file ID)
- When loading messages from the database, file parts are enhanced with
signed URLs to ensure they remain accessible
- The `loadMessagesFromDB()` method now requires the workspace ID to
properly sign file URLs
- File attachments are seamlessly integrated into the existing message
part system alongside text content

https://claude.ai/code/session_01TAdN1gBzeiYELX4XDrrYY1

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-09 22:37:03 +02:00
martmull
d2f51cc939
Fix pre post logic function not executed (#19462)
- removes pre-install function 
- execute **asyncrhonously** post-install function at application
installation
- add optional `shouldRunOnVersionUpgrade` boolean value on post-install
function definition default false
- update PostInstallPayload to 
```
export type PostInstallPayload = {
  previousVersion?: string;
  newVersion: string;
};
```

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-09 20:22:41 +00:00
github-actions[bot]
7a317b9182
i18n - translations (#19525)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 21:49:01 +02:00
Félix Malfait
df1d0d877d
Flatten AI tool call output structure (#19524)
## Summary
Refactored the `AgentChatMessageUIToolCallPart` output structure to
flatten the nested result object, simplifying the data model and
improving code clarity.

## Key Changes
- **Flattened output structure**: Moved `success`, `message`, and
`result` fields from `output.result` directly to `output`, eliminating
unnecessary nesting
- **Removed `toolName` field**: Deleted the unused `toolName` property
from the output object
- **Made fields optional**: Changed `error` and `result` to optional
fields to better represent cases where they may not be present
- **Updated hook logic**: Modified `useProcessUIToolCallMessage` to
access the flattened structure:
- Changed `toolExecutionPart.output.result.success` to
`toolExecutionPart.output.success`
- Changed `toolExecutionPart.output.result.result` to
`toolExecutionPart.output.result`
- Added null check for `navigateAppOutput` to handle cases where result
is undefined

## Implementation Details
The refactoring maintains backward compatibility in functionality while
simplifying the data structure. The optional `result` field now properly
reflects that navigation output may not always be present, with an
explicit guard clause added to handle this case gracefully.

https://claude.ai/code/session_01EnYKwVaCJyhgNPsi7p3YSq

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-09 19:36:03 +00:00
Baptiste Devessier
e3077691d1
Edit visibility restriction (#19499)
https://github.com/user-attachments/assets/98496f15-2e58-46a8-b233-9cf46b7b9600
2026-04-09 19:33:15 +00:00
neo773
8a10071253
add workspaceId to indirect entities (#19522)
Required for `workspace:export` command

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-09 19:30:28 +00:00
Félix Malfait
7ef80dd238
Add standard skills backfill and improve skill availability messaging (#19523)
## Summary
This PR adds a database migration command to backfill standard skills
for existing workspaces and improves the skill loading tool to provide
dynamic, workspace-specific skill availability information instead of
hardcoded skill names.

## Key Changes

- **New Migration Command**: Added `BackfillStandardSkillsCommand`
(v1.22.0) that:
  - Identifies missing standard skills in existing workspaces
  - Compares workspace skills against the standard skill definitions
  - Creates missing skills using the workspace migration service
  - Supports dry-run mode for safe testing
  - Properly logs all operations and handles failures

- **Enhanced Skill Loading Tool**: Updated `createLoadSkillTool` to:
  - Accept a new `listAvailableSkillNames` function parameter
- Dynamically fetch available skills from the workspace instead of using
hardcoded skill names
- Provide accurate, context-aware error messages when skills are not
found
  - Gracefully handle workspaces with no available skills

- **Service Updates**: Modified skill tool implementations in:
- `McpProtocolService`: Integrated `findAllFlatSkills` to list available
skills
- `ChatExecutionService`: Integrated `findAllFlatSkills` to list
available skills

- **Module Registration**: Added `BackfillStandardSkillsCommand` to the
v1.22 upgrade module

- **Test Updates**: Updated `McpProtocolService` tests to mock the new
`findAllFlatSkills` method

## Implementation Details

The backfill command uses the existing workspace migration
infrastructure to safely create skills, ensuring consistency with other
metadata operations. The skill availability messaging now reflects the
actual skills present in each workspace, improving user experience when
skills are not found.

https://claude.ai/code/session_012fXeP3bysaEgWsbkyu4ism

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-09 21:28:23 +02:00
github-actions[bot]
8fde5d9da3
i18n - translations (#19521)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 19:08:26 +02:00
Weiko
afdd914b83
Add rich-text field widget (#19512)
## Context
- Extend the FIELD widget to support RICH_TEXT fields alongside existing
RELATION/MORPH_RELATION fields
- Add EDITOR display mode that renders a full rich text editor, and
FIELD display mode that shows a compact single-line preview
- Enforce mutual exclusivity: EDITOR is only available for RICH_TEXT,
CARD only for RELATION, FIELD for both
- Refactor widget configuration into a centralized FIELD_WIDGET_CONFIG
constant and shared useFieldWidgetEligibleFields hook for better
scalability. Later we might use discriminative union from graphql
scehma)

<details>
<summary>
EDITOR mode
</summary>
<img width="1095" height="731" alt="Screenshot 2026-04-09 at 17 31 41"
src="https://github.com/user-attachments/assets/cebffd0e-07ea-4f74-a3dc-ef987daa17ea"
/>
</details>
<details>
<summary>
FIELD mode
</summary>
<img width="986" height="378" alt="Screenshot 2026-04-09 at 17 35 00"
src="https://github.com/user-attachments/assets/c90a8046-fdd0-4321-8ba6-f47d89e9d42a"
/>
</details>
<details>
<summary>
Open FIELD mode
</summary>
<img width="758" height="480" alt="Screenshot 2026-04-09 at 17 35 04"
src="https://github.com/user-attachments/assets/c53cc120-0b6f-47a0-808c-27b26f9f53ec"
/>
</details>
2026-04-09 16:52:30 +00:00
neo773
77498d2f73
Fix/align microsoft calendar error handling (#19519)
fixes sentry TWENTY-SERVER-FVC
2026-04-09 16:37:06 +00:00
Abdul Rahman
9bea5f73f1
Fix system objects not appearing in sidebar View picker due to filtering mismatch (#19502)
## Summary
- System objects with valid views were incorrectly hidden in the View >
System objects picker
- The system picker page used a different filter (`view.key !==
ViewKey.INDEX`) than the parent page
(`isViewDisplayableInNavigationMenu`), causing a mismatch where the
"System objects" link was visible but clicking it showed an empty list
- Aligned filtering in `SidePanelNewSidebarItemViewSystemPickerSubPage`
to use `isViewDisplayableInNavigationMenu` and exclude views already in
the workspace, consistent with the non-system picker page
2026-04-09 16:35:29 +00:00
github-actions[bot]
c54747379a
i18n - translations (#19520)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 18:43:56 +02:00
Thomas Trompette
00208dbf1f
Build lambda error - catch user code compilation errors (#19516)
Fixes
https://twenty-v7.sentry.io/issues/7335177730/events/latest/?environment=prod&project=4507072499810304&query=is%3Aunresolved&referrer=latest-event

- Provide a more explicit error for user
- Remove from sentry while keeping others due to infra issues
2026-04-09 16:28:27 +00:00
Paul Rastoin
e411f076f1
Replace typeorm binary by database:migrate:generate (#19515) 2026-04-09 16:25:10 +00:00
Charles Bochet
07745947ee
Fix rolesPermissions cache query cartesian product (62k → 162 rows) (#19511)
## Summary
- **Split the `rolesPermissions` cache query into parallel queries** to
eliminate a 5-way LEFT JOIN cartesian product. A workspace with a role
having 5 objectPermissions × 4 permissionFlags × 124 fieldPermissions ×
5 RLP predicates × 5 RLP groups was returning 62,000 rows from just ~162
distinct records, causing 10s+ query timeouts on view updates.
- **Added missing `roleId` index on `permissionFlag` table** — the only
permission table without one, which was causing sequential scans on the
JOIN.
2026-04-09 16:13:33 +00:00
Thomas Trompette
082822b790
Fix AI chat threads query firing for users without AI permissions (#19507)
<img width="1511" height="825" alt="Capture d’écran 2026-04-09 à 14 19
52"
src="https://github.com/user-attachments/assets/cd6c533e-abc9-45f9-b5c1-5cb7341d5dfe"
/>

- Move GetChatThreads query from the generic metadata pipeline
(useLoadStaleMetadataEntities) into AgentChatThreadInitializationEffect,
gated by useHasPermissionFlag(AI_SETTINGS) — prevents "Entity does not
have permissions" errors for users whose role lacks AI access
- Fix initialization bug where isDefined(currentAIChatThread) always
returned true for the 'unknown-thread' sentinel value, preventing thread
initialization from ever running — replaced with isValidUuid check
2026-04-09 15:17:30 +00:00
github-actions[bot]
0c7712926d
i18n - translations (#19510)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 17:23:49 +02:00
github-actions[bot]
086256eb8d
i18n - translations (#19510)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 16:58:39 +02:00
github-actions[bot]
93eeeb2397
i18n - docs translations (#19509)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 16:55:20 +02:00
Charles Bochet
36fbfca069
Add application-logs module with driver pattern for logic function log persistence (#19486)
## Summary

- Introduces a new `application-logs` core module with a driver pattern
(disabled/console/clickhouse) to capture and persist logic function
execution logs
- Adds a ClickHouse `applicationLog` table with per-line log storage,
30-day TTL, and `ORDER BY (workspaceId, timestamp, applicationId,
logicFunctionId)`
- Surfaces application logs in the existing frontend audit logs table as
a new "Application Logs" source with dedicated columns (Function,
Timestamp, Level, Message, Execution ID)

## Details

**Write path**: `LogicFunctionExecutorService.handleExecutionResult()`
parses the multi-line log string from driver output into individual `{
timestamp, level, message }` entries, generates an execution UUID, and
passes them to `ApplicationLogsService.writeLogs()` which delegates to
the configured driver.

**Driver pattern**: Follows the exception-handler module style (Symbol
injection token + `forRootAsync` dynamic module). Three drivers:
- `DISABLED` (default) — no-op, prevents information leaking
- `CONSOLE` — structured stdout logging with level-based `console.*`
calls
- `CLICKHOUSE` — inserts rows into the `applicationLog` ClickHouse table

**Read path**: Extends the existing event-logs module by adding
`APPLICATION_LOG` to the `EventLogTable` enum, table name mapping, and
normalization logic.

**Config**: New `APPLICATION_LOG_DRIVER_TYPE` environment variable
(default: `DISABLED`).
2026-04-09 14:35:24 +00:00
Abdullah.
5116002ca2
chore: replace glb files and lottie with optimized variants (#19503)
As per title. Replaced .json file with .lottie file - it is 10x smaller.

Illustrations folder went from 150mb to 5.9mb with compressed variants
of 3D models. The DracoLoader, GLTFLoader and some other logic logic is
repetitive across files, but Thomas is working on the same files at the
moment, so not touching too much to avoid conflicts with his PR. Once
he's done styling, we can extract shared logic into helpers.
2026-04-09 14:27:13 +00:00
Etienne
caac791421
Cleaning - Remove logs (#19498) 2026-04-09 14:19:53 +00:00
github-actions[bot]
d3a1f45027
i18n - translations (#19506)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 16:32:53 +02:00
github-actions[bot]
a5174c0519
i18n - translations (#19504)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 16:25:54 +02:00
nitin
50c5a1a8de
[AI] new model tab design (#19384)
closes
https://discord.com/channels/1130383047699738754/1480979892610007121

<img width="1839" height="1316" alt="CleanShot 2026-04-07 at 15 22 38"
src="https://github.com/user-attachments/assets/d8f6048c-4e0f-425f-b7ea-4913d116020e"
/>
2026-04-09 14:15:43 +00:00
Etienne
74e26ae635
Fix Date/DateTime/Relation field forms (#19463)
https://discord.com/channels/1130383047699738754/1486025695481168102

On Date and DateTime, picker doesn't open when clicking.
On Relation, No record option can't be selected. Introduce the "null",
to break relation.
2026-04-09 14:09:44 +00:00
Paul Rastoin
5e3cf7cd2b
Prepare 1.21 (#19501)
Migrating some 1.21 migrations to the new pattern so it writes in the
history
When release in prod we will ahve to manually inject them as they have
already been run
It's mainly for self host so when releasing the 1.22 we will be able to
rely on the 1.21 history
2026-04-09 13:53:21 +00:00
Baptiste Devessier
16e145b036
Fix moving a widget to another tab (#19450)
https://github.com/user-attachments/assets/aac81e79-7f2f-4a34-bf68-c76061086821

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-04-09 15:59:04 +02:00
neo773
1e908e5f0f
fix: SendEmail workflow ConnectedAccount query (#19484) 2026-04-09 15:58:12 +02:00
Weiko
d495a9f412
reorganize standard page layouts (#19482)
<details>
<summary>
Reorganizing standard page layouts (see screenshot below)
</summary>
<img width="355" height="617" alt="Screenshot 2026-04-09 at 10 44 02"
src="https://github.com/user-attachments/assets/0b71d607-1d9d-48b6-8612-3de98d12d1fa"
/>
</details>
<details>
<summary>
Note: All standard objects now have a last system section for
createdBy/createdAt however since all new custom fields are added to the
last section they are set there (see 2nd screenshot below) until people
move them to dedicated section which can be counterintuitive. There is
an upcoming feature that allows user to set a default section and
visibility for newly added fields that should solve that
</summary>
<img width="360" height="849" alt="Screenshot 2026-04-09 at 10 43 44"
src="https://github.com/user-attachments/assets/127990d0-66b7-4b1d-ab00-8251e9707a04"
/>
</details>

Another note: Section are not translated at the moment
2026-04-09 15:44:38 +02:00
github-actions[bot]
1df9942416
i18n - translations (#19500)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 15:05:50 +02:00
Etienne
34972f6167
Record table widget - Follow up (#19479)
closes https://github.com/twentyhq/core-team-issues/issues/2360
2026-04-09 12:50:10 +00:00
Abdullah.
37acd15033
More website fixes. (#19489)
This PR fixes most of the visual issues with home page and pricing page.
I think it would be a good checkpoint to merge.
2026-04-09 12:41:14 +00:00
github-actions[bot]
c76824e69f
i18n - docs translations (#19497)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 14:42:31 +02:00
neo773
7bd84a6029
messaging fix relaunch cron jobs (#19492) 2026-04-09 12:25:12 +00:00
Weiko
1577a1933f
Move page layout backfill command out of 1-21 release (#19483)
Note: The next tag might be 2.0 and this can be changed later, the point
here is to move the command out of 1-21
2026-04-09 12:21:47 +00:00
Raphaël Bosi
086ea644bb
Resolve frontComponent relation on command menu items via selector (#19493)
## Bug description

Headless command menu items were interpreted as non headless command
menu items and opened in the side panel.


https://github.com/user-attachments/assets/44d8b4d3-9ee5-4f12-9ff8-b3ed51e5b3b6

## Fix


https://github.com/user-attachments/assets/9d6eb900-9817-4ceb-87aa-547ad046a5a1

- Command menu items received via SSE lack the nested `frontComponent`
relation (only `frontComponentId` is present), causing
`item.frontComponent?.isHeadless` to always be undefined.
- Add `frontComponents` to the metadata store's stale entity loading
- Rewrite `commandMenuItemsSelector` to join `commandMenuItems` with
`frontComponents` by `frontComponentId`
2026-04-09 12:15:52 +00:00
Raphaël Bosi
19f11b1216
[COMMAND MENU ITEMS] Add dynamic label and icon to command menu navigation items (#19452) 2026-04-09 12:00:49 +00:00
Félix Malfait
9b8cb610c3
Flush Redis between server runs in breaking changes CI (#19491)
## Summary
Added a Redis cache flush step in the breaking changes CI workflow to
prevent stale data contamination between consecutive server runs.

## Key Changes
- Added a new workflow step that executes `redis-cli FLUSHALL` between
the current branch server run and the main branch server run
- Includes error handling to log a warning if the Redis flush fails,
without blocking the workflow
- Added explanatory comments documenting why this step is necessary

## Implementation Details
The Redis flush is necessary because both the current branch and main
branch servers share the same Redis instance during CI testing. The
`CoreEntityCacheService` and `WorkspaceCacheService` persist cached
entities across process restarts, which can cause stale data from the
current branch server to contaminate the main branch server comparison.
This step ensures a clean cache state before switching branches.

https://claude.ai/code/session_01BVMacfAXDMNx5WAFtP7GgW

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-09 12:56:24 +02:00
github-actions[bot]
64c7f52f06
i18n - docs translations (#19490)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 12:39:31 +02:00
github-actions[bot]
dc6e76b6ab
i18n - translations (#19488)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 12:26:14 +02:00
martmull
a78a843419
Fix install and deploy commands (#19481)
- disable publish with existing version
- disable installation of app version already installed

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-04-09 10:10:16 +00:00
martmull
5eaabe95e7
Fix role synchronisation (#19469)
As title
solves
https://discord.com/channels/1130383047699738754/1491167098398052503
2026-04-09 09:36:16 +00:00
Thomas Trompette
5edc034f8b
Enqueue a snack bar on merge preview errors (#19465)
Fixes https://github.com/twentyhq/twenty/issues/19312

<img width="400" height="500" alt="Capture d’écran 2026-04-08 à 18 17
57"
src="https://github.com/user-attachments/assets/41ac28b2-0208-47b4-bd85-f803c524330a"
/>
2026-04-09 08:43:54 +00:00
Raphaël Bosi
9080180156
[COMMAND MENU ITEMS] Sync object metadata with navigation command menu items (#19456)
When an object is created or enabled we create a navigation command menu
item to that object. And when the object is disabled or deleted, we
remove this command menu item.
2026-04-09 08:18:49 +00:00
Félix Malfait
f8dff23a3e
Refactor: Extract EventRow shared types and styles to EventRowBase (#19480)
## Summary
This PR refactors the EventRow component structure by extracting shared
types and styled components into a dedicated base file, improving code
organization and reducing duplication.

## Key Changes
- Created new `EventRowBase.tsx` file to centralize shared EventRow
utilities
- Moved `EventRowDynamicComponentProps` interface from
`EventRowDynamicComponent.tsx` to `EventRowBase.tsx`
- Moved styled components `StyledEventRowItemColumn` and
`StyledEventRowItemAction` from `EventRowDynamicComponent.tsx` to
`EventRowBase.tsx`
- Updated all imports across 6 files to reference the new `EventRowBase`
module instead of `EventRowDynamicComponent`
- Simplified `EventRowDynamicComponent.tsx` to re-export types and
styles from `EventRowBase` for backward compatibility

## Files Updated
- `EventRowDynamicComponent.tsx` - Simplified to import and re-export
from EventRowBase
- `EventRowActivity.tsx` - Updated import path
- `EventRowCalendarEvent.tsx` - Updated import path
- `EventRowMainObject.tsx` - Updated import path
- `EventRowMainObjectUpdated.tsx` - Updated import path
- `EventRowMessage.tsx` - Updated import path

## Benefits
- Better separation of concerns with shared base utilities in a
dedicated module
- Reduced circular dependency risks
- Clearer module structure for future EventRow-related components
- Maintains backward compatibility through re-exports

https://claude.ai/code/session_011EyhBJ56RGuZHxBVWwzEQy

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-09 10:14:18 +02:00
dependabot[bot]
1b8f26323a
Bump @babel/preset-react from 7.26.3 to 7.28.5 (#19475)
Bumps
[@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react)
from 7.26.3 to 7.28.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/releases"><code>@​babel/preset-react</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v7.28.5 (2025-10-23)</h2>
<p>Thank you <a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>, <a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a>, and
<a href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>
for your first PRs!</p>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17446">#17446</a>
Allow <code>Runtime Errors for Function Call Assignment Targets</code>
(<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-validator-identifier</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17501">#17501</a>
fix: update identifier to unicode 17 (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
</li>
</ul>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-plugin-proposal-destructuring-private</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17534">#17534</a>
Allow mixing private destructuring and rest (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
</ul>
</li>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17521">#17521</a>
Improve <code>@babel/parser</code> error typing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17491">#17491</a>
fix: improve ts-only declaration parsing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-proposal-discard-binding</code>,
<code>babel-plugin-transform-destructuring</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17519">#17519</a>
fix: <code>rest</code> correctly returns plain array (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-create-class-features-plugin</code>,
<code>babel-helper-member-expression-to-functions</code>,
<code>babel-plugin-transform-block-scoping</code>,
<code>babel-plugin-transform-optional-chaining</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17503">#17503</a> Fix
<code>JSXIdentifier</code> handling in
<code>isReferencedIdentifier</code> (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17504">#17504</a>
fix: ensure scope.push register in anonymous fn (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17494">#17494</a>
Type checking babel-types scripts (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏃‍♀️ Performance</h4>
<ul>
<li><code>babel-core</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17490">#17490</a>
Faster finding of locations in <code>buildCodeFrameError</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<h4>Committers: 8</h4>
<ul>
<li>Babel Bot (<a
href="https://github.com/babel-bot"><code>@​babel-bot</code></a>)</li>
<li>Byeongho Yoo (<a
href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>)</li>
<li>Huáng Jùnliàng (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li>Hyeon Dokko (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
<li>Nicolò Ribaudo (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
<li><a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a></li>
<li><a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a></li>
<li>fisker Cheung (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
<h2>v7.28.4 (2025-09-05)</h2>
<p>Thanks <a
href="https://github.com/gwillen"><code>@​gwillen</code></a> and <a
href="https://github.com/mrginglymus"><code>@​mrginglymus</code></a> for
your first PRs!</p>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-core</code>,
<code>babel-helper-check-duplicate-nodes</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17493">#17493</a>
Update Jest to v30.1.1 (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-regenerator</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17455">#17455</a>
chore: Clean up <code>transform-regenerator</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/blob/main/CHANGELOG.md"><code>@​babel/preset-react</code>'s
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<blockquote>
<p><strong>Tags:</strong></p>
<ul>
<li>💥 [Breaking Change]</li>
<li>👓 [Spec Compliance]</li>
<li>🚀 [New Feature]</li>
<li>🐛 [Bug Fix]</li>
<li>📝 [Documentation]</li>
<li>🏠 [Internal]</li>
<li>💅 [Polish]</li>
</ul>
</blockquote>
<p><em>Note: Gaps between patch versions are faulty, broken or test
releases.</em></p>
<p>This file contains the changelog starting from v8.0.0-alpha.0.</p>
<ul>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.15.0-v7.28.5.md">CHANGELOG
- v7.15.0 to v7.28.5</a> for v7.15.0 to v7.28.5 changes (the last common
release between the v8 and v7 release lines was v7.28.5).</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.0.0-v7.14.9.md">CHANGELOG
- v7.0.0 to v7.14.9</a> for v7.0.0 to v7.14.9 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7-prereleases.md">CHANGELOG
- v7 prereleases</a> for v7.0.0-alpha.1 to v7.0.0-rc.4 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v4.md">CHANGELOG
- v4</a>, <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v5.md">CHANGELOG
- v5</a>, and <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v6.md">CHANGELOG
- v6</a> for v4.x-v6.x changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-6to5.md">CHANGELOG
- 6to5</a> for the pre-4.0.0 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/packages/babel-parser/CHANGELOG.md">Babylon's
CHANGELOG</a> for the Babylon pre-7.0.0-beta.29 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel-eslint/releases"><code>babel-eslint</code>'s
releases</a> for the changelog before <code>@babel/eslint-parser</code>
7.8.0.</li>
<li>See <a
href="https://github.com/babel/eslint-plugin-babel/releases"><code>eslint-plugin-babel</code>'s
releases</a> for the changelog before <code>@babel/eslint-plugin</code>
7.8.0.</li>
</ul>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<h2>v8.0.0-rc.3 (2026-03-16)</h2>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17839">#17839</a>
Fix(parser): async x =&gt; {} must be in leading pos (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17803">#17803</a>
Disallow non-leading solo await within F# pipeline (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>💥 Breaking Change</h4>
<ul>
<li><code>babel-parser</code>,
<code>babel-plugin-proposal-do-expressions</code>,
<code>babel-plugin-proposal-pipeline-operator</code>,
<code>babel-plugin-transform-exponentiation-operator</code>,
<code>babel-plugin-transform-instanceof</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17867">#17867</a>
[Babel 8] Remove <code>Import</code> from the <code>Expression</code>
alias (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-react-jsx-development</code>,
<code>babel-plugin-transform-react-jsx</code>,
<code>babel-preset-react</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17845">#17845</a>
Gate jsxDEV source/self with <code>developmentSourceSelf</code> option
(<a
href="https://github.com/rootvector2"><code>@​rootvector2</code></a>)</li>
</ul>
</li>
<li><code>babel-generator</code>, <code>babel-parser</code>,
<code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17835">#17835</a>
fix: Remove <code>decorators</code> from <code>TSDeclareMethod</code>
(<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-import-to-platform-api</code>,
<code>babel-plugin-proposal-import-wasm-source</code>,
<code>babel-plugin-transform-json-modules</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17816">#17816</a>
Pass <code>file</code> instead of <code>path</code> to
importToPlatformApi builders (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
</ul>
<h4>🚀 New Feature</h4>
<ul>
<li><code>babel-plugin-transform-react-jsx-development</code>,
<code>babel-plugin-transform-react-jsx</code>,
<code>babel-preset-react</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17862">#17862</a> Add
<code>sourceSelf</code> option to
<code>@babel/plugin-transform-react-jsx-development</code> (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/16935">#16935</a>
feat: Add <code>locations</code> option to parser (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="61647ae239"><code>61647ae</code></a>
v7.28.5</li>
<li><a
href="42cb285b59"><code>42cb285</code></a>
Improve <code>@babel/core</code> types (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17404">#17404</a>)</li>
<li><a
href="eebd3a0602"><code>eebd3a0</code></a>
v7.27.1</li>
<li><a
href="fdc0fb59e1"><code>fdc0fb5</code></a>
[Babel 8] Bump nodejs requirements to <code>^20.19.0 || &gt;=
22.12.0</code> (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17204">#17204</a>)</li>
<li><a
href="cd24cc07ef"><code>cd24cc0</code></a>
chore: Update TS 5.7 (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17053">#17053</a>)</li>
<li>See full diff in <a
href="https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-react">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for <code>@​babel/preset-react</code> since
your current version.</p>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 06:58:41 +00:00
dependabot[bot]
80337d5c37
Bump tsdav from 2.1.5 to 2.1.8 (#19476)
Bumps [tsdav](https://github.com/natelindev/tsdav) from 2.1.5 to 2.1.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/natelindev/tsdav/releases">tsdav's
releases</a>.</em></p>
<blockquote>
<h2>v2.1.8</h2>
<h5>improvements</h5>
<ul>
<li>fixed <a
href="https://redirect.github.com/natelindev/tsdav/issues/272">#272</a>
malformed expand request in fetchCalendarObjects</li>
<li>optimized fetchCalendarObjects to reduce redundant requests when
expand is true</li>
<li>added Bearer auth support for token-based providers (e.g., Nextcloud
OIDC)</li>
<li>added fetch overrides across account, request, and collection
helpers for custom transports</li>
<li>prefer native fetch in Cloudflare Workers to avoid cross-fetch
incompatibilities</li>
<li>collectionQuery now rejects on non-OK responses instead of returning
empty arrays</li>
<li>fetchVCards filters out collection URLs to avoid empty/invalid
entries (Radicale-compatible)</li>
<li>docs updates for iCal feed import, providers, and custom transport
guidance</li>
</ul>
<h2>v2.1.7</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix: Handle empty body responses in davRequest to prevent crash by
<a href="https://github.com/hsvtslv"><code>@​hsvtslv</code></a> in <a
href="https://redirect.github.com/natelindev/tsdav/pull/267">natelindev/tsdav#267</a></li>
<li>Fix npm authentication in auto-release workflow by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/natelindev/tsdav/pull/271">natelindev/tsdav#271</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/hsvtslv"><code>@​hsvtslv</code></a> made
their first contribution in <a
href="https://redirect.github.com/natelindev/tsdav/pull/267">natelindev/tsdav#267</a></li>
<li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made
their first contribution in <a
href="https://redirect.github.com/natelindev/tsdav/pull/271">natelindev/tsdav#271</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/natelindev/tsdav/compare/v2.1.6...v2.1.7">https://github.com/natelindev/tsdav/compare/v2.1.6...v2.1.7</a></p>
<h2>v2.1.6</h2>
<h5>improvements</h5>
<ul>
<li>added AGENTS.md</li>
<li>updated dependencies</li>
<li>fixed docs build issues</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/natelindev/tsdav/blob/main/CHANGELOG.md">tsdav's
changelog</a>.</em></p>
<blockquote>
<h2>v2.1.8</h2>
<h5>improvements</h5>
<ul>
<li>fixed <a
href="https://redirect.github.com/natelindev/tsdav/issues/272">#272</a>
malformed expand request in fetchCalendarObjects</li>
<li>optimized fetchCalendarObjects to reduce redundant requests when
expand is true</li>
<li>added Bearer auth support for token-based providers (e.g., Nextcloud
OIDC)</li>
<li>added fetch overrides across account, request, and collection
helpers for custom transports</li>
<li>prefer native fetch in Cloudflare Workers to avoid cross-fetch
incompatibilities</li>
<li>collectionQuery now rejects on non-OK responses instead of returning
empty arrays</li>
<li>fetchVCards filters out collection URLs to avoid empty/invalid
entries (Radicale-compatible)</li>
<li>docs updates for iCal feed import, providers, and custom transport
guidance</li>
</ul>
<h2>v2.1.7</h2>
<h5>improvements</h5>
<ul>
<li>docs: add browser usage example and clarify class-based login</li>
<li>docs: add Nextcloud connection guidance and Apple password
reference</li>
</ul>
<h2>v2.1.6</h2>
<h5>improvements</h5>
<ul>
<li>added AGENTS.md</li>
<li>updated dependencies</li>
<li>fixed docs build issues</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="1800c16c8e"><code>1800c16</code></a>
Merge pull request <a
href="https://redirect.github.com/natelindev/tsdav/issues/273">#273</a>
from natelindev/fix/fix-all-issues</li>
<li><a
href="ad7782b7a0"><code>ad7782b</code></a>
fix ci</li>
<li><a
href="c0275001ba"><code>c027500</code></a>
fix ci</li>
<li><a
href="2f818c06f8"><code>2f818c0</code></a>
chore: prepare v2.1.8 release</li>
<li><a
href="bfab3ac8eb"><code>bfab3ac</code></a>
fix: prefer global fetch for Cloudflare Workers compatibility (Issue
215)</li>
<li><a
href="1942d42d9b"><code>1942d42</code></a>
fix: support custom fetch override in DAVClient for Electron/CORS (<a
href="https://redirect.github.com/natelindev/tsdav/issues/216">#216</a>)</li>
<li><a
href="3d52838fcd"><code>3d52838</code></a>
docs: add guide for importing iCal feeds (Airbnb, Booking)</li>
<li><a
href="318b0d3486"><code>318b0d3</code></a>
fix: ensure collectionQuery throws on gateway errors and status &gt;=
400</li>
<li><a
href="aa9445a3aa"><code>aa9445a</code></a>
feat: support custom fetch override for KaiOS mozSystem support (Issue
220)</li>
<li><a
href="ce48770780"><code>ce48770</code></a>
fix: address issues with Radicale and CardDAV vCard fetching (Issue
239)</li>
<li>Additional commits viewable in <a
href="https://github.com/natelindev/tsdav/compare/v2.1.5...v2.1.8">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for tsdav since your current version.</p>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 06:42:56 +00:00
dependabot[bot]
91b40b391d
Bump graphql-sse from 2.5.4 to 2.6.0 (#19477)
Bumps [graphql-sse](https://github.com/enisdenjo/graphql-sse) from 2.5.4
to 2.6.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/enisdenjo/graphql-sse/releases">graphql-sse's
releases</a>.</em></p>
<blockquote>
<h2>v2.6.0</h2>
<h1><a
href="https://github.com/enisdenjo/graphql-sse/compare/v2.5.4...v2.6.0">2.6.0</a>
(2025-10-22)</h1>
<h3>Features</h3>
<ul>
<li><strong>client:</strong> Support dynamic URLs and headers per
request for distinct connection mode (<a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/124">#124</a>)
(<a
href="7be7251b42">7be7251</a>),
closes <a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/110">#110</a>
<a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/113">#113</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/enisdenjo/graphql-sse/blob/master/CHANGELOG.md">graphql-sse's
changelog</a>.</em></p>
<blockquote>
<h1><a
href="https://github.com/enisdenjo/graphql-sse/compare/v2.5.4...v2.6.0">2.6.0</a>
(2025-10-22)</h1>
<h3>Features</h3>
<ul>
<li><strong>client:</strong> Support dynamic URLs and headers per
request for distinct connection mode (<a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/124">#124</a>)
(<a
href="7be7251b42">7be7251</a>),
closes <a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/110">#110</a>
<a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/113">#113</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="918b9d414a"><code>918b9d4</code></a>
chore(release): 🎉 2.6.0 [skip ci]</li>
<li><a
href="2320882f6d"><code>2320882</code></a>
docs(readme): unnecessary badge</li>
<li><a
href="d1a1e82aa9"><code>d1a1e82</code></a>
ci: bump actions</li>
<li><a
href="ad13eb1283"><code>ad13eb1</code></a>
docs: algolia is not used anymore</li>
<li><a
href="7be7251b42"><code>7be7251</code></a>
feat(client): Support dynamic URLs and headers per request for distinct
conne...</li>
<li>See full diff in <a
href="https://github.com/enisdenjo/graphql-sse/compare/v2.5.4...v2.6.0">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/~theguild-bot">theguild-bot</a>, a new
releaser for graphql-sse since your current version.</p>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 06:40:38 +00:00
github-actions[bot]
c27e3cd1a0
chore: sync AI model catalog from models.dev (#19478)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-09 08:28:33 +02:00
Félix Malfait
ffb6029c2d
fix(ai-models): use AWS provider chain for Bedrock IRSA auth (#19470)
## Summary
- `buildBedrockProvider` ignored `authType: "role"` and only honored
static `accessKeyId`/`secretAccessKey` pairs, so deployments relying on
IRSA (e.g. EKS pods with `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN`)
had no way to authenticate — `@ai-sdk/amazon-bedrock` does not walk the
AWS default credential chain on its own.
- When `authType === 'role'`, wire `fromNodeProviderChain()` from
`@aws-sdk/credential-providers` (already a server dependency, used by
the S3 and logic-function drivers) into `createAmazonBedrock` so IRSA
and other ambient AWS credentials resolve correctly.
- The static-credentials path is unchanged for `authType !== 'role'`.

## Test plan
- [ ] Deploy a server pod with IRSA + an `ai-models-config` entry using
`"authType": "role"` and verify Bedrock calls succeed (no `Could not
load credentials from any providers` error).
- [ ] Verify static-credentials providers (`accessKeyId` +
`secretAccessKey`) still work as before.
- [ ] `npx nx lint:diff-with-main twenty-server` passes.
- [ ] `npx nx typecheck twenty-server` passes.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 22:54:35 +02:00
Félix Malfait
6073bb6706
Fix AI model registry staleness on self-hosted instances (#19427)
## Summary

Fixes #19422.

Self-hosted users hitting "No AI models are available" after configuring
API keys via the admin panel were victims of a stale
`AiModelRegistryService` cache. Only the four `addAiProvider` /
`removeAiProvider` / `addModelToProvider` / `removeModelFromProvider`
mutations called `refreshRegistry()` — setting an API key through
`set/update/deleteDatabaseConfigVariable` left the registry pointing at
the pre-mutation provider state.

Rather than patch each mutation site (and re-introduce the same class of
bug on the next one), the registry now invalidates lazily based on the
LLM config-group hash, mirroring the pattern `WebSearchDriverFactory`
already uses via `DriverFactoryBase`. Any mutation to an LLM-tagged
config variable is picked up automatically on the next read — callers
never have to remember to refresh.

- Extracted `getConfigGroupHash` into a shared util reused by both
`DriverFactoryBase` and `AiModelRegistryService` (and switched the hash
to `JSON.stringify` so object-typed config vars like `AI_PROVIDERS`
actually contribute meaningfully).
- `AiModelRegistryService` gates all internal `Map` access behind
private getters that call `ensureFresh()`, so future read paths can't
accidentally observe stale state. Build path uses underscored backing
fields directly to avoid recursing.
- Dropped the now-redundant `refreshRegistry()` public method and its
four call sites in `admin-panel.resolver.ts`.

## Test plan

- [x] `nx typecheck twenty-server` passes
- [x] Existing admin-panel + ai-models specs pass (79 tests)
- [ ] Manual: on a self-hosted instance, set `OPENAI_API_KEY` (or
another provider key) via the admin panel `setDatabaseConfigVariable`
mutation and confirm AI features work without a server restart
- [ ] Manual: existing flows (`addAiProvider`, `addModelToProvider`,
etc.) still pick up new providers on the next read

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 22:32:05 +02:00
github-actions[bot]
5874fb5f39
i18n - translations (#19467)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 18:50:23 +02:00
github-actions[bot]
37dbd94596
i18n - docs translations (#19466)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 18:46:25 +02:00
Weiko
238018dc7b
Reset Tab Page Layout (#19453)
## Context
- Add "Reset to default" for page layout tabs and widgets — backend
mutation resets overrides, reactivates deactivated children, and deletes
custom children
- Fix override write bug where mutating an overridable property (e.g.
widget title) on a standard-app entity incorrectly overwrote the base
column instead of writing to the overrides JSONB —
PageLayoutUpdateService now uses resolveFlatEntityOverridableProperties
for accurate diffing and routes properties through
sanitizeOverridableEntityInput
- Deprecate isOverridden - We need more time to think about this
feature. Currently this adds too much complexity for a very small
benefit



https://github.com/user-attachments/assets/a84546c8-1e15-4d9e-a489-0825cf8b8ed2
2026-04-08 18:43:57 +02:00
Raphaël Bosi
2267c56f15
[COMMAND MENU ITEMS] Disable hide label toggle for items with no short label (#19464)
<img width="824" height="1140" alt="CleanShot 2026-04-08 at 18 08 31@2x"
src="https://github.com/user-attachments/assets/67989cd7-ac0f-4c28-b128-6db50abecc88"
/>
2026-04-08 16:32:01 +00:00
Félix Malfait
7aed9291cd
fix(ai-chat): preload web_search action tool when driver is enabled (#19461)
## Summary
- When `WEB_SEARCH_DRIVER` is set to a non-native driver (e.g. `EXA`),
the `web_search` action tool was only listed in the tool catalog and had
to be discovered via `learn_tools` before use. Worse,
`system-prompt-builder` explicitly instructed the model **not** to call
`web_search` whenever it wasn't in the preloaded set, so even setting
`EXA_API_KEY` correctly resulted in the model never calling Exa.
- Fix: preload `web_search` from the registry alongside the other action
tools when `shouldUseNativeSearch()` is false, mirroring how native
provider search is already injected into `directTools`. The system
prompt builder picks this up automatically and advertises `web_search`
as a ready-to-use tool.
- Unrelated: pass `isSystemBuild: true` to the messageThread upgrade
command's migration build.

## Test plan
- [ ] With `WEB_SEARCH_DRIVER=EXA` and `EXA_API_KEY` set, ask the chat
agent for current/news information and verify it calls `web_search`
directly (not via `learn_tools` / `execute_tool`) and that Exa is hit.
- [ ] With `WEB_SEARCH_PREFER_NATIVE=true`, verify native provider
search is still used and the action tool is not preloaded.
- [ ] With `WEB_SEARCH_DRIVER=DISABLED`, verify behavior is unchanged
(`shouldUseNativeSearch()` returns true → no Exa preload).
- [ ] Run the 1-21 fix-message-thread-view-and-label-identifier upgrade
command on a workspace and confirm the migration builds/runs as a system
build.

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 18:22:27 +02:00
BOHEUS
5c867303e0
Fix documentation about importing dates (#19434)
Related to https://github.com/twentyhq/core-team-issues/issues/2364
2026-04-08 14:48:04 +00:00
neo773
fda3eabb5f
introduce MESSAGING_MESSAGES_GET_BATCH_SIZE as config variable (#19455)
makes`MESSAGING_MESSAGES_GET_BATCH_SIZE` configurable for self hosters

/closes #19147
2026-04-08 14:47:04 +00:00
Paul Rastoin
7d6111b0a5
Fix workspace command name inserted in db (#19458)
# Introduction
Persist in the db the registry computed name that contains
version_classname_timestamp to be consistent with the instance command
pattern too
2026-04-08 14:43:24 +00:00
Thomas Trompette
4df3539ba1
Fix: Staled data on record table after merging records (#19457)
Fix record table not refreshing after merging records

The record table's virtualization state persists across navigations, so
when the table remounts after a merge redirect, none of the existing
reload conditions were satisfied and the table rendered stale data.

Add an isInitializedOnMount local state to
RecordTableVirtualizedInitialDataLoadEffect that guarantees a fresh
triggerInitialRecordTableDataLoad on every mount, acting as a fallback
when no other reload condition matches.
2026-04-08 14:34:09 +00:00
Paul Rastoin
8905d860c7
Store upgrade commands error message (#19443)
# Introduction
Storing the failing upgrade command formatted message in database.
2026-04-08 13:21:18 +00:00
Charles Bochet
bc7b5aee58
chore: centralize deploy/install CD actions in twentyhq/twenty (#19454)
## Summary

- Adds `deploy-twenty-app` and `install-twenty-app` composite actions to
`.github/actions/` so app repos can reference them remotely — same
pattern as `spawn-twenty-app-dev-test` for CI
- Updates `cd.yml` in template, hello-world, and postcard to use
`twentyhq/twenty/.github/actions/deploy-twenty-app@main` /
`install-twenty-app@main` instead of local `./.github/actions/` copies
- Removes the 6 local action files that were duplicated across template
and example apps

**Before** (each app repo carried its own action copies):
```yaml
uses: ./.github/actions/deploy
```

**After** (centralized, like CI):
```yaml
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
```


Made with [Cursor](https://cursor.com)
2026-04-08 15:25:51 +02:00
Raphaël Bosi
31b6dbc583
[COMMAND MENU ITEMS] Remove object metadata name from search commands (#19435)
Command menu items label become "Search" instead of "Search companies"
or "Search people" since the search is made across all objects.
2026-04-08 13:15:26 +00:00
Etienne
3306d66f5b
cleaning - remove logs (#19445) 2026-04-08 13:05:47 +00:00
Charles Bochet
6cd3f2db2b
chore: replace spawn-twenty-app-dev-test with native postgres/redis services (#19449)
## Summary

- Replaces the `spawn-twenty-app-dev-test` Docker action with native
GitHub Actions services (`postgres:18` + `redis`) and direct server
startup (`npx nx start:ci twenty-server`)
- Aligns with the pattern already used by `ci-sdk.yaml` for e2e tests
- Removes dependency on the `twenty-app-dev` Docker image for CI

Updated workflows:
- `ci-example-app-postcard`
- `ci-example-app-hello-world`
- `ci-create-app-e2e-minimal`
- `ci-create-app-e2e-hello-world`
- `ci-create-app-e2e-postcard`

Server setup pattern:
1. Postgres 18 + Redis as job services
2. `CREATE DATABASE "test"` via psql
3. `npx nx run twenty-server:database:reset` (migrations + seed)
4. `nohup npx nx start:ci twenty-server &`
5. `npx wait-on http://localhost:3000/healthz`


Made with [Cursor](https://cursor.com)
2026-04-08 13:03:35 +00:00
Thomas des Francs
0e0fb246e6
Refactor the website hero into an interactive multi-view illustration (#19429)
## Summary
- replace the home hero visual with an interactive multi-view experience
for table, kanban, workflow, and dashboard states
- add the supporting hero data model, page normalizers, loaders, chips,
and sales dashboard assets
- update related website illustration components and ignore local Claude
worktrees in `.gitignore`

## Testing
- Not run (not requested)
2026-04-08 13:00:41 +00:00
Abdullah.
83917f0dca
Isolate illustrations from sections to style them independently. (#19447)
This PR moves illustrations from sections to a separate folder of their
own and accesses which illustration to load on a specific page using a
registry.

The reason for this change is that we want to be able to independently
style each illustration since shared styles being applied to nine
illustrations of ThreeCards, for example, does not get us the eventual
output visually that we're after because the underlying assets have
different lighting, angles etc.

Once we're at a position where we know what can be reused, I will
refactor. For now, Thomas would to take illustration styling to try and
implement what he has in mind, so isolating these files for him to work
without breaking anything.
2026-04-08 12:58:46 +00:00
github-actions[bot]
ac4819758d
i18n - translations (#19451)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 15:03:32 +02:00
Raphaël Bosi
d07c27a907
[COMMAND MENU ITEMS] Create union type for command menu item payload (#19432)
Replace generic JSON scalar with a typed GraphQL union
CommandMenuItemPayload (PathNavigationPayload |
ObjectMetadataNavigationPayload) for the CommandMenuItem.payload field
2026-04-08 12:47:34 +00:00
github-actions[bot]
265d859c6e
i18n - docs translations (#19446)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 14:42:19 +02:00
Charles Bochet
1ae88f4e4f
chore: add CD workflow template and point spawn action to main (#19430)
## Summary

- Adds reusable composite GitHub Actions for Twenty app deployment:
- `.github/actions/deploy` — builds and deploys to a remote instance
(`api-url`, `api-key` inputs)
- `.github/actions/install` — installs/upgrades on a specific workspace
(`api-url`, `api-key` inputs)
- Adds a `cd.yml` CD workflow that calls both actions in sequence. The
workflow:
  - Deploys on push to `main`
  - Can be triggered from a PR by adding a `deploy` label
- Configures a named remote via `TWENTY_DEPLOY_URL` env var and
`TWENTY_DEPLOY_API_KEY` secret
- Applied to: `create-twenty-app` template, `postcard` example,
`hello-world` example
- Updates the `spawn-twenty-app-dev-test` action ref from
`@feature/sdk-config-file-source-of-truth` to `@main` in all `ci.yml`
files
2026-04-08 12:31:38 +00:00
Paul Rastoin
85be463487
Slow instance commands (#19431)
# Introduction
This PR introduces the slow instance commands pattern, that allow
migrating data in prior of the schema migration, that would fail if not.

Slow instance commands runs after the fast instance commands and before
the workspace commands.
On twenty instance that do not has any active or suspended workspace the
data migration part is skipped but the migration still runs, especially
for fresh installs

We were previously hacking through typeorm transaction system to gain
such granularity using save points:
```ts
export class AddPayloadToCommandMenuItem1775129635528
  implements MigrationInterface
{
  name = 'AddPayloadToCommandMenuItem1775129635528';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `ALTER TABLE "core"."commandMenuItem" ADD "payload" jsonb`,
    );

    const savepointName =
      'sp_add_payload_check_constraint_to_command_menu_item';

    try {
      await queryRunner.query(`SAVEPOINT ${savepointName}`);

      await addPayloadCheckConstraintToCommandMenuItem(queryRunner);

      await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
    } catch (e) {
      try {
        await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
        await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
      } catch (rollbackError) {
        // oxlint-disable-next-line no-console
        console.error(
          'Failed to rollback to savepoint in AddPayloadToCommandMenuItem1775129635528',
          rollbackError,
        );
        throw rollbackError;
      }

      // oxlint-disable-next-line no-console
      console.error(
        'Swallowing AddPayloadToCommandMenuItem1775129635528 error',
        e,
      );
    }
  }
```

It was afterwards re-applied within an workspace commands, it was hacky
and missleading for the self host having false positive in logs

## New pattern

Generate the slow instance command
```
npx nx database:migrate:generate twenty-server -- --name add-foo-bar-columns --type slow
```

```ts
import { DataSource, QueryRunner } from 'typeorm';

import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';

@RegisteredInstanceCommand('1.21.0', 1775640902366, { type: 'slow' })
export class AddPrastoinColToWorkspaceSlowInstanceCommand implements SlowInstanceCommand {
  async runDataMigration(dataSource: DataSource): Promise<void> {
    // TODO: implement data backfill before the DDL migration
  }

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query('ALTER TABLE "core"."workspace" ADD "prastoin" character varying');
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query('ALTER TABLE "core"."workspace" DROP COLUMN "prastoin"');
  }
}
```

### Why `run-instance-commands` and `upgrade` remain separate commands

These two commands serve fundamentally different purposes with
incompatible scoping semantics:
The run-instance-commands iterates over all the legacy typeorm and the
instance commands of all versions, used for database init and so on. we
could be centralizing both but the readability tradeoff isn't worth it

In the future thanks to the cross-version pattern we will be able to
centralize them but not right now

Please note that by default the `run-instance-commands` only run the
fast instance commands which is expected for our cloud prod CD
2026-04-08 12:09:29 +00:00
BugIsGod
8a84e32cf6
fix(ai): use @ai-sdk/openai-compatible for third-party providers (#19438)
## Summary:

I found a bug: 404 call when I use third-party providers (I used
deepseek). I found the final request url is:
`https://api.deepseek.com/v1/responses' `if we use createOpenAI. But the
correct one should be `https://api.provider.com/v1/chat/completions` for
the thirty provider. So I replace createOpenAI with
createOpenAICompatible in the openai-compatible provider path.

**Reproduction**: Add DeepSeek as a new AI provider (deepseek-chat,
deepseek-reasoner)

Also check the source code in Open Code project, they also use
createOpenAICompatible for the @ai-sdk/openai-compatible scenario
<img width="703" height="369" alt="image"
src="https://github.com/user-attachments/assets/90c4f924-6f1a-4fe7-821b-f13ee86a7a39"
/>


official docs: https://ai-sdk.dev/providers/openai-compatible-providers
https://ai-sdk.dev/providers/ai-sdk-providers/openai

<img width="860" height="293" alt="image"
src="https://github.com/user-attachments/assets/283b9b91-f1d6-4b1c-bb91-16ddccdff8b4"
/>



## Before
<img width="473" height="750" alt="deepseek before"
src="https://github.com/user-attachments/assets/f6b89294-1fa7-4ddc-a6a8-d396070caaca"
/>

## After

<img width="468" height="753" alt="deepseek after"
src="https://github.com/user-attachments/assets/0d170b70-e829-4a4c-abad-38879427c2df"
/>

## Backend error log

```json
[1] [Nest] 50907  - 07/04/2026, 23:48:57     LOG [ChatExecutionService] Starting chat execution with model deepseek/deepseek-chat, 4 active tools
[1] APICallError [AI_APICallError]: Not Found
[1]     at /Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/provider-utils/dist/index.js:2512:14
[1]     at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
[1]     at async postToApi (/Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/provider-utils/dist/index.js:2373:28)
[1]     at async OpenAIResponsesLanguageModel.doStream (/Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/openai/dist/index.js:4925:50)
[1]     at async fn (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7106:27)
[1]     at async /Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2340:24
[1]     at async _retryWithExponentialBackoff (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2599:12)
[1]     at async streamStep (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7063:17)
[1]     at async fn (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7455:9)
[1]     at async /Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2340:24 {
[1]   cause: undefined,
[1]   url: 'https://api.deepseek.com/v1/responses',
[1]   requestBodyValues: {
[1]     model: 'deepseek-chat',
[1]     input: [ [Object], [Object] ],
[1]     temperature: undefined,
[1]     top_p: undefined,
[1]     max_output_tokens: undefined,
[1]     conversation: undefined,
[1]     max_tool_calls: undefined,
[1]     metadata: undefined,
[1]     parallel_tool_calls: undefined,
[1]     previous_response_id: undefined,
[1]     store: undefined,
[1]     user: undefined,
[1]     instructions: undefined,
[1]     service_tier: undefined,
[1]     include: undefined,
[1]     prompt_cache_key: undefined,
[1]     prompt_cache_retention: undefined,
[1]     safety_identifier: undefined,
[1]     top_logprobs: undefined,
[1]     truncation: undefined,
[1]     tools: [ [Object], [Object], [Object], [Object] ],
[1]     tool_choice: 'auto',
[1]     stream: true
[1]   },
[1]   statusCode: 404,
[1]   responseHeaders: {
[1]     'access-control-allow-credentials': 'true',
[1]     connection: 'keep-alive',
[1]     'content-length': '0',
[1]     date: 'Tue, 07 Apr 2026 22:48:57 GMT',
[1]     server: 'elb',
[1]     'strict-transport-security': 'max-age=31536000; includeSubDomains; preload',
[1]     vary: 'origin, access-control-request-method, access-control-request-headers',
[1]     via: '1.1 83867089cd39052cd05f9e04909bedde.cloudfront.net (CloudFront)',
[1]     'x-amz-cf-id': 'O4b0VTi9Q1VVmTmq6czGlEWst7IPnAQl544hB7uIvfnSphBvUKbZTw==',
[1]     'x-amz-cf-pop': 'DUB56-P3',
[1]     'x-cache': 'Error from cloudfront',
[1]     'x-content-type-options': 'nosniff',
[1]     'x-ds-trace-id': 'a90e91809d285170338ef077f67ae2be'
[1]   },
[1]   responseBody: '',
[1]   isRetryable: false,
[1]   data: undefined,
[1]   Symbol(vercel.ai.error): true,
[1]   Symbol(vercel.ai.error.AI_APICallError): true
[1] }
[1] Exception Captured
[1]   undefined
[1]   [
[1]     NoOutputGeneratedError [AI_NoOutputGeneratedError]: No output generated. Check the stream for errors.
[1]         at Object.flush (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:6656:103)
[1]         at invokePromiseCallback (node:internal/webstreams/util:172:10)
[1]         at Object.<anonymous> (node:internal/webstreams/util:177:23)
[1]         at transformStreamDefaultSinkCloseAlgorithm (node:internal/webstreams/transformstream:621:43)
[1]         at node:internal/webstreams/transformstream:379:11
[1]         at writableStreamDefaultControllerProcessClose (node:internal/webstreams/writablestream:1162:28)
[1]         at writableStreamDefaultControllerAdvanceQueueIfNeeded (node:internal/webstreams/writablestream:1253:5)
[1]         at writableStreamDefaultControllerClose (node:internal/webstreams/writablestream:1220:3)
[1]         at writableStreamClose (node:internal/webstreams/writablestream:722:3)
[1]         at writableStreamDefaultWriterClose (node:internal/webstreams/writablestream:1091:10) {
[1]       cause: undefined,
[1]       Symbol(vercel.ai.error): true,
[1]       Symbol(vercel.ai.error.AI_NoOutputGeneratedError): true
[1]     }
[1]   ]
[1] [Nest] 50907  - 07/04/2026, 23:48:59     LOG [BullMQDriver] Job 24 with name StreamAgentChatJob processed on queue ai-stream-queue in 2756.63ms
2026-04-08 12:01:49 +00:00
Weiko
8aa208fc93
Fix overridable entities logic for SSE (#19433)
## Context
SSE metadata events for overridable entities (viewField, viewFieldGroup,
pageLayoutWidget, pageLayoutTab) were sending raw flat entity data
without resolving overrides into base properties or filtering isActive:
false entities. This caused the frontend to display stale/incorrect
values (e.g., a hidden viewField still appearing visible).

## Implementation
Add a sanitization step in
MetadataEventPublisher.enrichMetadataEventBatch that resolves overrides
and converts isActive transitions into the appropriate event types
(deactivated -> delete, reactivated -> create), matching what GraphQL
resolvers already do
2026-04-08 11:53:55 +00:00
Thomas Trompette
9a58f3d459
Add a lock on function creation (#19428)
Fixes
https://twenty-v7.sentry.io/issues/7368127776/?environment=prod&project=4507072499810304&query=is%3Aunresolved&referrer=issue-stream

- Add a distributed lock (via CacheLockService) around the Lambda
executor build in LambdaDriver to prevent concurrent workflow runs from
racing on CreateFunctionCommand for the same logic function
- Use double-checked locking: isAlreadyBuilt is checked lock-free first
(fast path), then re-checked inside the lock to avoid redundant rebuilds
2026-04-08 11:50:57 +00:00
Baptiste Devessier
9d96e529c8
Fix last widget bottom bar inconsistencies (#19440)
Widgets weren't sorted
2026-04-08 11:29:11 +00:00
Baptiste Devessier
e9310555fb
Fix tab selection in layout edit mode (#19436)
https://github.com/user-attachments/assets/20e4a506-5f22-4017-8300-a494b70e8d51
2026-04-08 11:12:46 +00:00
github-actions[bot]
8090fa4364
i18n - docs translations (#19437)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 12:40:41 +02:00
martmull
90de1d4a34
Add twenty sync command (#19413)
Add one shot app synchronisation `twenty sync command` command

Complementary with `twenty app dev` command which is watch mode

Fixes
https://discord.com/channels/1130383047699738754/1489644493106839663
2026-04-08 09:26:42 +00:00
github-actions[bot]
8026451220
chore: sync AI model catalog from models.dev (#19426)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-08 08:27:14 +02:00
Arturo Mantinetti
b3d46b0fa3
feat: Add support for CLF currency code (#19420)
## Summary
This PR adds support for the **CLF (Unidad de Fomento)** currency code
across the application.

## Changes
- Added `CLF` to the supported currency list
- Updated validation logic to recognize CLF as a valid currency
- Adjusted formatting and handling where applicable

## Motivation
CLF is a widely used unit of account in Chile, commonly used for
financial operations such as real estate, contracts, and indexed
payments.
Supporting CLF improves localization and enables better adoption of
Twenty CRM in the Chilean market.

## Files Modified
- Updated 3 files to integrate CLF support (currency configuration,
validation, and related logic)

## Testing
- Verified that CLF can be selected and processed correctly
- Confirmed no regression in existing currency behavior
2026-04-08 05:10:36 +00:00
github-actions[bot]
a8085db5fd
i18n - translations (#19425)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 07:09:03 +02:00
github-actions[bot]
b540ae9735
i18n - translations (#19424)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 07:05:51 +02:00
neo773
20adb86917
fix: IMAP skip no-select flag folders properly (#19402)
Tested with a dovecot server running in Docker with synthetic seed

<img width="546" height="54" alt="image"
src="https://github.com/user-attachments/assets/81cbeae6-9cb6-406b-846a-209af403385f"
/>


/closes #19090
2026-04-08 04:54:01 +00:00
martmull
c91d642f29
App feedbacks front (#19417)
## Before
<img width="777" height="284" alt="image"
src="https://github.com/user-attachments/assets/22a8c318-ed7c-4ec5-b7a8-a688098ec103"
/>


## After
<img width="897" height="329" alt="image"
src="https://github.com/user-attachments/assets/16ad78bf-a449-4f25-99f6-13c35e448f1b"
/>
2026-04-08 04:51:17 +00:00
Charles Bochet
15eb3e7edc
feat(sdk): use config file as single source of truth, remove env var fallbacks (#19409)
## Summary

- **Config as source of truth**: `~/.twenty/config.json` is now the
single source of truth for SDK authentication — env var fallbacks have
been removed from the config resolution chain.
- **Test instance support**: `twenty server start --test` spins up a
dedicated Docker instance on port 2021 with its own config
(`config.test.json`), so integration tests don't interfere with the dev
environment.
- **API key auth for marketplace**: Removed `UserAuthGuard` from
`MarketplaceResolver` so API key tokens (workspace-scoped) can call
`installMarketplaceApp`.
- **CI for example apps**: Added monorepo CI workflows for `hello-world`
and `postcard` example apps to catch regressions.
- **Simplified CI**: All `ci-create-app-e2e` and example app workflows
now use a shared `spawn-twenty-app-dev-test` action (Docker-based)
instead of building the server from source. Consolidated auth env vars
to `TWENTY_API_URL` + `TWENTY_API_KEY`.
- **Template publishing fix**: `create-twenty-app` template now
correctly preserves `.github/` and `.gitignore` through npm publish
(stored without leading dot, renamed after copy).

## Test plan

- [x] CI SDK (lint, typecheck, unit, integration, e2e) — all green
- [x] CI Example App Hello World — green
- [x] CI Example App Postcard — green
- [x] CI Create App E2E minimal — green
- [x] CI Front, CI Server, CI Shared — green
2026-04-08 06:49:10 +02:00
github-actions[bot]
af3423dc6f
i18n - translations (#19421)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 03:42:20 +02:00
Abdul Rahman
a0484f686a
Add color to object icon picker in data model (#19368)
Closes [#2291](https://github.com/twentyhq/core-team-issues/issues/2291)
2026-04-08 01:27:08 +00:00
martmull
5c8b6e395b
App feedbacks front (#19416)
- Removes agents from app settings
https://discord.com/channels/1130383047699738754/1488500960656490618
- fix public assets not properly pack in deploy and build commands
-
2026-04-07 19:49:02 +00:00
Félix Malfait
9bdef449e6
Fix messageThread view and labelIdentifier on legacy workspaces (#19414)
## Summary
Adds `upgrade:1-21:fix-message-thread-view-and-label-identifier` to
retroactively apply two messageThread changes from #19351 that never
propagated to existing workspaces (because
`synchronizeTwentyStandardApplicationOrThrow` only runs at workspace
creation):

1. **`allMessageThreads` view fields**: delete-and-recreate all view
fields for the standard view from the current twenty-standard
definition, which adds the new `subject` and `updatedAt` columns. Same
pattern as the FIELDS_WIDGET sync in
`1-21-workspace-command-1775500005000-backfill-page-layouts-and-fields-widget-view-fields`.
2. **`messageThread.labelIdentifierFieldMetadataId`**: repointed to the
`subject` field via `validateBuildAndRunWorkspaceMigration`. The
migration runner already resolves
`labelIdentifierFieldMetadataUniversalIdentifier` → id in
`update-object-action-handler`, so this goes through the standard flow
and properly invalidates caches.

Both fixes share a single `validateBuildAndRunWorkspaceMigration` call
(one `viewField` op, one `objectMetadata` update op). Idempotent — skips
if nothing to do.

Depends on `upgrade:1-21:backfill-message-thread-subject` having run
first (the label identifier fix needs the `subject` field to exist; it
logs a warning and skips that part otherwise).

## Test plan
- [ ] Legacy workspace: run \`backfill-message-thread-subject\`, then
this command, then verify:
- the \`allMessageThreads\` view shows the new \`subject\` and
\`updatedAt\` columns
  - messageThread records display their \`subject\` as the record label
- [ ] Re-run the command: no-op, logs \`Nothing to fix\`
- [ ] \`--dry-run\` logs planned changes without applying them

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 21:57:35 +02:00
Baptiste Devessier
e2bd3e8ef4
Unselect widget when exiting layout customization mode (#19411) 2026-04-07 17:26:07 +00:00
Paul Rastoin
c0eacedfec
Workspace command decorators (#19397)
# Introduction
Migrating the workspace commands to the decorator version + timestamp
listing as for the instance commands

We've now been able to remove the upgrade command abstraction where we
needed to import all modules and order them
Now they're dynamically retrieved at upgrade runtime, sorted by
timestamp

## Instance and workspace commands name
The name is computed from the command metadata `version` `className` and
`timestamp` we have a duplicate validation at module init from the
unified registry
2026-04-07 16:33:48 +00:00
github-actions[bot]
24a5273a79
i18n - docs translations (#19410)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 18:36:49 +02:00
Raphaël Bosi
607e708670
[COMMAND MENU ITEMS] Add navigate to settings pages commands (#19408)
Add commands to navigate to every settings page.
2026-04-07 16:06:40 +00:00
neo773
61abfd103d
fix TextVariableEditor layout and add support for multi line paste (#18721)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-07 15:58:42 +00:00
neo773
54be3b7e87
docs: remove reference to sync metadata (#19400) 2026-04-07 15:55:23 +00:00
Paul Rastoin
137e068ced
Fix yarn-install-lambda and lambda-build target (#19407)
# Size issue
```
Yarn install Lambda failed: {"errorType":"Error","errorMessage":"yarn install failed: ➤ Y/tmp/3bac8baafe6354db414389434726b02c/nodejs/node_modules/tar ENOSPC: no space left on device, write","➤ YN0000: └ Completed in 3s 57ms","➤ YN0000: · Failed with errors in 11s 684ms",""," at runYarnInstall (file:///var/task/index.mjs:48:11)"," at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"," at async Runtime.handler (file:///var/task/index.mjs:119:5)"]}
```

# target esnext
setting the same target for each logic function build funnels
- sdk
- local driver
- lambda driver
2026-04-07 15:53:31 +00:00
Thomas des Francs
91f8f7329a
improve pricing card header [website] (#19385)
## Summary
- Reduce the plan title size from `md` to `xs`
- Switch the pricing card header layout from grid to flex for tighter
title/price control
- Tighten the title line-height and add a `black/60` color override for
the `/month...` suffix
- Add a 4px gap between the price amount and suffix
- Reduce the illustration height to 80px and shift it slightly right on
desktop

## Testing
- Not run (not requested)

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-07 15:02:48 +00:00
Raphaël Bosi
d341d0d624
Refactor navigation commands to use NAVIGATION engine key with payload (#19303)
- Adds a payload JSON column to `CommandMenuItem` and introduces a
unified `NAVIGATION` engine component key that replaces all individual
GO_TO_* keys
- Navigation commands now use the payload to determine their target
(either an objectMetadataItemId or a path), making navigation commands
dynamic and eliminating the need for a hardcoded engine key per object
- Includes a 1.21 upgrade command (refactor-navigation-commands) that
migrates existing GO_TO_* items to NAVIGATION items with the appropriate
payload, and applies a CHECK constraint enforcing payload coherence



https://github.com/user-attachments/assets/4d305ba2-ae0b-4556-bb0e-e9d899777350


TODO: In a second PR, create the sync between object metadata items and
the navigation command menu items
- Object metadata item created or enabled -> Create navigation command
- Object metadata item deleted or disabled -> Delete associated
navigation command

In another PR:
- Allow `label`, `shortLabel` and `icon` to resolve the
`navigateToObjectMetadataItem` dynamically in their interpolation
instead of being hardcoded in the command menu item
- Make the icon dynamic in the command menu items as the label so that
we can resolve ${navigateToObjectMetadataItem.icon} at runTime -> This
way we won't need to keep update the command menu item icon when we
update the objectMetadataItem icon
2026-04-07 15:00:04 +00:00
github-actions[bot]
5e9792009f
i18n - docs translations (#19405)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 16:51:49 +02:00
Abdullah.
38802bd0b8
Style the remaining website visuals. (#19401)
This PR styles the remaining website visuals. First implementation.
2026-04-07 14:33:54 +00:00
github-actions[bot]
e692e97428
i18n - translations (#19404)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 16:37:15 +02:00
Baptiste Devessier
1f3965e5f8
Add Manage and Placement sections in widget side panel page for record page layouts (#19310)
https://github.com/user-attachments/assets/f6120c2e-95e7-4b9b-abb5-69a10c3f2f3b
2026-04-07 14:21:35 +00:00
Thomas Trompette
e048d03872
Improve workflow crons efficiency (#19381)
**Optimize workflow cron jobs: partition workspaces and use raw
queries**

- Split all 3 workflow cron jobs (WorkflowRunEnqueueCronJob,
WorkflowHandleStaledRunsCronJob, WorkflowCleanWorkflowRunsCronJob) to
process only 1/10th of workspaces per invocation using minute-based
partitioning, reducing per-run load
- Replace ORM repository + workspace context loading with raw SQL
queries in WorkflowRunEnqueueCronJob and
WorkflowHandleStaledRunsCronJob, avoiding costly cache/metadata
hydration for a simple existence check
2026-04-07 14:09:11 +00:00
github-actions[bot]
653180d10f
i18n - translations (#19403)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 16:10:37 +02:00
martmull
8702300b07
App feedbacks fix option id required in apps (#19386)
fixes
https://discord.com/channels/1130383047699738754/1488226371032453292
2026-04-07 13:53:16 +00:00
Weiko
6e23ca35e6
Pagelayout backfill command standard app (#19380)
## Context
Due to the chosen strategy for "Reset to default" feature for page
layouts. Those overridable entities need to be associated to the
Standard app to work properly (there is no "Default" state for custom
entities). Until we find a better implementation, I'm changing the
backfill command to reflect that
2026-04-07 12:49:35 +00:00
Félix Malfait
96a242eb7d
fix(messaging): create messageThread.subject field metadata + column in 1-21 backfill (#19394)
## Summary
- The 1-21 \`upgrade:1-21:backfill-message-thread-subject\` command
assumed the legacy \`sync-metadata\` flow would create the new
\`messageThread.subject\` field metadata and column on existing
workspaces. That sync was removed, so the column was never added and the
backfill silently skipped.
- The command now ensures the field exists by computing the standard
\`messageThread.subject\` flat field from the twenty-standard
application and running it through
\`WorkspaceMigrationValidateBuildAndRunService\` (same pattern used by
the page-layout / command-menu-item backfills). This creates both the
field metadata row and the workspace schema column.
- After ensuring the field, the existing \`UPDATE messageThread SET
subject = ...\` runs as before.

## Test plan
- [ ] On a workspace with no \`subject\` column on \`messageThread\`,
run \`yarn command:prod upgrade:1-21:backfill-message-thread-subject\`
and confirm:
  - the field metadata row is created in \`core.\"fieldMetadata\"\`
- the \`subject\` column is created on \`workspace_<id>.messageThread\`
  - existing message threads are backfilled from the most recent message
- [ ] Re-run on the same workspace and confirm it is a no-op (field
already exists, no rows to update)
- [ ] Run on a workspace that already has the column but \`NULL\`
subjects and confirm only the backfill runs

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 14:42:20 +02:00
github-actions[bot]
d97a1cfc27
i18n - docs translations (#19399)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 14:39:30 +02:00
github-actions[bot]
c844109ffc
i18n - translations (#19398)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 14:35:24 +02:00
neo773
dd2a09576b
Imap-smtp-caldav form fixes (#19392)
/closes #19273
2026-04-07 12:18:11 +00:00
martmull
fe07de63b0
Enterprise plan required for private app sharing (#19393)
## before

<img width="1512" height="696" alt="image"
src="https://github.com/user-attachments/assets/d57f398e-6ac3-4ce0-a54b-a23ae41b1890"
/>


## after

<img width="923" height="448" alt="image"
src="https://github.com/user-attachments/assets/f4fea42e-1019-4287-9302-42da143ee878"
/>
2026-04-07 12:17:59 +00:00
Paul Rastoin
1c9fc94c1f
Workspace commands writes inupgradeMigration (#19379)
# Introduction
As for the instance commands we want to keep a track of what has been
run for the workspace commands
Note that the history will be updated only when the workspace command
has been run through the upgrade directly and not when run atomically

## What's next
Later we will use this history in order to determine the current
workspace's version and instance's version getting rid of the version in
database, that will be the last stone
2026-04-07 12:15:06 +00:00
github-actions[bot]
d10ef156c1
i18n - translations (#19395)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 14:17:20 +02:00
nitin
d3ec64072b
[AI] Improve AI System Prompt page layout and token display (#19391)
closes
https://discord.com/channels/1130383047699738754/1480982048050249900

<img width="1438" height="1315" alt="CleanShot 2026-04-07 at 17 02 09"
src="https://github.com/user-attachments/assets/43c5a16f-6dd0-49da-8138-a11a7476e612"
/>
2026-04-07 11:52:48 +00:00
Etienne
1b14e7e1f1
Fix - Update package.json (#19390)
https://github.com/twentyhq/twenty/pull/19383#discussion_r3044330450
2026-04-07 11:44:23 +00:00
neo773
6ad9566043
Add messaging upgrade command 1 21 (#19389)
```
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [WorkspaceIteratorService] Running on workspace 20202020-1c25-4d02-bf25-6aeccf7ea419 1/2
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 5 connected accounts for workspace 20202020-1c25-4d02-bf25-6aeccf7ea419
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 6 message channels for workspace 20202020-1c25-4d02-bf25-6aeccf7ea419
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 6 calendar channels for workspace 20202020-1c25-4d02-bf25-6aeccf7ea419
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 5 message folders for workspace 20202020-1c25-4d02-bf25-6aeccf7ea419
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [WorkspaceIteratorService] Running on workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db 2/2
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 5 connected accounts for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Nest] 50057  - 04/07/2026, 4:14:11 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 4 message channels for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Nest] 50057  - 04/07/2026, 4:14:11 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 4 calendar channels for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Nest] 50057  - 04/07/2026, 4:14:11 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 10 message folders for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Nest] 50057  - 04/07/2026, 4:14:11 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Command completed!
```
2026-04-07 11:21:27 +00:00
Félix Malfait
f39fccc3c4
fix(messaging): split thread create/update statements and stop clobbering accumulator (#19388)
## Summary

Fixes two bugs in
`MessagingMessageService.saveMessagesWithinTransaction`.

### Bug 1 — `ON CONFLICT DO UPDATE command cannot affect row a second
time`

Reported in production logs from `MessagingMessagesImportService`.
Postgres rejects a single `INSERT … ON CONFLICT DO UPDATE` when the same
conflict target appears twice in the values list, and that's exactly
what was happening to `messageThread`.

Where it came from: #19351 added the message-thread subject refresh
feature and, in doing so, switched the existing thread `insert` to a
bulk `upsert(['id'])` over a list built by concatenating two sources:

```ts
const threadsToUpsert = [
  ...messageThreadsToCreate,        // brand-new thread rows
  ...threadSubjectUpdates entries,  // subject refreshes for existing threads
];
await messageThreadRepository.upsert(threadsToUpsert, ['id'], txManager);
```

Each list is internally unique, but they are **not disjoint**.
`enrichMessageAccumulatorWithMessageThreadToCreate`, when it sees two
messages in the same batch sharing a brand-new thread external id,
copies the first sibling's freshly-minted thread id into the second
sibling's `existingThreadInDB`. The subject-update gate later in the
loop then trusts that field and queues a subject refresh for that id —
which is also already in `messageThreadsToCreate`. Same id, same
statement, two rows → Postgres aborts the transaction and the import
retries forever on the same batch.

**Fix:** stop merging the two lists. Issue creates and subject updates
as two separate statements within the same transaction:

```ts
if (messageThreadsToCreate.length > 0) {
  await messageThreadRepository.insert(messageThreadsToCreate, txManager);
}
if (threadSubjectUpdates.size > 0) {
  await messageThreadRepository.upsert(
    Array.from(threadSubjectUpdates.entries()).map(([id, { subject }]) => ({ id, subject })),
    ['id'],
    txManager,
  );
}
```

This is closer to the pre-#19351 shape (`insert` for new rows) and
side-steps the duplicate-row constraint entirely: each statement is
internally unique (creates use freshly minted UUIDs; updates are keyed
by a `Map<id, …>`), and within the same transaction Postgres happily
applies a subject update to a row inserted by a previous statement.

### Bug 2 —
`enrichMessageAccumulatorWithExistingMessageChannelMessageAssociations`
clobbers the accumulator

Independent latent bug spotted while tracing the flow. The helper did:

```ts
if (existingMessageChannelMessageAssociation) {
  messageAccumulatorMap.set(message.externalId, {
    existingMessageInDB: existingMessage,
    existingMessageChannelMessageAssociationInDB: existingMessageChannelMessageAssociation,
  });
}
```

i.e. it **replaces** the accumulator object, dropping the
`existingThreadInDB` set just before by
`enrichMessageAccumulatorWithExistingMessageThreadIds`.

The branch only fires when re-encountering a message that's already been
fully synced on this channel (matched on `headerMessageId` AND already
has an association row) — i.e. routinely on Gmail/IMAP incremental syncs
whenever the connector re-delivers an existing message (label change,
read/unread, archive, full-resync after error, …).

When it fires, the next enrichment step sees `existingThreadInDB` as
`undefined`, falls into the "create a new thread" branch, mints a fresh
`threadToCreate`, but the main loop never queues a `messageToCreate` or
association for it (because both `existingMessageInDB` and the existing
association are still set). Net effect: **one orphan `messageThread` row
inserted per re-encountered message, with nothing referencing it.**

The existing message in the DB keeps pointing at its real thread, so
this is invisible to users — no thread fragmentation, no UI symptoms, no
error logs. Just slow accumulation of orphan thread rows that no query
joins onto. Probably worth running

```sql
SELECT COUNT(*)
FROM "messageThread" mt
WHERE NOT EXISTS (
  SELECT 1 FROM message m WHERE m."messageThreadId" = mt.id
);
```

on a busy production workspace once this lands to size whether a cleanup
migration is warranted.

**Fix:** mutate the existing accumulator in place instead of replacing
it.

## Test plan

- [x] `oxlint --type-aware` clean on touched file
- [x] `prettier` clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-07 13:23:31 +02:00
Etienne
ac1ec91f25
Direct execution - Remove conditional schema (#19383) 2026-04-07 10:22:56 +00:00
Abdullah.
d324bbfc25
Styled illustrations for Hero, ThreeCards, Helped sections. (#19387)
This PR adds styles and animations to illustrations for Hero, ThreeCards
and Helped sections.
2026-04-07 10:17:32 +00:00
eason
7c2a9abed4
fix: prevent NaN in health indicator calculations (#19378)
## Summary

Fixes #19377

- **Redis health**: The hit rate calculation divides by zero when both
`keyspace_hits` and `keyspace_misses` are `"0"` (common on fresh
instances). The string `"0"` is truthy so the guard
`statsData.keyspace_hits ? ...` doesn't catch this case, resulting in
`0/0 = NaN`. Fixed by computing the total first and checking it's a
valid non-zero number.
- **Database health**: The cache hit ratio query returns `null` when
`pg_statio_user_tables` is empty (no user tables). `parseFloat(null)` →
`NaN`. Fixed by adding a null check.

## Test plan

- [ ] Verify health indicators display correctly on a fresh instance
with no Redis keyspace activity
- [ ] Verify health indicators display correctly on a database with no
user tables
- [ ] Existing tests in `redis.health.spec.ts` still pass

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

---------

Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-07 10:01:49 +00:00
github-actions[bot]
c0c0cbb896
i18n - translations (#19382)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 11:50:21 +02:00
Weiko
a2188cb0eb
Reset Fields widget implementation (#19283)
## Context
This PR implements the first steps for overridable entities resets.

<img width="1277" height="568" alt="Screenshot 2026-04-02 at 18 58 04"
src="https://github.com/user-attachments/assets/4c7f93b1-c453-4905-a919-cd6af11e0e16"
/>
2026-04-07 09:34:44 +00:00
Paul Rastoin
23874848a4
Instance commands and upgrade_migrations table (#19356)
# Introduction
Now only using typeorm to generate migrations up and down statement
We handle and maintain our own migration table history

## What's new
Now all the instance commands will live within the same module and
folder than the upgrade commands
Sequentiality comes from the timestamp located in the filename
Same sequentiality also applies to the workspace commands in the future,
for the moment still expected a as code explicit declaration

( below screen is an example see below section )
<img width="1382" height="634" alt="image"
src="https://github.com/user-attachments/assets/5610a246-4eae-485e-99f4-98fb89ad5ac8"
/>

## Existing 1.21 migrations
We won't start following this pattern in 1.21 yet at least not with the
migration that has already been released as typeorm migrations in cloud
production as they would rerun


## Small duplication
Duplicating the legacy typeorm and instance commands run in the
`run-instance-commands` to avoid any merge of interest for the moment

## Concurrency
Not handling any run in parrallel of the upgrade for the moment
2026-04-07 08:55:17 +00:00
neo773
b23d2b4e73
messaging post migration cleanup (#19365)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-07 10:56:49 +02:00
Paul Rastoin
601dc02ed7
Fix s3 driver empty objects (#19361)
`undefined === 0` breaks the early return
2026-04-07 08:20:56 +00:00
Abdullah.
2d552fc9fd
Remove hover and scroll transitions from website. (#19369)
This PR removes hover and scroll transitions from the website. 

Per the conversation with Thomas, we should introduce them one by one
for each section.
2026-04-07 07:58:40 +00:00
Paul Rastoin
f33ad53e72
chore: remove registeredCoreMigration (#19376)
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-04-07 10:02:07 +02:00
Félix Malfait
7053e1bbc5
fix: bypass permission checks in 1.21 backfill-message-thread-subject command (#19375)
## Summary
- `upgrade:1-21:backfill-message-thread-subject` was failing on every
workspace with `Method not allowed because permissions are not
implemented at datasource level`.
- The global workspace datasource gates raw `query()` calls behind
`shouldBypassPermissionChecks`. Both the column-existence probe and the
UPDATE in this command now pass that flag, matching the pattern used by
the other 1.20/1.21 upgrade commands.

## Test plan
- [ ] Re-run `yarn command:prod
upgrade:1-21:backfill-message-thread-subject` and confirm all workspaces
complete without the permissions error
- [ ] Spot-check a workspace to confirm `messageThread.subject` is
backfilled from the most recent message

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 09:59:30 +02:00
Félix Malfait
955aa9191f
fix: unify settings layout prep when entering settings from outside (#19373)
## Summary

Clicking **Compose** in the emails tab without a connected account
redirects to the New Account settings page, but the settings nav drawer
was left in its previous (collapsed / "main") state — producing a
visibly half-broken transition.

### Root cause

The "enter settings" preparation (memorize previous URL + drawer state,
expand the desktop drawer, switch the mobile drawer to `'settings'`) was
duplicated **inline in three different places**:
- `NavigationDrawerOtherSection.handleSettingsClick`
- `MultiWorkspaceDropdownDefaultComponents` Settings link
- Implicitly expected (but missing) from every `useNavigateSettings`
caller

Every other entry point — `ComposeEmailButton`, `ComposeEmailCommand`,
`AIChatCreditsExhaustedMessage`, several workflow/role components — just
called `navigateSettings(...)` and skipped the prep entirely,
reproducing the bug.

### Fix

- Move the full prep into `useOpenSettingsMenu`, with a
`useIsSettingsPage()` short-circuit so internal navigation doesn't
clobber the memorized return target.
- `useNavigateSettings` delegates to `openSettingsMenu()` before
navigating — fixing every caller in one place.
- Collapse the duplicated inline logic in `NavigationDrawerOtherSection`
and `MultiWorkspaceDropdownDefaultComponents` to a single call.

Net **−9 lines**, single source of truth, no behavior change for the
existing happy paths.

## Test plan

- [x] \`nx typecheck twenty-front\` passes
- [x] \`oxlint\` + \`prettier\` clean on all 4 changed files
- [x] Existing \`useNavigateSettings\` tests pass (4/4)
- [ ] Manual: Compose button on a Person/Company/Opportunity emails tab
with no connected account → settings drawer renders fully expanded,
"Exit Settings" returns to the record
- [ ] Manual: "Settings" entry in the main nav drawer still works
(return path memorized)
- [ ] Manual: "Settings" entry in the multi-workspace dropdown still
works, and right-click → open in new tab still works (kept
\`UndecoratedLink\`)
- [ ] Manual: Navigating between settings pages does not overwrite the
memorized return URL

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 09:50:31 +02:00
oniani1
3f87d27d5d
fix: use AND instead of OR in neq filter for null-equivalent values (#19071)
Fixes #19070

The `neq` operator in `compute-where-condition-parts.ts` uses `OR` where
it should use `AND` when handling null-equivalent values.

Currently generates:
```sql
field != '' OR field IS NOT NULL
```

For a row where `field = ''`:
- `'' != ''` = false
- `'' IS NOT NULL` = true
- `false OR true` = true -- row incorrectly passes the filter

The `eq` operator correctly uses `OR field IS NULL` because it's
additive (match value or its null equivalent). By De Morgan's law, the
negation `neq` needs `AND field IS NOT NULL` -- exclude if the value
doesn't match AND is not a null equivalent.

With the fix:
```sql
field != '' AND field IS NOT NULL
```
- `'' != ''` = false, `'' IS NOT NULL` = true, `false AND true` = false
-- correctly excluded
- `NULL != ''` = NULL, `NULL IS NOT NULL` = false, `NULL AND false` =
false -- correctly excluded
- `'Alice' != ''` = true, `'Alice' IS NOT NULL` = true, `true AND true`
= true -- correctly included

Affects `neq` filters on TEXT fields and all composite sub-fields
(firstName, lastName, primaryEmail, primaryPhoneNumber, address
sub-fields, etc.) when filtering against null-equivalent values.

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-04-07 07:36:41 +00:00
Abdullah.
68cd2f6d61
fix: node-tar symlink path traversal via drive-relative linkpath (#19360)
Resolves [Dependabot Alert
619](https://github.com/twentyhq/twenty/security/dependabot/619) and
[Dependabot Alert
629](https://github.com/twentyhq/twenty/security/dependabot/629).
2026-04-07 07:15:55 +00:00
Abdullah.
8c9228cb2b
fix: SVGO DoS through entity expansion in DOCTYPE (#19359)
Resolves [Dependabot Alert
604](https://github.com/twentyhq/twenty/security/dependabot/604) and
[Dependabot Alert
605](https://github.com/twentyhq/twenty/security/dependabot/605).
2026-04-07 07:15:35 +00:00
Abdullah.
35b76539cc
fix: minimatch related dependabot alerts. (#19357)
Resolves [Dependabot Alert
491](https://github.com/twentyhq/twenty/security/dependabot/491).

Expecting it to resolve a few other minimatch generated alerts too, but
merging shall confirm which ones since minimatch has a lot of different
versions being imported by different packages as a transitive
dependency.
2026-04-07 07:15:08 +00:00
Thomas des Francs
ea4ef99565
Salesforce section (#19366)
## Summary
- add the retro `VT323` font to the marketing site and apply it across
the Salesforce pricing card and popups
- expand the Salesforce pricing simulator with per-row metadata, unique
popup messages, dynamic price calculation, enterprise shared-cost
handling, and fixed-cost totals
- align the Salesforce card UI with the wireframes: sticky pricing
header, updated checkbox states, popup styling/behavior, add-on link,
and footer cleanup
- remove obsolete shared popup constants and quote form logic tied to
the Salesforce card
- refresh nearby pricing page UI details, including sticky menu behavior
and related pricing section polish

## Testing
- `yarn workspace twenty-website-new build`
2026-04-07 07:14:31 +00:00
github-actions[bot]
c7d1cd11e0
i18n - translations (#19370)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 08:50:50 +02:00
Félix Malfait
83d30f8b76
feat: Send email from UI — inline reply composer & SendEmail mutation (#19363)
## Summary

- **Inline email reply**: Replace external email client redirects
(Gmail/Outlook deeplinks) with an in-app email composer. Users can reply
to email threads directly from the email thread widget or via the
command menu.
- **SendEmail GraphQL mutation**: New backend mutation that reuses
`EmailComposerService` for body sanitization, recipient validation, and
SMTP dispatch via the existing outbound messaging infrastructure.
- **Side panel compose page**: Command menu "Reply" action now opens a
side-panel compose email page with pre-filled To, Subject, and
In-Reply-To fields.

### Backend
- `SendEmailResolver` with `SendEmailInput` / `SendEmailOutputDTO`
- `SendEmailModule` wired into `CoreEngineModule`
- Reuses `EmailComposerService` + `MessagingMessageOutboundService`

### Frontend
- `EmailComposer` / `EmailComposerFields` components
- `useSendEmail`, `useReplyContext`, `useEmailComposerState` hooks
- `useOpenComposeEmailInSidePanel` + `SidePanelComposeEmailPage`
- `EmailThreadWidget` inline Reply bar with toggle composer
- `ReplyToEmailThreadCommand` now opens side-panel instead of external
links

### Seeds
- Added `handle` field to message participant seeds for realistic email
addresses
- Seed `connectedAccount` and `messageChannel` in correct batch order

## Test plan

- [ ] Open an email thread on a person/company record → verify
"Reply..." bar appears below the last message
- [ ] Click "Reply..." → composer opens inline with pre-filled To and
Subject
- [ ] Type a message and click Send → email is sent via SMTP, composer
closes
- [ ] Use command menu Reply action → side panel opens with compose
email page
- [ ] Verify Send/Cancel buttons work correctly in side panel
- [ ] Test with Cc/Bcc toggle in composer fields
- [ ] Verify error handling: invalid recipients, missing connected
account


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 08:43:48 +02:00
github-actions[bot]
aec43da1e2
i18n - translations (#19355)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-06 12:13:17 +02:00
github-actions[bot]
646edec104
i18n - translations (#19354)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-06 12:08:11 +02:00
Félix Malfait
ea572975d8
feat: generic web search driver abstraction with Exa support and billing (#19341)
## Summary

- Introduces a pluggable `WebSearchDriver` abstraction (interface,
factory, service, module) so web search is no longer tied to native
provider tools (Anthropic/OpenAI)
- **Exa** is the first driver implementation with support for
category-filtered search (company, people, news, research paper, etc.) —
particularly useful for CRM workflows
- Per-query billing for both Exa ($0.007/query) and native provider
surcharges ($0.01/query for Anthropic/OpenAI) via the existing
`USAGE_RECORDED` pipeline
- New config variables: `WEB_SEARCH_DRIVER` (EXA/DISABLED),
`EXA_API_KEY`, `WEB_SEARCH_PREFER_NATIVE` (default false — prefers Exa
over native when both available)
- `WEB_SEARCH` operation type added for usage tracking and Stripe
metering

### Architecture

```
WebSearchDriver (interface)
├── ExaDriver          — Exa neural search with category support
└── DisabledDriver     — throws when search is disabled

WebSearchDriverFactory (extends DriverFactoryBase)
└── creates driver based on WEB_SEARCH_DRIVER config

WebSearchService (facade)
├── search(query, options?, billingContext?)
├── isEnabled()
└── emits USAGE_RECORDED events per query

WebSearchTool (Tool implementation)
└── registered in ActionToolProvider, available via tool catalog
```

### Native search billing gap fixed

Anthropic and OpenAI both charge $0.01/search on top of token costs. The
token costs were already billed, but the per-call surcharge was not.
Added `countNativeWebSearchCallsFromSteps` utility +
`billNativeWebSearchUsage` to `AiBillingService`, wired into both chat
and workflow agent paths.

## Test plan

- [ ] Set `WEB_SEARCH_DRIVER=EXA` + `EXA_API_KEY=...` and verify AI chat
can search the web
- [ ] Verify category parameter works (ask about a specific
company/person)
- [ ] Set `WEB_SEARCH_DRIVER=DISABLED` and verify search tool is not
exposed
- [ ] Set `WEB_SEARCH_PREFER_NATIVE=true` with Anthropic model and
verify native search is used
- [ ] Verify usage events are emitted in ClickHouse for both Exa and
native search paths
- [ ] Verify existing billing tests pass (`npx jest
ai-billing.service.spec.ts`)


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-06 12:02:46 +02:00
Félix Malfait
8acfacc69c
Add email thread widget and message thread record page layout (#19351)
## Summary
- Move email thread display from side panel to a dedicated record page
with a new `EMAIL_THREAD` widget type
- Add message thread as a standard object with page layout, subject
field, and backfill command
- Add reply-to-email command menu item for message thread records
- Remove old side panel message thread components in favor of the new
widget-based approach

## Type fixes
- Add `EMAIL_THREAD` to `WidgetConfigurationType`, `WidgetType`, and all
configuration/validator maps
- Create `EmailThreadConfigurationDTO` and shared
`EmailThreadConfiguration` type
- Register EMAIL_THREAD in widget type validators, configuration
resolvers, and standard widget mappings

## Test plan
- [ ] Verify message thread record pages render with the email thread
widget
- [ ] Verify email thread preview navigates to the record page instead
of opening side panel
- [ ] Verify reply-to-email command appears for message thread records
- [ ] Verify typecheck passes for both twenty-front and twenty-server
- [ ] Run existing test suites to check for regressions

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 12:02:04 +02:00
Thomas des Francs
4aa1d71b12
few website improvements (#19353)
## Summary
- refine the home so it scrolls after home illustration scroll right
- Added few tweaks to pricing hero
2026-04-06 09:52:40 +00:00
BOHEUS
7bf309ba73
Update last interaction app (#19332)
Rewrite to 0.8.0 SDK
2026-04-06 09:22:24 +00:00
neo773
8d61bb9ae6
Migrate messageFolder parentFolderId from UUID to externalId (#19348)
This PR migrates `messageFolder`.`parentFolderId` from an internal
`UUID` reference to external provider id.
Eliminates unnecessary lookup and complexity

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-06 09:12:44 +00:00
github-actions[bot]
0d44d7c6d7
i18n - translations (#19352)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-06 10:28:59 +02:00
neo773
d3f0162cf5
Remove connected account feature flag (#19286)
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-06 08:13:41 +00:00
Abdullah.
3a1e112b86
Fixes and updates to the website. (#19350)
This PR implements a whole lot of fixes and updates to the website. It
adds a release-notes page, terms and conditions page, privacy policy
page, while fixing HomeStepper, ProductStepper, and WhyTwentyStepper. 3D
models still need to be styled and their sizes need to be fixed.
2026-04-06 09:42:30 +02:00
Paul Rastoin
e062343802
Refactor typeorm migration lifecycle and generation (#19275)
# Introduction

Typeorm migration are now associated to a given twenty-version from the
`UPGRADE_COMMAND_SUPPORTED_VERSIONS` that the current twenty core engine
handles

This way when we upgrade we retrieve the migrations that need to be run,
this will be useful for the cross-version incremental upgrade so we
preserve sequentiality

## What's new

To generate
```sh
npx nx database:migrate:generate twenty-server -- --name add-index-to-users
```

To apply all
```sh
npx nx database:migrate twenty-server
```

## Next
Introduce slow and fast typeorm migration in order to get rid of the
save point pattern in our code base
Create a clean and dedicated `InstanceUpgradeService` abstraction
2026-04-06 07:11:47 +00:00
Rohit Sekh
3747af4b5a
Fix readonly date still editable (#19334)
Fixes #19319

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-06 06:42:25 +00:00
Neil Kanakia
bad488494f
Fix dark mode text color on permissions tab empty stat (#19336) (#19340)
Before: black text in dark mode
<img width="1224" height="860" alt="image"
src="https://github.com/user-attachments/assets/a419ef77-9358-4588-ad19-a15b57448682"
/>


After updated styling: the correct grey text in dark mode
<img width="638" height="249" alt="Screenshot 2026-04-05 at 2 47 24 PM"
src="https://github.com/user-attachments/assets/bf736464-d33c-40f1-8244-2cf5d93b7e9c"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-05 19:46:10 +00:00
Charles Bochet
f25e040712
fix: replace npm pkg set with node script in set-local-version target (#19344)
## Summary

- Replaces `npm pkg set version=X` with a direct `node -e` script in the
`set-local-version` Nx target
- Fixes `CI Create App E2E minimal` workflow failures on fork PRs where
`npm pkg set` fails with `EJSONPARSE: Unexpected end of JSON input while
parsing empty string`

The `npm` command has unreliable `package.json` resolution in certain CI
checkout contexts (shallow clones of fork PR merge refs). Using `node`
directly to read/write the JSON file avoids this entirely.
2026-04-05 18:56:37 +00:00
yukuotec
265b2582ee
Fix zh-CN sidebar translations in minimalMetadata API (#19342)
The minimalMetadata query was returning untranslated English labels for
standard objects (Companies, People, Opportunities, etc.) when users
requested zh-CN locale. This fix adds translation logic to
MinimalMetadataService using the existing I18nService.

Changes:
- Add translateLabel() method to translate standard object labels
- Pass locale from GraphQL context through resolver to service
- Only translate non-custom objects (custom objects use user-defined
labels)

Co-authored-by: Kevin Yu <kevin.yu@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 20:14:20 +02:00
github-actions[bot]
ed912ec548
chore: sync AI model catalog from models.dev (#19339)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-05 08:22:58 +02:00
Félix Malfait
563e27831b
Clean up tool output architecture: remove wrappers, enforce ToolOutput everywhere (#19321)
## Summary

- **Remove `ExecuteToolResult` wrapper** — `execute_tool` is now a
transparent dispatcher that returns the raw `ToolOutput` from underlying
tools. No more `{ toolName, result }` envelope.
- **Type the entire execution chain as `Promise<ToolOutput>`** — from
`ToolExecutorService.dispatch()` through `resolveAndExecute()` to
`execute_tool.execute()`. Zero `Promise<unknown>` remaining in the tool
layer.
- **Use `Extract<ToolExecutionRef, ...>`** for dispatch methods,
enabling exhaustive switch checking and removing `as never` casts.
- **Relax `ToolOutput.result` to accept `null`** — removes `??
undefined` hacks at the boundary with logic function results.
- **Enforce 1-export-per-file** across tool type/interface files (split
`tool-descriptor.type.ts`, `tool-provider.interface.ts`, `tool.type.ts`,
`tool-output.type.ts`, `tool-executor.service.ts`).
- **Simplify error handling** — `wrapWithErrorHandler` and all
meta-errors (tool not found, tool excluded) now return consistent
`ToolOutput` shape with `error` as a plain string.
- **Frontend reads output directly** — removed `unwrapToolOutput`
utility; `ToolStepRenderer` and `ThinkingStepsDisplay` extract
`message`/`error` from the raw output with simple type guards.
- **Add permission error detection** for email tools via
`isInsufficientPermissionsError`, guiding the AI model to suggest
account reconnection instead of hallucinating about visibility settings.

## Test plan

- [ ] AI chat tool calls return visible output (not "null") in the UI
- [ ] Tool errors display correctly in the JSON tree
- [ ] Email draft/send tools return actionable permission errors
- [ ] Code interpreter output renders correctly
- [ ] Thinking steps display tool outputs properly


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 14:05:42 +02:00
github-actions[bot]
9cc794ddb6
i18n - translations (#19330)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-04 09:34:32 +02:00
martmull
80c5cfc2ec
Add npm packages app settings list (#19324)
solves
https://discord.com/channels/1130383047699738754/1488499318762635344
2026-04-04 07:20:11 +00:00
martmull
804e0539d9
Publish 0.8.0 (#19323)
as title
2026-04-04 06:05:51 +00:00
github-actions[bot]
4db7ae20c4
i18n - translations (#19329)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-04 08:11:17 +02:00
Félix Malfait
8da69e0f77
Fix stored XSS via unsafe URL protocols in href attributes (#19282)
## Summary

- Fixes **GHSA-7w89-7q26-gj7q**: stored XSS via `javascript:` URIs in
BlockNote `FileBlock` `props.url`, rendered as a clickable `<a href>`.
- Audited the full codebase and hardened **all** surfaces where
user-controlled URLs are rendered as `href` or passed to `window.open`.
- Applies defense-in-depth: server-side input validation + client-side
render-time checks + lint rules to prevent regressions.

### Changes

**New utility** — `isSafeUrl` (`~/utils/isSafeUrl.ts`):  
Allowlists `http:`, `https:`, `mailto:`, `tel:` protocols and relative
paths (`/`). Returns `false` for `javascript:`, `data:`, `vbscript:`,
etc.

**Server-side** — `validateBlocknoteFieldOrThrow`:  
- Recursively walks all blocks and validates `props.url` and inline link
`href` values
- Rejects payloads with unsafe URL protocols at save time (before data
is stored)

**Client-side** — 8 components hardened:
| Component | Fix |
|-----------|-----|
| `FileBlock` (reported vuln) | `isSafeUrl` gate, fixed
`target="__blank"` → `_blank`, added `rel="noopener noreferrer"` |
| `LazyMarkdownRenderer` | `isSafeUrl` gate on markdown `<a href>`,
added `target`/`rel` |
| `EditLinkPopover` (TipTap) | Validates + auto-prefixes `https://`,
rejects unsafe URLs |
| `LinkBubbleMenu` (TipTap) | `isSafeUrl` gate on `window.open`, added
`noopener,noreferrer` |
| `AttachmentRow` | `isSafeUrl` gate on file attachment `href` |
| `URLDisplay` / `LinkDisplay` | `isSafeUrl` as second check after
`startsWith('http')` |
| `IframeWidget` | `isSafeUrl` gate on `src`, shows error state for
unsafe URLs |
| `InformationBannerMaintenance` | `isSafeUrl` gate on `window.open` |

**Lint rules** — `.oxlintrc.json`:
- `no-script-url: error` — catches `javascript:` string literals
- `react/jsx-no-script-url: error` — catches `javascript:` in JSX href
attributes

## Test plan

- [ ] Create a note via GraphQL mutation with `"url":
"javascript:void(alert(1))"` in a file block — should be rejected by
server validation
- [ ] Verify existing file attachments in notes still render and are
clickable
- [ ] Verify TipTap link insertion works for normal `https://` URLs
- [ ] Verify TipTap link insertion rejects `javascript:` URIs
- [ ] Verify markdown links in AI chat render correctly for safe URLs
- [ ] Verify URL/Link field displays still work for normal URLs
- [ ] Verify iframe widget rejects non-http(s) URLs


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 08:05:27 +02:00
github-actions[bot]
282ee9ac42
i18n - docs translations (#19320)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-03 18:27:51 +02:00
neo773
0b44c9e4e5
Remove connected account upgrade command (#19316) 2026-04-03 18:01:21 +02:00
github-actions[bot]
e1a58df140
i18n - translations (#19318)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-03 17:44:32 +02:00
github-actions[bot]
93c0c98495
i18n - translations (#19317)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-03 17:38:09 +02:00
martmull
f8c3960cf2
Disable setting tab when empty (#19315)
## before
<img width="1063" height="520" alt="image"
src="https://github.com/user-attachments/assets/db92432b-a30c-49e8-9246-a09d0815e6c9"
/>


## after
<img width="1049" height="490" alt="image"
src="https://github.com/user-attachments/assets/e926b065-6a65-417b-b802-0e1df6ca638f"
/>
2026-04-03 15:27:17 +00:00
Etienne
d562a384c2
Remove direct execution feature flag - WIP (#19254)
Bug fixes exposed by always-on direct execution
1. GraphQL spec compliance — data[field] = null on resolver error
direct-execution.service.ts — Changed from Promise.allSettled (which
lost the responseKey on rejection) to Promise.all with per-field
try/catch; errors now set data[responseKey] = null per spec
2. Empty object arguments skipped (extractArgumentsFromAst)
extract-arguments-from-ast.util.ts — Removed isEmptyObject check;
filter: {}, data: {} now correctly passed to resolvers instead of
silently dropped (which caused permissions to never be checked)
3. orderBy: {} factory default treated as "no ordering"
direct-execution.service.ts — Before calling the resolver, strips
orderBy: {} and orderByForRecords: {} (empty-object factory defaults
that mean "no ordering")
assert-find-many-args.util.ts / assert-group-by-args.util.ts — Accept {}
for orderBy without throwing
4. orderBy: { field: '...' } object auto-coerced to [{ field: '...' }]
array
direct-execution.service.ts — Applies GraphQL list coercion: a
non-array, non-empty orderBy object is wrapped in an array before
assertion and resolver call
5. totalCount and aggregate fields returned as strings from PostgreSQL
graphql-format-result-from-selected-fields.util.ts — Added
coerceAggregateValue that parses numeric strings to numbers for
totalCount, sum*, avg*, min*, max*, count*, percentageOf* fields
Test updates
nested-relation-queries.integration-spec.ts — Updated expected error
message from Yoga schema-validation message to direct execution resolver
message
~30 snapshot files — Updated to reflect direct execution's error
messages (different from Yoga schema-validation messages for input type
errors)
2026-04-03 15:23:01 +00:00
Thomas Trompette
90597e47ca
Add redis cache for cron triggers (#19306)
- WorkflowCronTriggerCronJob was querying every active workspace (~700)
sequentially every minute to find cron triggers, causing regular CPU
spikes to 100% on worker pods

- Added a Redis hashset cache that stores cron triggers. On cache miss
(TTL expired, cold start, or explicit invalidation), a full scan
rebuilds the cache

- Creating/deleting a new cron trigger updates the cache, only if exists
2026-04-03 15:08:31 +00:00
Raphaël Bosi
b55765a991
Fix delete/restore/destroy commands unavailable in select-all mode (#19311)
Fixes https://github.com/twentyhq/twenty/issues/19309

In exclusion mode (select all), selectedRecords is always [] since
individual record IDs aren't tracked. The noneDefined()/everyDefined()
checks return false on empty arrays by design, which hides the delete,
restore, and destroy commands from the command menu.

- Wrap selectedRecords array checks with (isSelectAll or ...) to bypass
when in exclusion mode
- Remove the `numberOfSelectedRecords < 10000` limit
- Add `upgrade:1-21:fix-select-all-command-menu-items` command to
backfill existing workspaces
- Add tests
2026-04-03 15:03:45 +00:00
github-actions[bot]
0b8421a45a
i18n - docs translations (#19314)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-03 16:31:55 +02:00
Thomas Trompette
4ba2f3b184
Read cache value for total event stream count (#19313)
As title
2026-04-03 14:19:57 +00:00
Raphaël Bosi
8543576dae
Dynamic icon for command menu items (#19307)
Allow for dynamic icons in command menu items. For instance
`${objectMetadataItem.icon}` will resolve the object metadata item icon
2026-04-03 13:41:56 +00:00
martmull
119014f86d
Improve apps (#19256)
- simplify the base application template
- remove --exhaustive option and replace by a --example option like in
next.js https://nextjs.org/docs/app/api-reference/cli
- Fix some bugs and logs
- add a post-card app in twenty-apps/examples/
2026-04-03 12:44:03 +00:00
github-actions[bot]
2ff2c39cf4
i18n - translations (#19305)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-03 12:05:17 +02:00
Raphaël Bosi
da72075841
Fix search fallback command menu item (#19304)
The search fallback was treated as a standard action and not as a
fallback by the frontend.

## Before


https://github.com/user-attachments/assets/4d2f1e09-b109-46eb-89ff-ca589eeb7239


## After


https://github.com/user-attachments/assets/e4523047-eb1e-4386-9c19-57797e1ef26c
2026-04-03 09:50:07 +00:00
nitin
f01bcf60ee
[AI] Fix record chips not rendering inside markdown tables in AI chat (#19260)
closes
https://discord.com/channels/1130383047699738754/1480976327204016131

before - 

<img width="778" height="1259" alt="CleanShot 2026-04-02 at 18 33 03"
src="https://github.com/user-attachments/assets/66ee2fc7-a116-458e-86a6-2aa6806b9407"
/>


after -


<img width="758" height="1291" alt="CleanShot 2026-04-02 at 18 32 21"
src="https://github.com/user-attachments/assets/9e303ff5-d8f4-4c9e-83e7-9a8e6427673a"
/>
2026-04-03 11:29:45 +02:00
neo773
c521ba29be
fix: CalDAV sync broken by SSRF hostname replacement (#19291)
/closes #19272
2026-04-03 08:15:59 +00:00
neo773
baa4fda3d2
fix: create company workflow (use psl for domain suffix) (#19289)
Fixes edge case where suffix like `.com.cy` which is TLD for Cyprus
would attach person to wrong company

<img width="715" height="115" alt="image"
src="https://github.com/user-attachments/assets/bbaf7c2c-8365-4655-afda-508fbc04de2a"
/>
2026-04-03 08:14:11 +00:00
Félix Malfait
bb3d556799
Refined demo workspace creation skill (rebased, review fixes) (#19274)
## Summary

Rebased version of #19051 with all review comments addressed. Clean
branch on latest main, lint/typecheck/tests passing.

### Changes from original PR
- AI can now create, update, and delete **view filters**
(`ViewFilterToolsFactory`) and **view sorts** (`ViewSortToolsFactory`)
- `create_view` now accepts `calendarFieldName`, `calendarLayout`, and
`fieldNames` to configure views at creation time
- Three new standard skills: `view-building`, `view-filters-and-sorts`,
`custom-objects-cleanup`
- `workspace-demo-seeding` skill reworked to keep standard objects and
enrich them with custom fields
- Cache invalidation for nav menu items when object `isActive` changes
- Dashboard tool descriptions improved (RECORD_TABLE widget workflow)

### Review comments addressed (all 10 from #19051)
1. **Sentry + Cubic**: Calendar field DATE/DATE_TIME validation — added
`resolveCalendarFieldMetadataId` using `isFieldMetadataDateKind`
2. **Cubic**: "navigate tool" → "navigate_app tool" in skill metadata
(all 7 occurrences)
3. **Copilot**: KANBAN views now require `mainGroupByFieldName` — throws
clear error if missing
4. **Copilot**: CALENDAR views now require both `calendarFieldName` and
`calendarLayout` — validated before DB call
5. **Copilot**: Mock field fixtures include `type` property (DATE_TIME,
TEXT, SELECT)
6. **Copilot**: `ViewFilterValue` type assertion instead of unsafe `as
string` casts (3 locations)
7. **FelixMalfait**: Removed
`NavigationMenuItemObjectDeactivationListener` — replaced with cache
invalidation
8. **FelixMalfait**: Consolidated `ViewFilterToolProvider` and
`ViewSortToolProvider` into single `ViewToolProvider`
9. Removed `VIEW_FILTER` and `VIEW_SORT` from `ToolCategory` enum
(merged into `VIEW`)
10. Removed stale `existingFeatureFlagsMap` param incompatible with
current main

## Test plan
- [x] `npx nx lint:diff-with-main twenty-server` — passes
- [x] `npx nx typecheck twenty-server` — passes
- [x] `view-tools.factory.spec.ts` — all 20 tests pass (including 3 new
validation tests)

Supersedes #19051

https://claude.ai/code/session_01QPV74NU6vzmJb32e4i899E

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-03 09:19:46 +02:00
github-actions[bot]
12b031b67d
chore: sync AI model catalog from models.dev (#19298)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-03 08:21:46 +02:00
Bennett
2ae6a9bb98
fix: bump handlebars to 4.7.9 (CVE-2026-33937) (#19288)
## Summary
- Bumps `handlebars` from `^4.7.8` to `^4.7.9` in
`packages/twenty-shared`

## Why
- **CVE-2026-33937** — Prototype pollution via crafted template input
- **GHSA-2w6w-674q-4c4q** — Related handlebars security advisory
- Severity: **Critical** (CVSS 9.8)
- Detected by Trivy and Grype scanning `twentycrm/twenty:v1.20.0`

## What changed
- `packages/twenty-shared/package.json`: `"handlebars": "^4.7.8"` →
`"^4.7.9"`
- `yarn.lock`: updated accordingly

## Impact
`handlebars` is used in `packages/twenty-shared` for template
evaluation. The fix patches the prototype pollution vector without any
API changes.

## Test plan
- [ ] `yarn build` passes
- [ ] `yarn test` passes in twenty-shared
- [ ] Template evaluation works as before

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-03 03:38:14 +00:00
Thomas des Francs
175ae5f0aa
polishing next home hero visual (#19284)
**Summary**
- update the home hero navbar controls and surface styling to better
match the latest visual design
- simplify row hover actions by removing edit affordances and disabling
hover controls for `createdBy` and `accountOwner`
- tighten chip typography and spacing for more consistent hero table
rendering
- include the captured Playwright screenshot artifact for reference

**Testing**
- Not run (not requested)
2026-04-02 20:26:05 +00:00
github-actions[bot]
04d22feef6
i18n - translations (#19285)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 19:38:43 +02:00
Marie
2d6c8be7df
[Apps] Fix - app-synced object should be searchable (#19206)
## Summary

- **Make app-synced objects searchable**: `isSearchable` was hardcoded
to `false` and the `searchVector` field was missing the `GENERATED
ALWAYS AS (...)` expression, causing all records to have a `NULL` search
vector and be excluded from search results. Fixed by defaulting
`isSearchable` to `true` (configurable via the object manifest),
computing the `asExpression` from the label identifier field, and
allowing the update-field-action-handler to handle the `null` → defined
`asExpression` transition.
- **Make `isSearchable` updatable on an object**: The property had
`toCompare: false` in the entity properties configuration, so updates
via the API were silently ignored and never persisted. Fixed by setting
`toCompare: true`.
2026-04-02 17:14:37 +00:00
BugIsGod
43baf7b91c
fix: prepend UTF-8 BOM to fix non-Latin characters in csv (#19246)
Fixes: #19230 

CSV exports containing Arabic, Chinese, Japanese, Korean characters
displayed as garbled text in Excel because the file lacked a UTF-8 BOM.
So I added `\uFEFF` prefix to the CSV Blob so Excel and other
spreadsheet apps correctly interpret the file as UTF-8

## Table value
<img width="370" height="277" alt="image"
src="https://github.com/user-attachments/assets/ed668f00-93f0-4991-bc87-42e3cb314dbc"
/>

## Before
<img width="206" height="160" alt="image"
src="https://github.com/user-attachments/assets/a5324dbc-bd65-4443-a7fb-78047028bf91"
/>

## After
<img width="180" height="155" alt="image"
src="https://github.com/user-attachments/assets/cae5f38d-6c91-4017-96ae-9924bb41d0e0"
/>


And those are my test data

Script | Example | Language
-- | -- | --
Arabic | مرحبا | Arabic, Persian, Urdu
Chinese | 你好 | Chinese
Japanese | こんにちは | Japanese
Korean | 안녕하세요 | Korean

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-04-02 19:23:22 +02:00
Paul Rastoin
a7b2bec529
Remove try catch from generate sdk client job (#19279)
Auto merged passed to fast on
https://github.com/twentyhq/twenty/pull/19271

Try catch has been added while debugging an integration test suite
covering the workspace creation that would fail due to sdk generation
failure

As they're run synchronously in integration tests they would stop the
workspace creation process

If the proposed fix here isn't enough I'll wrapp the run sync test to be
catching any unexpected issue
2026-04-02 16:43:11 +00:00
Raphaël Bosi
77315b7f6e
Sync record selection with the edit mode (#19276)
## PR description

The previous approach used a detached flag to represent selection mode,
which could was not synced sync with actual UI selection state. This
change ties the edit mode's selection/fallback behavior directly to
whether records are truly selected in the record index.

- Removes `commandMenuItemEditSelectionModeState` (a standalone 'none' |
'selection' atom) and replaces it with
`mainContextStoreHasSelectedRecordsSelector`, which derives selection
state from the real record selection in the context store.
- Adds `useSelectFirstRecordForEditMode` to programmatically select the
first record (table row or kanban card) when toggling to selection mode,
and useResetRecordIndexSelection to clear selection when toggling off or
exiting edit mode.
- Ensures record selection is properly cleaned up when exiting layout
customization mode or closing the side panel.

## Video QA

### Table


https://github.com/user-attachments/assets/1d845809-8369-4872-b3a9-a0281b8e464b

### Board


https://github.com/user-attachments/assets/325c2d58-4ca0-408a-ab4a-66b97d9d70de
2026-04-02 16:35:14 +00:00
Paul Rastoin
a2533baa66
Local driver layer resolution wipe partial local layers relicas (#19267)
# Introduction
Wipe partial layer local driver relicas that do not implem a
node_modules folder
2026-04-02 16:29:54 +00:00
Raphaël Bosi
30517eb06c
Remove more button in edit mode (#19281)
Remove more button in edit mode
2026-04-02 16:29:47 +00:00
Paul Rastoin
331c223e73
Fix create e2e app ci set version flakiness (#19278)
Calling npm version in // with interdependent packages fait des
chocapics

<img width="225" height="225" alt="image"
src="https://github.com/user-attachments/assets/6d10e72a-87dc-4745-b078-7dd1b4706203"
/>
2026-04-02 16:05:02 +00:00
Félix Malfait
5022bf03ba
Add AI as a public feature flag in the Lab (#19277)
## Summary
- Adds `IS_AI_ENABLED` to `PUBLIC_FEATURE_FLAGS` so it appears in
**Settings > Lab** as a self-serve toggle for users
- Includes a cover image (`is-ai-enabled.png`) hosted on
`twenty.com/images/lab/`, following the existing convention for lab
feature flag images
- AI is now ready for public beta — users can enable it directly from
the Lab

## Test plan
- [ ] Verify the AI card appears in Settings > Lab with the cover image
banner
- [ ] Toggle AI on/off and confirm the feature flag is persisted
- [ ] Confirm AI features (agent chat, etc.) activate when the flag is
enabled


Made with [Cursor](https://cursor.com)
2026-04-02 15:56:27 +00:00
Paul Rastoin
a53b9cb68a
On workspace creation generate sdk client through a job (#19271)
Avoid overloading the workspace creation with the standard and custom
app sdk generation, do through a job

```ts
[Nest] 90397  - 04/02/2026, 4:44:20 PM     LOG [CoreEntityCacheService] Cache stats: localCacheSize=0 memoizerCacheSize=0 memoizerPendingSize=0 entriesByKey={} versionsByKey={}
[Nest] 90397  - 04/02/2026, 4:45:03 PM     LOG [BullMQDriver] Processing job 4325 with name CallWebhookJobsForMetadataJob on queue webhook-queue
[Nest] 90397  - 04/02/2026, 4:45:03 PM     LOG [BullMQDriver] Processing job 1 with name GenerateSdkClientJob on queue workspace-queue
[Nest] 90397  - 04/02/2026, 4:45:03 PM     LOG [BullMQDriver] Job 1 with name GenerateSdkClientJob processed on queue workspace-queue in 0.64ms
[Nest] 90397  - 04/02/2026, 4:45:03 PM     LOG [BullMQDriver] Processing job 2 with name GenerateSdkClientJob on queue workspace-queue
[Nest] 90397  - 04/02/2026, 4:45:03 PM     LOG [BullMQDriver] Job 2 with name GenerateSdkClientJob processed on queue workspace-queue in 0.06ms
[Nest] 90397  - 04/02/2026, 4:45:03 PM     LOG [BullMQDriver] Job 4325 with name CallWebhookJobsForMetadataJob processed on queue webhook-queue in 17.47ms
[Nest] 90397  - 04/02/2026, 4:45:03 PM     LOG [BullMQDriver] Processing job 4326 with name CallWebhookJobsForMetadataJob on queue webhook-queue
```
2026-04-02 15:53:56 +00:00
Thomas Trompette
779e613df3
Fixes: relation settings design + workflow input border (#19269)
Before 

<img width="450" height="172" alt="Capture d’écran 2026-04-02 à 15 57
06"
src="https://github.com/user-attachments/assets/1b2c3f56-f696-407d-a00f-ae567842dbfc"
/>
<img width="450" height="172" alt="Capture d’écran 2026-04-02 à 15 57
22"
src="https://github.com/user-attachments/assets/51ee6db8-0974-46e1-a65f-25fb8aef7126"
/>

After

<img width="450" height="172" alt="Capture d’écran 2026-04-02 à 15 56
38"
src="https://github.com/user-attachments/assets/4843c36c-3c04-4c5a-b2dc-f48b02c9a907"
/>
<img width="450" height="172" alt="Capture d’écran 2026-04-02 à 15 56
58"
src="https://github.com/user-attachments/assets/ec404b4d-6c37-4694-a68c-63f98cdf7388"
/>
2026-04-02 15:43:35 +00:00
Abdul Rahman
7f2b853ae1
feat: add message compaction for AI chats (#19205) 2026-04-02 15:13:11 +00:00
Weiko
c1e4756f9c
Add is active to overridable entities and deactivation logic for page layouts (#19200)
- Replace soft-deletion (deletedAt) with isActive boolean for
overridable entities (tabs, widgets, viewFieldGroups, viewFields).
Standard entities are deactivated (isActive: false) when removed from
update payloads, while custom entities are hard-deleted.
- When a viewFieldGroup is deactivated/deleted, its viewFields are
reassigned to the next section by position (or null if none remain).
- Add isActive: true filters to viewFieldGroup and viewField API queries
so deactivated entities are excluded from responses.

Next: 
- fields-widget-upsert.service.ts should be refactored a bit
- Add restore logic
2026-04-02 14:50:50 +00:00
Charles Bochet
67d34be7c8
Add new workspace feature flag fix (#19270) 2026-04-02 14:26:56 +00:00
Weiko
6f3a86c4a9
Fix insert conflict between field permission and RLS (#19244)
Insert operations blocked by field-level update permissions on
non-editable fields

The insert code path in permissions.utils.ts fell through to the update
case (no break), causing validateUpdateFieldPermissionOrThrow to reject
inserts when any field had "Edit disabled" which could conflict with RLS
predicates (used for insertion of new records)

Fixes https://github.com/twentyhq/twenty/issues/19201

We will keep checking update permissions for insertion (until we decide
to have a separate permission flag for insertion) but to fix the issue
we will skip this part if it conflicts with an RLS predicate
2026-04-02 14:01:51 +00:00
Baptiste Devessier
4dfd6426c2
Disable going to settings in layout edit mode (#19265)
https://github.com/user-attachments/assets/c5e07ef3-f4c5-4dc1-b577-c333525327f5
2026-04-02 13:54:20 +00:00
nitin
aadbf67ec8
[CommandMenu] Disable record selection dropdown on record pages in command menu editor (#19264) 2026-04-02 13:53:15 +00:00
Abdullah.
d7e31ab2f0
Interactive home visual for website. (#19261)
This PR makes the Home Visual interactive.

Not the perfect code, but does the trick for now to convey what we're
trying to do. As per the conversation with Thomas, we will iterate over
it and make it better when we get past the first release.

---------

Co-authored-by: Thomas des Francs <tdesfrancs@gmail.com>
2026-04-02 13:45:22 +00:00
Charles Bochet
e1caf6caa1
Add datasource migration and connected account feature flag (#19268) 2026-04-02 15:58:32 +02:00
github-actions[bot]
6a8bab6a47
i18n - translations (#19266)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 15:52:30 +02:00
nitin
047dd350f6
fix workflow node (#19262)
<img width="999" height="1090" alt="CleanShot 2026-04-02 at 18 41 58"
src="https://github.com/user-attachments/assets/1e0db9e5-e6ed-4899-910d-14b3db420740"
/>
2026-04-02 15:46:33 +02:00
nitin
3c067f072c
[AI] Improve tools tab (#19221) 2026-04-02 13:36:33 +00:00
Paul Rastoin
cd23a2bc80
Cleaning UpgradeCommand code flow (#19241)
# Introduction
Currently preparing the `UpgradeCommand` refactor, in this way started
by cleaning up the existing avoiding unecessary dependencies to others
services allowing easier readability and concern centralization for
upcoming refactor
The UpgradeCommand was extending up to five classes, overriding
abstracted class and so on. It was also cascade
injecting 3 services

Introducing the `WorkspaceIteratorService` that centralize the commands
set to run over a single workspace logic shared between both atomic
upgrade command call and global upgradeCommand

## Tradeoff
Duplicated `@Option` between both `UpgradeCommandRunner` and
`WorkspaceMigrationRunner`


## Before
```
UpgradeCommand
  └─ extends UpgradeCommandRunner
       └─ extends ActiveOrSuspendedWorkspacesMigrationCommandRunner
            └─ extends WorkspacesMigrationCommandRunner  (owns workspace iteration loop + ORM deps)
                 └─ extends MigrationCommandRunner       (dry-run, verbose, error handling)
                      └─ extends CommandRunner            (nest-commander)
```

## Now

```
UpgradeCommand
  └─ extends UpgradeCommandRunner
       └─ extends CommandRunner                 (nest-commander)
       uses ─► Services         (via composition)
```

## Logging management
At the moment all services are logging, in the best of the world only
the runners should be doing so
2026-04-02 13:24:55 +00:00
Weiko
6ae5900ac9
Fix field permission validation rejecting undefined optional fields (#19243)
The backend validation for field permissions was using !== null to check
canReadFieldValue and canUpdateFieldValue, but these optional GraphQL
fields can also be undefined when omitted from the input. This caused
the validation to incorrectly reject legitimate requests (e.g.,
restricting only "update" without specifying "read") with the error
"Field permissions can only be used to restrict access, not to grant
additional permissions."

Replaced !== null checks with isDefined() so that both null and
undefined are treated as "no opinion" on that permission.

To reproduce:
- Create a single FieldPermission on an object with canEdit: false
without any other rule on that same object
2026-04-02 13:15:14 +00:00
Baptiste Devessier
6f89098340
Prevent hovered tab to overflow (#19259)
## Before

<img width="1274" height="102" alt="image"
src="https://github.com/user-attachments/assets/86088cd3-6e15-40f2-8fe4-0090e4fe9572"
/>

## After


https://github.com/user-attachments/assets/fb892f16-f1ca-4e02-b588-2651d5738a9b
2026-04-02 13:09:04 +00:00
github-actions[bot]
634a5e9599
i18n - translations (#19263)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 15:13:10 +02:00
Charles Bochet
45c1c60acc
Fix maintenance mode banner button color, timezone and date picker UX (#19255)
## Summary
- Set `accent="blue"` on InformationBanner action button so it renders
blue instead of default gray
- Add Banner storybook stories for all color × variant combinations
(BluePrimary, BlueSecondary, DangerPrimary, DangerSecondary)
- Use `useUserTimezone()` in `SettingsDatePickerInput` instead of
browser timezone (`Temporal.Now.timeZoneId()`) so dates respect the
admin's profile timezone preference
- Separate `onChange` from `onClose` in `SettingsDatePickerInput` so
changing the hour no longer forces the date picker to close
2026-04-02 12:58:03 +00:00
github-actions[bot]
8581c11d56
i18n - docs translations (#19257)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 14:38:19 +02:00
Félix Malfait
2ebff5f4d7
fix: harden token renewal and soften refresh token revocation (#19175)
## Summary
- **Apollo factory** (`apollo.factory.ts`): Bail early with `EMPTY` when
no token pair exists so no request is forwarded without credentials.
Propagate renewal success/failure as a boolean so failed renewals stop
the operation chain instead of forwarding with stale tokens.
- **Refresh token service** (`refresh-token.service.ts`): When a revoked
refresh token is reused past the grace period, reject only that token
instead of mass-revoking all user tokens. The most common cause is a
lost renewal response (e.g. navigation during refresh), not actual token
theft. This eliminates the "Suspicious activity detected" errors users
were seeing. Also switches to `findOneBy` since the `appTokens` relation
is no longer needed.

## Test plan
- [x] `refresh-token.service.spec.ts` — all 7 tests pass
- [ ] Verify login flow: sign in from `app.localhost`, get redirected to
workspace subdomain without "Suspicious activity" errors
- [ ] Verify token renewal: let access token expire, confirm silent
renewal works and operations resume
- [ ] Verify concurrent tabs: open multiple tabs, let tokens expire,
confirm no mass revocation cascade

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-02 12:01:31 +00:00
Charles Bochet
b8e7179a85
Add missing index on viewField.viewFieldGroupId (#19253)
## Summary

- Adds an index on `viewField.viewFieldGroupId` to fix a ~28s `DELETE
FROM core.viewFieldGroup WHERE workspaceId = $1` query observed in
production
- The slow query is triggered during workspace hard deletion
(`deleteWorkspaceSyncableMetadataEntities` in `workspace.service.ts`)
- Root cause: the FK constraint `viewField.viewFieldGroupId →
viewFieldGroup.id` with `ON DELETE SET NULL` forces PostgreSQL to
sequentially scan the entire `viewField` table for every deleted
`viewFieldGroup` row, because no index exists on the referencing column
- Uses `CREATE INDEX CONCURRENTLY` in the migration to avoid locking the
table on production
2026-04-02 11:57:27 +00:00
Baptiste Devessier
b1a7155431
Disable edition for record page layouts disabled in the side panel (#19248)
## Before


https://github.com/user-attachments/assets/8e5151d8-7e90-420c-a826-b6e670c68000

## After


https://github.com/user-attachments/assets/a230bc46-5991-4972-94d1-9fffe894ff4a

Fixes
https://discord.com/channels/1130383047699738754/1488106565117673513
2026-04-02 11:51:44 +00:00
Etienne
a303d9ca1b
Gql direct execution - Handle introspection queries (#19219)
### Context
GraphQL introspection queries (__schema, __type) were going through the
full Yoga server pipeline, which forces a complete workspace schema
build, loading flat metadata maps, building all GraphQL types, wiring
all resolver factories, and calling makeExecutableSchema. This is
expensive, even though introspection only needs the type structure and
zero resolver execution.

A new WorkspaceGraphqlSchemaSDLService extracts the SDL computation that
was previously embedded inside WorkspaceSchemaFactory.

From `direct-execution.service.ts` : 
- `buildSchema(sdl)` reconstructs a resolver-free GraphQLSchema from the
SDL
     - pure CPU, not cached, not sure it worths it ?

- `execute({ schema, document, variableValues })` from graphql-js,
introspection is answered entirely by the graphql-js runtime from type
metadata, no resolver execution needed


#### Nice to do ?
- Use new cache service for typeDefs ?


### Renaming bonus: `typeDefs` → `sdl`

`typeDefs` is an Apollo/graphql-tools convention. It's the parameter
name in
`makeExecutableSchema({ typeDefs, resolvers })`, not a native GraphQL
spec term.

In proper GraphQL semantics, what this service produces is the **SDL**
(Schema
Definition Language): the official term for the string representation of
a schema.
`printSchema()` produces it, `buildSchema()` consumes it.

##### Why the distinction matters

- **Type definitions** implies partial type declarations (objects,
scalars, enums…)
- **Schema SDL** conveys a *complete* schema document: all types
**plus** the root
operation types (`Query`, `Mutation`) which is exactly what
`printSchema(schema)`
  produces
2026-04-02 11:50:18 +00:00
Etienne
579714b62f
Investigate memory leak (#19213)
In search for cache leak + Remove old instrumentation
2026-04-02 11:23:10 +00:00
Baptiste Devessier
885ab8b444
Seed Front Components (#19220)
https://github.com/user-attachments/assets/6b0887e8-8ba4-49c9-9371-e3db3e9fdde8
2026-04-02 11:20:08 +00:00
Abdullah.
268543a08e
Complete sections of the new website. (#19239)
This PR completes the markup for all the sections of all pages of the
new website. There are a lot of improvements to come, but the entire
structure is in place now.
2026-04-02 11:17:30 +00:00
nitin
c854d28d60
fix: make models.dev provider logos theme-aware (#19225)
closes
https://discord.com/channels/1130383047699738754/1486675857245212682
2026-04-02 11:10:30 +00:00
nitin
223943550c
[AI] Unify code-interpreter streaming rendering and fix assistant width jitter (#19235)
closes
https://discord.com/channels/1130383047699738754/1480991390782455838

- Use data-code-execution as the streaming source of truth and hide
duplicate code-interpreter tool parts (including tool-execute_tool
wrappers).
- Ensure wrapped execute_tool code-interpreter outputs still render
correctly after refetch.
- Gate code-interpreter server behavior by enablement state and keep
assistant messages full-width to avoid streaming vs completed width
shifts.

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-04-02 10:58:14 +00:00
github-actions[bot]
1c7bda8448
i18n - docs translations (#19251)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 12:36:19 +02:00
github-actions[bot]
391f6c0dab
i18n - translations (#19250)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 12:33:12 +02:00
Charles Bochet
81f10c586f
Add workspace DDL lock env var and maintenance mode UI (#19130)
## Summary

- Add `WORKSPACE_SCHEMA_DDL_LOCKED` env-only boolean config variable
that blocks all workspace schema DDL changes when set to `true`. This is
intended for hot upgrades where logical replication cannot handle DDL
changes. Enforced at two chokepoints:
- `WorkspaceMigrationRunnerService.run` — blocks all metadata-driven DDL
(object/field/index CRUD, app sync/uninstall, standard app sync, upgrade
commands)
- `WorkspaceDataSourceService.createWorkspaceDBSchema` /
`deleteWorkspaceDBSchema` — blocks workspace creation (sign-up) and hard
deletion. Uses a dedicated `WorkspaceDataSourceException` (not
ForbiddenException)

- Add maintenance mode feature with Admin Panel UI and user-facing
banner:
- **Backend**: `MaintenanceModeService` stores maintenance window
(startAt, endAt, optional link) in `core.keyValuePair` as
`CONFIG_VARIABLE`. Validates endAt > startAt. Uses `GraphQLISODateTime`
scalar for date fields. Exposed via `clientConfig` REST endpoint and
admin GraphQL mutations (`setMaintenanceMode`, `clearMaintenanceMode`)
- **Admin Panel**: New "Maintenance Mode" section in Health tab with UTC
datetime pickers and activate/deactivate controls
- **Banner**: `InformationBannerMaintenance` displayed at the top of
`DefaultLayout` for all users, using Temporal API for timezone-aware
formatting with an optional "Learn more" link

These two features are **independent** — the DDL lock is controlled via
env var for operational use, while maintenance mode is a UI notification
mechanism controlled from the admin panel.
2026-04-02 10:17:04 +00:00
github-actions[bot]
9438b9869c
i18n - translations (#19249)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 12:06:28 +02:00
Raphaël Bosi
f688db30a9
Command to deduplicate command menu items (#19202)
- Deduplicate standard record command menu items by merging
single-record and multi-record delete, restore, destroy, and export
actions into shared record commands.
- Update frontend command registrations and engine component mappings to
use the new unified keys, while keeping deprecated engine keys
temporarily mapped for backward compatibility.
- Add a 1-21 upgrade command that removes legacy command menu items from
workspaces and creates the new unified standard items.
2026-04-02 09:51:15 +00:00
Raphaël Bosi
16033e9f99
Fix front component worker re-creation on every render (#19245)
- `frontComponentHostCommunicationApi` gets a new object reference on
every render, causing the `useMemo`/`useEffect` in
`FrontComponentWorkerEffect` to tear down and re-create the web worker
each time.
- Decouple the host API lifecycle from the worker lifecycle by moving
thread.exports updates into a dedicated
`FrontComponentUpdateHostCommunicationApiEffect` that mutates the
thread's exports object in place via Object.assign.
- Rename `FrontComponentHostCommunicationApiEffect` to
`FrontComponentInitializeHostCommunicationApiEffect` for clarity.

## Before


https://github.com/user-attachments/assets/6f3a5c14-2ae7-4317-82b5-1625abb4143e

## After


https://github.com/user-attachments/assets/1059d6cd-e02c-4477-b3e8-8e965a716434
2026-04-02 09:29:42 +00:00
dependabot[bot]
2c73d47555
Bump @storybook/react-vite from 10.2.13 to 10.3.3 (#19232)
Bumps
[@storybook/react-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/react-vite)
from 10.2.13 to 10.3.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/storybookjs/storybook/releases"><code>@​storybook/react-vite</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v10.3.3</h2>
<h2>10.3.3</h2>
<ul>
<li>Addon-Vitest: Streamline vite(st) config detection across init and
postinstall - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34193">#34193</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
</ul>
<h2>v10.3.2</h2>
<h2>10.3.2</h2>
<ul>
<li>CLI: Shorten CTA link messages - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34236">#34236</a>,
thanks <a
href="https://github.com/shilman"><code>@​shilman</code></a>!</li>
<li>React Native Web: Fix vite8 support by bumping vite-plugin-rnw - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34231">#34231</a>,
thanks <a
href="https://github.com/dannyhw"><code>@​dannyhw</code></a>!</li>
</ul>
<h2>v10.3.1</h2>
<h2>10.3.1</h2>
<ul>
<li>CLI: Use npm info to fetch versions in repro command - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34214">#34214</a>,
thanks <a
href="https://github.com/yannbf"><code>@​yannbf</code></a>!</li>
<li>Core: Prevent story-local viewport from persisting in URL - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34153">#34153</a>,
thanks <a
href="https://github.com/ghengeveld"><code>@​ghengeveld</code></a>!</li>
</ul>
<h2>v10.3.0</h2>
<h2>10.3.0</h2>
<p><em>&gt; Improved developer experience, AI-assisting tools, and
broader ecosystem support</em></p>
<p>Storybook 10.3 contains hundreds of fixes and improvements
including:</p>
<ul>
<li>🤖 Storybook MCP: Agentic component dev, docs, and test (Preview
release for React)</li>
<li> Vite 8 support</li>
<li>▲ Next.js 16.2 support</li>
<li>📝 ESLint 10 support</li>
<li>〰️ Addon Pseudo-States: Tailwind v4 support</li>
<li>🔧 Addon-Vitest: Simplified configuration - no more setup files
required</li>
<li> Numerous accessibility improvements across the UI</li>
</ul>
<!-- raw HTML omitted -->
<ul>
<li>A11y: Add ScrollArea prop focusable for when it has static children
- <a
href="https://redirect.github.com/storybookjs/storybook/pull/33876">#33876</a>,
thanks <a
href="https://github.com/Sidnioulz"><code>@​Sidnioulz</code></a>!</li>
<li>A11y: Ensure popover dialogs have an ARIA label - <a
href="https://redirect.github.com/storybookjs/storybook/pull/33500">#33500</a>,
thanks <a
href="https://github.com/gayanMatch"><code>@​gayanMatch</code></a>!</li>
<li>A11y: Make resize handles for addon panel and sidebar accessible <a
href="https://redirect.github.com/storybookjs/storybook/pull/33980">#33980</a></li>
<li>A11y: Underline MDX links for WCAG SC 1.4.1 compliance - <a
href="https://redirect.github.com/storybookjs/storybook/pull/33139">#33139</a>,
thanks <a
href="https://github.com/NikhilChowdhury27"><code>@​NikhilChowdhury27</code></a>!</li>
<li>Actions: Add expandLevel parameter to configure tree depth - <a
href="https://redirect.github.com/storybookjs/storybook/pull/33977">#33977</a>,
thanks <a
href="https://github.com/mixelburg"><code>@​mixelburg</code></a>!</li>
<li>Actions: Fix HandlerFunction type to support async callback props -
<a
href="https://redirect.github.com/storybookjs/storybook/pull/33864">#33864</a>,
thanks <a
href="https://github.com/mixelburg"><code>@​mixelburg</code></a>!</li>
<li>Addon-Docs: Add React as optimizeDeps entry - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34176">#34176</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
<li>Addon-Docs: Add support for `sourceState: 'none'` to canvas block
parameters - <a
href="https://redirect.github.com/storybookjs/storybook/pull/33627">#33627</a>,
thanks <a
href="https://github.com/quisido"><code>@​quisido</code></a>!</li>
<li>Addon-docs: Restore `docs.components` overrides for doc blocks <a
href="https://redirect.github.com/storybookjs/storybook/pull/34111">#34111</a></li>
<li>Addon-Vitest: Add channel API to programmatically trigger test runs
- <a
href="https://redirect.github.com/storybookjs/storybook/pull/33206">#33206</a>,
thanks <a
href="https://github.com/JReinhold"><code>@​JReinhold</code></a>!</li>
<li>Addon-Vitest: Handle additional vitest config export patterns in
postinstall - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34106">#34106</a>,
thanks <a
href="https://github.com/copilot-swe-agent"><code>@​copilot-swe-agent</code></a>!</li>
<li>Addon-Vitest: Make Playwright `--with-deps` platform-aware to avoid
`sudo` prompt on Linux <a
href="https://redirect.github.com/storybookjs/storybook/pull/34121">#34121</a></li>
<li>Addon-Vitest: Refactor Vitest setup to eliminate the need for a
dedicated setup file - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34025">#34025</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
<li>Addon-Vitest: Support Vitest canaries - <a
href="https://redirect.github.com/storybookjs/storybook/pull/33833">#33833</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
<li>Angular: Add moduleResolution: bundler to tsconfig - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34085">#34085</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md"><code>@​storybook/react-vite</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>10.3.3</h2>
<ul>
<li>Addon-Vitest: Streamline vite(st) config detection across init and
postinstall - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34193">#34193</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
</ul>
<h2>10.3.2</h2>
<ul>
<li>CLI: Shorten CTA link messages - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34236">#34236</a>,
thanks <a
href="https://github.com/shilman"><code>@​shilman</code></a>!</li>
<li>React Native Web: Fix vite8 support by bumping vite-plugin-rnw - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34231">#34231</a>,
thanks <a
href="https://github.com/dannyhw"><code>@​dannyhw</code></a>!</li>
</ul>
<h2>10.3.1</h2>
<ul>
<li>CLI: Use npm info to fetch versions in repro command - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34214">#34214</a>,
thanks <a
href="https://github.com/yannbf"><code>@​yannbf</code></a>!</li>
<li>Core: Prevent story-local viewport from persisting in URL - <a
href="https://redirect.github.com/storybookjs/storybook/pull/34153">#34153</a>,
thanks <a
href="https://github.com/ghengeveld"><code>@​ghengeveld</code></a>!</li>
</ul>
<h2>10.3.0</h2>
<p><em>&gt; Improved developer experience, AI-assisting tools, and
broader ecosystem support</em></p>
<p>Storybook 10.3 contains hundreds of fixes and improvements
including:</p>
<ul>
<li>🤖 Storybook MCP: Agentic component dev, docs, and test (Preview
release for React)</li>
<li> Vite 8 support</li>
<li>▲ Next.js 16.2 support</li>
<li>📝 ESLint 10 support</li>
<li>〰️ Addon Pseudo-States: Tailwind v4 support</li>
<li>🔧 Addon-Vitest: Simplified configuration - no more setup files
required</li>
<li> Numerous accessibility improvements across the UI</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b0acfb41eb"><code>b0acfb4</code></a>
Bump version from &quot;10.3.2&quot; to &quot;10.3.3&quot; [skip
ci]</li>
<li><a
href="308656fe0f"><code>308656f</code></a>
Bump version from &quot;10.3.1&quot; to &quot;10.3.2&quot; [skip
ci]</li>
<li><a
href="24c2c2c3f2"><code>24c2c2c</code></a>
Bump version from &quot;10.3.0&quot; to &quot;10.3.1&quot; [skip
ci]</li>
<li><a
href="06cb6a6874"><code>06cb6a6</code></a>
Bump version from &quot;10.3.0-beta.3&quot; to &quot;10.3.0&quot; [skip
ci]</li>
<li><a
href="94b94304e4"><code>94b9430</code></a>
Bump version from &quot;10.3.0-beta.2&quot; to &quot;10.3.0-beta.3&quot;
[skip ci]</li>
<li><a
href="af5b7de899"><code>af5b7de</code></a>
Bump version from &quot;10.3.0-beta.1&quot; to &quot;10.3.0-beta.2&quot;
[skip ci]</li>
<li><a
href="a571619e5c"><code>a571619</code></a>
Bump version from &quot;10.3.0-beta.0&quot; to &quot;10.3.0-beta.1&quot;
[skip ci]</li>
<li><a
href="546aece1ec"><code>546aece</code></a>
Bump version from &quot;10.3.0-alpha.17&quot; to
&quot;10.3.0-beta.0&quot; [skip ci]</li>
<li><a
href="ceda0b4de6"><code>ceda0b4</code></a>
Bump version from &quot;10.3.0-alpha.16&quot; to
&quot;10.3.0-alpha.17&quot; [skip ci]</li>
<li><a
href="1ed871cb53"><code>1ed871c</code></a>
Bump version from &quot;10.3.0-alpha.15&quot; to
&quot;10.3.0-alpha.16&quot; [skip ci]</li>
<li>Additional commits viewable in <a
href="https://github.com/storybookjs/storybook/commits/v10.3.3/code/frameworks/react-vite">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-02 08:49:11 +00:00
dependabot[bot]
6d5580c6fd
Bump qs from 6.14.2 to 6.15.0 (#19233)
Bumps [qs](https://github.com/ljharb/qs) from 6.14.2 to 6.15.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ljharb/qs/blob/main/CHANGELOG.md">qs's
changelog</a>.</em></p>
<blockquote>
<h2><strong>6.15.0</strong></h2>
<ul>
<li>[New] <code>parse</code>: add <code>strictMerge</code> option to
wrap object/primitive conflicts in an array (<a
href="https://redirect.github.com/ljharb/qs/issues/425">#425</a>, <a
href="https://redirect.github.com/ljharb/qs/issues/122">#122</a>)</li>
<li>[Fix] <code>duplicates</code> option should not apply to bracket
notation keys (<a
href="https://redirect.github.com/ljharb/qs/issues/514">#514</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d9b4c66303"><code>d9b4c66</code></a>
v6.15.0</li>
<li><a
href="cb41a545a3"><code>cb41a54</code></a>
[New] <code>parse</code>: add <code>strictMerge</code> option to wrap
object/primitive conflicts in...</li>
<li><a
href="88e15636da"><code>88e1563</code></a>
[Fix] <code>duplicates</code> option should not apply to bracket
notation keys</li>
<li><a
href="9d441d2704"><code>9d441d2</code></a>
Merge backport release tags v6.0.6–v6.13.3 into main</li>
<li><a
href="85cc8cac6b"><code>85cc8ca</code></a>
v6.12.5</li>
<li><a
href="ffc12aa710"><code>ffc12aa</code></a>
v6.11.4</li>
<li><a
href="0506b11e45"><code>0506b11</code></a>
[actions] update reusable workflows</li>
<li><a
href="6a37fafc75"><code>6a37faf</code></a>
[actions] update reusable workflows</li>
<li><a
href="8e8df5a3b1"><code>8e8df5a</code></a>
[Fix] fix regressions from robustness refactor</li>
<li><a
href="d60bab35a4"><code>d60bab3</code></a>
v6.10.7</li>
<li>Additional commits viewable in <a
href="https://github.com/ljharb/qs/compare/v6.14.2...v6.15.0">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 08:35:51 +00:00
Raphaël Bosi
691c86a4a7
Fix front component not opening in side panel (#19238)
# PR Description

Fix branching priority in `useCommandMenuItemsFromBackend` so
non-headless front components are routed through
`FrontComponentCommandMenuItem` (which opens the side panel) instead of
always going through the `HeadlessCommandMenuItem` path.

# Video QA

## Before


https://github.com/user-attachments/assets/4449b849-ae71-4ca2-b22e-0efd62ad1b1b

## After


https://github.com/user-attachments/assets/d361d1d1-83fb-4588-a5da-d1bae8747954
2026-04-02 08:33:58 +00:00
dependabot[bot]
1af7ac5fc4
Bump react-hotkeys-hook from 4.5.0 to 4.6.2 (#19231)
Bumps
[react-hotkeys-hook](https://github.com/JohannesKlauss/react-keymap-hook)
from 4.5.0 to 4.6.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/JohannesKlauss/react-keymap-hook/releases">react-hotkeys-hook's
releases</a>.</em></p>
<blockquote>
<h2>v4.6.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Update advanced-usage.mdx by <a
href="https://github.com/VladimirTambovtsev"><code>@​VladimirTambovtsev</code></a>
in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1232">JohannesKlauss/react-hotkeys-hook#1232</a></li>
<li>feature(addEventListener): passthrough event listener options by <a
href="https://github.com/wiserockryan"><code>@​wiserockryan</code></a>
in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1234">JohannesKlauss/react-hotkeys-hook#1234</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/VladimirTambovtsev"><code>@​VladimirTambovtsev</code></a>
made their first contribution in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1232">JohannesKlauss/react-hotkeys-hook#1232</a></li>
<li><a
href="https://github.com/wiserockryan"><code>@​wiserockryan</code></a>
made their first contribution in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1234">JohannesKlauss/react-hotkeys-hook#1234</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/JohannesKlauss/react-hotkeys-hook/compare/v4.6.1...v4.6.2">https://github.com/JohannesKlauss/react-hotkeys-hook/compare/v4.6.1...v4.6.2</a></p>
<h2>v4.6.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Consider custom element when checking if event is by <a
href="https://github.com/HJK181"><code>@​HJK181</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1164">JohannesKlauss/react-hotkeys-hook#1164</a></li>
<li>Bump http-proxy-middleware from 2.0.6 to 2.0.7 in /documentation by
<a href="https://github.com/dependabot"><code>@​dependabot</code></a> in
<a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1217">JohannesKlauss/react-hotkeys-hook#1217</a></li>
<li>Bump express from 4.19.2 to 4.21.0 in /documentation by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1210">JohannesKlauss/react-hotkeys-hook#1210</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/HJK181"><code>@​HJK181</code></a> made
their first contribution in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1164">JohannesKlauss/react-hotkeys-hook#1164</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/JohannesKlauss/react-hotkeys-hook/compare/v4.6.0...v4.6.1">https://github.com/JohannesKlauss/react-hotkeys-hook/compare/v4.6.0...v4.6.1</a></p>
<h2>v4.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>chore(deps): update all non-major dependencies by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1204">JohannesKlauss/react-hotkeys-hook#1204</a></li>
<li>Feat: Helps to identify which Shortcut was triggered exactly by <a
href="https://github.com/prostoandrei"><code>@​prostoandrei</code></a>
in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1219">JohannesKlauss/react-hotkeys-hook#1219</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/prostoandrei"><code>@​prostoandrei</code></a>
made their first contribution in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1219">JohannesKlauss/react-hotkeys-hook#1219</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/JohannesKlauss/react-hotkeys-hook/compare/v4.5.1...v4.6.0">https://github.com/JohannesKlauss/react-hotkeys-hook/compare/v4.5.1...v4.6.0</a></p>
<h2>v4.5.1</h2>
<h2>What's Changed</h2>
<ul>
<li>chore(deps): update all non-major dependencies by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1136">JohannesKlauss/react-hotkeys-hook#1136</a></li>
<li>chore(deps): update dependency <code>@​types/react</code> to
v18.2.56 by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1140">JohannesKlauss/react-hotkeys-hook#1140</a></li>
<li>fix: example code in use-hotkeys docs by <a
href="https://github.com/jvn4dev"><code>@​jvn4dev</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1142">JohannesKlauss/react-hotkeys-hook#1142</a></li>
<li>chore(deps): update actions/setup-node action to v4 by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1141">JohannesKlauss/react-hotkeys-hook#1141</a></li>
<li>chore(deps): update actions/checkout action to v4 by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1137">JohannesKlauss/react-hotkeys-hook#1137</a></li>
<li>chore(deps): update all non-major dependencies by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1147">JohannesKlauss/react-hotkeys-hook#1147</a></li>
<li>chore(deps): update all non-major dependencies by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1149">JohannesKlauss/react-hotkeys-hook#1149</a></li>
<li>chore(deps): update all non-major dependencies by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1156">JohannesKlauss/react-hotkeys-hook#1156</a></li>
<li>chore(deps): update all non-major dependencies by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1158">JohannesKlauss/react-hotkeys-hook#1158</a></li>
<li>chore(deps): update all non-major dependencies by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1162">JohannesKlauss/react-hotkeys-hook#1162</a></li>
<li>chore(deps): update all non-major dependencies by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1166">JohannesKlauss/react-hotkeys-hook#1166</a></li>
<li>chore(deps): update dependency <code>@​types/react</code> to
v18.2.79 by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1169">JohannesKlauss/react-hotkeys-hook#1169</a></li>
<li>chore(deps): update all non-major dependencies by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1171">JohannesKlauss/react-hotkeys-hook#1171</a></li>
<li>chore(deps): update all non-major dependencies to v7.24.5 by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1173">JohannesKlauss/react-hotkeys-hook#1173</a></li>
<li>chore(deps): update dependency <code>@​types/react</code> to v18.3.2
by <a href="https://github.com/renovate"><code>@​renovate</code></a> in
<a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1175">JohannesKlauss/react-hotkeys-hook#1175</a></li>
<li>chore(deps): update all non-major dependencies by <a
href="https://github.com/renovate"><code>@​renovate</code></a> in <a
href="https://redirect.github.com/JohannesKlauss/react-hotkeys-hook/pull/1178">JohannesKlauss/react-hotkeys-hook#1178</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="47f15a6554"><code>47f15a6</code></a>
4.6.2</li>
<li><a
href="bfe319bb5f"><code>bfe319b</code></a>
Merge pull request <a
href="https://redirect.github.com/JohannesKlauss/react-keymap-hook/issues/1234">#1234</a>
from wiserockryan/feature/passthrough-event-listener...</li>
<li><a
href="67b1f2e3d9"><code>67b1f2e</code></a>
Merge pull request <a
href="https://redirect.github.com/JohannesKlauss/react-keymap-hook/issues/1232">#1232</a>
from VladimirTambovtsev/patch-1</li>
<li><a
href="aa6ce06378"><code>aa6ce06</code></a>
feature(addEventListener): fix removeEventListener and add abort signal
test</li>
<li><a
href="e1cb7b41f2"><code>e1cb7b4</code></a>
feat(addEventListener): passthrough event listener options</li>
<li><a
href="157b512253"><code>157b512</code></a>
Update advanced-usage.mdx</li>
<li><a
href="bc55a281f1"><code>bc55a28</code></a>
4.6.1</li>
<li><a
href="0e41a2ac4a"><code>0e41a2a</code></a>
Fix ts error</li>
<li><a
href="1f95c28ee2"><code>1f95c28</code></a>
Fix ts error</li>
<li><a
href="4668ebb8c6"><code>4668ebb</code></a>
Merge pull request <a
href="https://redirect.github.com/JohannesKlauss/react-keymap-hook/issues/1210">#1210</a>
from JohannesKlauss/dependabot/npm_and_yarn/document...</li>
<li>Additional commits viewable in <a
href="https://github.com/JohannesKlauss/react-keymap-hook/compare/v4.5.0...v4.6.2">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 08:29:48 +00:00
github-actions[bot]
72ac10fb7a
i18n - translations (#19242)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 10:40:25 +02:00
github-actions[bot]
986256f56d
i18n - docs translations (#19240)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 10:39:32 +02:00
nitin
ade6ed9c32
[AI] agent node prompt tab new design + refactor (#19012) 2026-04-02 08:24:58 +00:00
nitin
19c710b87f
fix read only text editor color (#19223) 2026-04-02 10:23:46 +02:00
github-actions[bot]
7991ce7823
i18n - translations (#19237)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 10:21:19 +02:00
Félix Malfait
fd7387928c
feat: queue messages + replace AI SDK with GraphQL SSE subscription (#19203)
## Summary

- **Queue messages while streaming**: Messages sent during active AI
streaming are queued server-side and auto-flushed when the current
stream completes. Frontend renders queued messages optimistically in a
dedicated queue UI.
- **Drop `@ai-sdk/react` + `resumable-stream`**: Replace the dual HTTP
SSE + AI SDK client architecture with a single GraphQL SSE subscription
per thread. All events (token chunks, message persistence, queue
updates, errors) flow through Redis PubSub → GraphQL subscription.
- **Server-driven architecture**: The server decides whether to queue or
stream (via `POST /:threadId/message`). The frontend mirrors this
decision for optimistic rendering but defers to the server response.
- **Reuse AI SDK accumulation logic**: `readUIMessageStream` from the
`ai` package handles chunk-to-message accumulation on the frontend,
avoiding a custom 780-line accumulator.

## Key files

**Backend:**
- `agent-chat-event-publisher.service.ts` — publishes events to Redis
PubSub
- `agent-chat-subscription.resolver.ts` — GraphQL subscription resolver
- `stream-agent-chat.job.ts` — publishes chunks via PubSub instead of
resumable-stream
- `agent-chat.controller.ts` — unified `POST /:threadId/message`
endpoint

**Frontend:**
- `useAgentChatSubscription.ts` — subscribes to `onAgentChatEvent`,
bridges to `readUIMessageStream`
- `useAgentChat.ts` — send/stop/optimistic rendering (no more AI SDK)
- `AgentChatStreamSubscriptionEffect.tsx` — replaces
`AgentChatAiSdkStreamEffect.tsx`

## Test plan

- [ ] Send message on new thread → optimistic render, streaming response
appears
- [ ] Send message while streaming → queued instantly (no flash in main
thread)
- [ ] Queued message auto-flushes after current stream completes
- [ ] Remove queued message via queue UI
- [ ] Stop streaming mid-response
- [ ] Leave chat idle for several minutes → streaming still works after
(SSE client recycling)
- [ ] Token refresh during session → requests succeed (authenticated
fetch)
- [ ] Switch threads while streaming → clean subscription handoff


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 10:10:13 +02:00
github-actions[bot]
3733a5a763
i18n - translations (#19236)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 09:30:11 +02:00
Weiko
0b2f4cdb57
Add delete widget action in fields widget side panel (#19167)
## Context
Add a new "Manage" section in FIELDS widget side panel with a "Delete
widget" action

Next step: Deleting a non-custom fields widget should be reversible
("Reset to default" followup)

<img width="1286" height="398" alt="Screenshot 2026-03-31 at 15 17 31"
src="https://github.com/user-attachments/assets/9ee63c14-52f1-470e-9112-4538271dc6fc"
/>
2026-04-02 07:15:19 +00:00
github-actions[bot]
1622c87b7a
i18n - docs translations (#19234)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 08:44:39 +02:00
github-actions[bot]
f3e2e00e79
i18n - docs translations (#19229)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 07:00:47 +02:00
github-actions[bot]
5de5ed2cb4
i18n - docs translations (#19228)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 05:20:37 +02:00
github-actions[bot]
6eb4c4ca4b
i18n - docs translations (#19227)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 03:04:12 +02:00
github-actions[bot]
ae202a1b59
i18n - docs translations (#19226)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-02 00:27:21 +02:00
martmull
16e3e38b79
Improve getting started doc (#19138)
- improves
`packages/twenty-docs/developers/extend/apps/getting-started.mdx`

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-04-01 20:39:44 +00:00
github-actions[bot]
4cc3deb937
i18n - docs translations (#19217)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-01 18:40:26 +02:00
github-actions[bot]
e15feda3c3
i18n - translations (#19216)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-01 18:15:16 +02:00
Raphaël Bosi
9f95c4763c
[COMMAND MENU ITEMS] Remove deprecated code (#19199)
This PR is the first one of a cleanup after upgrading command menu items
to V2.
2026-04-01 15:56:52 +00:00
github-actions[bot]
e6fe48b66d
i18n - translations (#19215)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-01 18:00:09 +02:00
Baptiste Devessier
20ac5b7e84
Field widget edition (#19209)
https://github.com/user-attachments/assets/2f1a5847-3375-414f-a2b8-ce4b533b5512

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 15:44:49 +00:00
github-actions[bot]
136f362b24
i18n - translations (#19214)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-01 17:40:00 +02:00
Baptiste Devessier
291792f864
Repair Edit Layout command menu item and add a button in settings to start edition too (#19208)
https://github.com/user-attachments/assets/f23f0777-abf3-4ff4-8fab-ec2004df60bc
2026-04-01 15:24:05 +00:00
Raphaël Bosi
9054d3aef6
[COMMAND MENU ITEMS] Resolve object metadata label in dynamic command menu item label (#19211)
- Adds a resolveObjectMetadataLabel utility that returns the singular or
plural label from object metadata based on the number of selected
records (e.g., "person" vs "people").
- Exposes a pre-computed objectMetadataLabel string on
CommandMenuContextApi, making it available for label interpolation
(e.g., Delete ${capitalize(objectMetadataLabel)}).
2026-04-01 15:02:24 +00:00
Thomas Trompette
19dd4d6c1b
Fix workflow date fields (#19210)
Before: broken on forms, missing border right, no fullWidth, not
properly saved
<img width="220" height="160" alt="Capture d’écran 2026-03-31 à 17 10
47"
src="https://github.com/user-attachments/assets/4143fcb7-909f-42a3-b05e-39185395f657"
/> <img width="231" height="102" alt="Capture d’écran 2026-03-31 à 17
11 05"
src="https://github.com/user-attachments/assets/3989f6b8-ef8a-42a3-9ccc-35a9be1fb67f"
/>
<img width="230" height="75" alt="Capture d’écran 2026-03-31 à 17 12
15"
src="https://github.com/user-attachments/assets/67f5f93f-887f-4e3b-95c2-f5076f41fb21"
/>

After: save and validate on edition, fix design
<img width="231" height="132" alt="Capture d’écran 2026-03-31 à 17 15
08"
src="https://github.com/user-attachments/assets/d1aa0a64-499d-479d-8d5c-e5e104ad6464"
/>
<img width="231" height="75" alt="Capture d’écran 2026-03-31 à 17 15
33"
src="https://github.com/user-attachments/assets/8d668713-8466-4e81-8901-4b12b9271244"
/>
<img width="461" height="156" alt="Capture d’écran 2026-03-31 à 17 14
54"
src="https://github.com/user-attachments/assets/f9d33242-f1cc-48f7-9f63-322799e1f9b8"
/>

Simplifying FormDateInput using the existing DatePicker
2026-04-01 14:44:24 +00:00
github-actions[bot]
a0c6727a61
i18n - docs translations (#19212)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-01 16:48:34 +02:00
BOHEUS
b002930554
Update documentation on how to upload a file (#19197)
As per title, update a documentation on how to upload a file given
increasing amount of questions for this problem

CC: @StephanieJoly4

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-04-01 14:15:43 +00:00
Marie
aa0ea96582
Improve app errors logs at sync (#19174)
1. Fix scrollbar
Before

https://github.com/user-attachments/assets/29792a71-b2dd-49f6-bb90-9d15feeb95aa

After

https://github.com/user-attachments/assets/939a000a-b787-4ea5-a9f0-61fbac886025

2. Introduce verbose vs non-verbose
verbose = what we have today (very detailed)
non-verbose = summarized (with a log to say add --verbose for full
logs!)

without --verbose
<img width="1256" height="876" alt="updated_non_verbose"
src="https://github.com/user-attachments/assets/d6194c41-2366-4297-a7ac-b3f3b27e08dd"
/>


with --verbose
<img width="422" height="819" alt="verbose_logs"
src="https://github.com/user-attachments/assets/409e2e88-ec3d-4bab-957c-ef319895f8c5"
/>
2026-04-01 13:46:37 +00:00
Gabriel
36dece43c7
Fix: Upgrade Nodemailer to address SMTP command injection vulnerability (#19151)
📄 Summary

This PR upgrades the nodemailer dependency to a secure version (≥ 8.0.4)
to fix a known SMTP command injection vulnerability
(GHSA-c7w3-x93f-qmm8).

🚨 Issue

The current version used in twenty-server (^7.0.11, resolved to 7.0.11 /
7.0.13) is vulnerable to SMTP command injection due to improper
sanitization of the envelope.size parameter.
This could allow CRLF injection, potentially enabling attackers to add
unauthorized recipients to outgoing emails.

🔍 Root Cause

The vulnerability originates from insufficient validation of
user-controlled input in the SMTP envelope, specifically the size field,
which can be exploited via crafted input containing CRLF sequences.

 Changes
Upgraded nodemailer to version ^8.0.4
Ensured compatibility with existing email sending logic
Verified that no breaking changes affect current usage

🔐 Security Impact

This update mitigates the risk of:

SMTP command injection
Unauthorized email recipient manipulation
Potential data leakage via crafted email payloads
📎 References
GHSA: GHSA-c7w3-x93f-qmm8
CVE: (see linked report in issue)

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-31 19:55:50 +00:00
Charles Bochet
ac8e0d4217
Replace twentycrm/twenty-postgres-spilo with official postgres:16 in CI (#19182)
## Summary
- Replaces `twentycrm/twenty-postgres-spilo` with the official
`postgres:16` image across all 7 CI workflow files
- Removes Docker Hub `credentials` blocks from all service containers
(postgres, redis, clickhouse)
- Removes the `Login to Docker Hub` step from the breaking changes
workflow

## Context
Fork PRs cannot access repository secrets/variables, causing `${{
vars.DOCKERHUB_USERNAME }}` and `${{ secrets.DOCKERHUB_PASSWORD }}` to
resolve to empty strings. GitHub Actions rejects empty credential values
at template validation time, failing the job before any step runs.

The custom spilo image was the original reason credentials were needed
(to avoid Docker Hub rate limits on non-official images). The only
Postgres extensions required in CI (`uuid-ossp`, `unaccent`) are built
into the official `postgres:16` image. Official Docker Hub images have
significantly higher pull rate limits and don't require authentication.
2026-03-31 21:41:42 +02:00
Charles Bochet
ee3ebd0ca0
Add "search" to reserved metadata name keywords (#19181)
## Summary
- Adds `search` and `searches` to the `RESERVED_METADATA_NAME_KEYWORDS`
list in `twenty-shared`
- Prevents users from creating custom objects named "search", which
collides with the core `search` GraphQL resolver
2026-03-31 21:16:46 +02:00
Baptiste Devessier
c11e4ece39
Fallback to field metadata (#19131)
Rely on the field metadata items to always display all object's fields
in the fields widget configuration editor. If fields are missing in the
returned view fields, we add the missing fields through object metadata.


https://github.com/user-attachments/assets/3c4d45e8-05d0-4943-be4b-bcf1e310155c
2026-03-31 19:00:16 +00:00
Charles Bochet
5bbfce7789
Add 1-21 upgrade command to backfill datasource to workspace table (#19180)
## Summary
- Adds a new `upgrade:1-21:backfill-datasource-to-workspace` command
that copies `dataSource.schema` into `workspace.databaseSchema` for all
active/suspended workspaces that haven't been migrated yet
- Registers the command in the 1-21 upgrade module and wires it into the
upgrade runner (runs before other 1-21 commands)
- Part of the ongoing deprecation of the `dataSource` table in favor of
storing `databaseSchema` directly on `WorkspaceEntity`
2026-03-31 20:34:46 +02:00
Etienne
887e0283c5
Direct execution - Follow up (#19177)
Feedbacks from https://github.com/twentyhq/twenty/pull/18972
2026-03-31 17:38:24 +00:00
Abdullah.
94e019f012
Complete the structure for homepage in new website. (#19162)
This PR completes the sections we need for the Homepage. Assets, such as
images, are still placeholder, and will be replaced as they become
available. We're still waiting on Lottie and 3d asset files from the
design team.
2026-03-31 17:19:50 +00:00
Abdul Rahman
c23961fa81
Fix navbar object color not updating immediately when changed in edit mode (#19075)
https://github.com/user-attachments/assets/f2a8f2af-b92f-4d2e-9570-fe66f0a3eca0
2026-03-31 16:51:01 +00:00
Baptiste Devessier
2612145436
Fix sdk tests (#19172) 2026-03-31 15:08:21 +00:00
github-actions[bot]
4c97642258
i18n - translations (#19171)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 16:33:53 +02:00
nitin
08f019e9c9
[AI] More tab new design (#19165)
closes
https://discord.com/channels/1130383047699738754/1480981616666087687

<img width="1596" height="1318" alt="CleanShot 2026-03-31 at 18 06 37"
src="https://github.com/user-attachments/assets/0d87610e-e4ce-4f3d-8047-97d242f230c5"
/>
2026-03-31 16:26:40 +02:00
Marie
888fa271f0
[Apps SDK] Fix rich app link in documentation (#19007)
- Fix link to rich app for LLMS
- Add example of extension of existing object in rich app (post card
app)
2026-03-31 13:35:57 +00:00
github-actions[bot]
bb5c64952c
i18n - translations (#19169)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 15:29:13 +02:00
github-actions[bot]
6dedb35a1f
i18n - translations (#19168)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 15:22:30 +02:00
Weiko
436333f110
Add see records link to data model (#19163)
## Context
Add a link to the INDEX view page of an object to list all its records
directly from the data model page of the object.

<img width="556" height="460" alt="Screenshot 2026-03-31 at 14 04 20"
src="https://github.com/user-attachments/assets/3daa9ee5-ad09-42b5-a406-088ce8c53be4"
/>
2026-03-31 13:12:43 +00:00
Abdul Rahman
ec283b8f2d
Fix dropping workspace nav items at the bottom of the list (#18989)
https://github.com/user-attachments/assets/64abd955-892e-48c8-bf7b-d4d8645cf33e

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-31 13:06:31 +00:00
github-actions[bot]
c3b969ab74
i18n - translations (#19166)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 14:55:02 +02:00
Weiko
287fe90ce9
Add link to workspace members index view page from the settings (#19164)
## Context
Fixes https://github.com/twentyhq/core-team-issues/issues/2039

<img width="608" height="519" alt="Screenshot 2026-03-31 at 14 28 00"
src="https://github.com/user-attachments/assets/939e47c6-84c2-471c-8da5-b76b161f086a"
/>
2026-03-31 12:39:22 +00:00
Thomas Trompette
d10c0a6439
Fix: composite update events not received (#19053)
Database batch events (update / insert / soft-delete / hard-delete) were
building recordsBefore / recordsAfter after formatResult ran twice: once
inside WorkspaceSelectQueryBuilder.getMany() / getOne(), and again in
the CUD query builders. On the second pass, already-shaped composite
fields (e.g. emails) went through formatFieldMetadataValue and kept the
same object references as the live TypeORM row, so recordsBefore could
change when the entity was updated—breaking workflow triggers and diffs.
2026-03-31 11:58:12 +00:00
BugIsGod
176e81cd76
fix: clear navigation stack immediately in goBackFromSidePanel (#19153)
Fixes: #19152 

## Summary
goBackFromSidePanel returned early without writing the new (empty) stack
to the store, leaving stale navigation state. And it caused
openRecordInSidePanel to think the record was already open and skip
reopening.
<img width="587" height="405" alt="image"
src="https://github.com/user-attachments/assets/eb36f4c6-43ca-4c08-891a-c592ff673837"
/>

## Before


https://github.com/user-attachments/assets/03d1e24a-6d1e-4efc-93c1-72be0bc31a89


## After
When close the side panel with Escape, now it can be opened by clicking
arrow button again.




https://github.com/user-attachments/assets/ae700d41-4f81-4d1a-af9a-f97f31f489a6

---------

Co-authored-by: Devessier <baptiste@devessier.fr>
2026-03-31 11:29:03 +00:00
Charles Bochet
1ce4da5b67
Fix upgrade commands 2 (#19157) 2026-03-31 12:47:01 +02:00
github-actions[bot]
ef66d6b337
i18n - translations (#19158)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 12:23:19 +02:00
BugIsGod
bcca5d0002
fix: show disabled file chip when file no longer exists in timeline (#19045)
Fixes: #18943 
Follow-up pr: #19001

## Summary:                                                             
- Timeline activity shows file upload history, but deleted files had no
signed URL and were still rendered as clickable — clicking did nothing
- grab the fileId from properties.diff.after, look it up in the current
record's files field: if present, use live signed URL; if absent, mark
as deleted
- Deleted file chips show line-through label, not-allowed cursor, and
"File no longer exists" tooltip on hover
  


https://github.com/user-attachments/assets/5df6a675-0003-4fd1-ad57-a07e4338923f

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-31 10:07:19 +00:00
Weiko
e0630b8653
Fix TransactionNotStartedError (#19155)
## Context
Checked in the codebase where we are trying to rollback a non-active
transaction.


packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts
seemed to be the only place where it happens.

We could enforce this with a lint rule in the future 🤔
2026-03-31 10:02:50 +00:00
Paul Rastoin
61a27984e8
0.8.0.canary.7 bump (#19150)
Already published on npm
2026-03-31 09:50:28 +02:00
github-actions[bot]
fd21d0c6ca
i18n - docs translations (#19142)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 00:23:38 +02:00
github-actions[bot]
8d539f0e49
i18n - docs translations (#19139)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 22:30:14 +02:00
github-actions[bot]
a6cecdbd49
i18n - docs translations (#19137)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 20:38:05 +02:00
Raphaël Bosi
383935d0d9
Fix widgets drag handles (#19133)
- Remove drag handles and cursor resize for the placeholder
- Fix the drag handles no longer appearing on hover and always present
drag handle for north and south

This was due to the linaria migration: adding a minus sign or px after a
css variable isn't working, we have to use calc.

## Before


https://github.com/user-attachments/assets/cba398ab-92c8-4924-ba3c-1a28fb4fd163

## After


https://github.com/user-attachments/assets/aec675cd-b809-434d-a8a7-edb12ce37364
2026-03-30 17:07:49 +00:00
Paul Rastoin
37908114fc
[SDK] Extract twenty-front-component-renderer outside of twenty-sdk ( 2.8MB ) (#19021)
Followup https://github.com/twentyhq/twenty/pull/19010

## Dependency diagram

```
┌─────────────────────┐
│     twenty-front    │
│   (React frontend)  │
└─────────┬───────────┘
          │ imports runtime:
          │   FrontComponentRenderer
          │   FrontComponentRendererWithSdkClient
          │   useFrontComponentExecutionContext
          ▼
┌──────────────────────────────────┐         ┌─────────────────────────┐
│ twenty-front-component-renderer  │────────▶│       twenty-sdk        │
│   (remote-dom host + worker)     │         │  (app developer SDK)    │
│                                  │         │                         │
│  imports from twenty-sdk:        │         │  Public API:            │
│   • types only:                  │         │   defineFrontComponent  │
│     FrontComponentExecutionContext│         │   navigate, closeSide…  │
│     NavigateFunction             │         │   useFrontComponent…    │
│     CloseSidePanelFunction       │         │   Command components    │
│     CommandConfirmation…         │         │   conditional avail.    │
│     OpenCommandConfirmation…     │         │                         │
│     EnqueueSnackbarFunction      │         │  Internal only:         │
│     etc.                         │         │   frontComponentHost…   │
│                                  │         │   front-component-build │
│  owns locally:                   │         │   esbuild plugins       │
│   • ALLOWED_HTML_ELEMENTS        │         │                         │
│   • EVENT_TO_REACT               │         └────────────┬────────────┘
│   • HTML_TAG_TO_CUSTOM_ELEMENT…  │                      │
│   • SerializedEventData          │                      │ types
│   • PropertySchema               │                      ▼
│   • frontComponentHostComm…      │         ┌─────────────────────────┐
│     (local ref to globalThis)    │         │     twenty-shared       │
│   • setFrontComponentExecution…  │         │  (common types/utils)   │
│     (local impl, same keys)      │         │   AppPath, SidePanelP…  │
│                                  │         │   EnqueueSnackbarParams │
└──────────────────────────────────┘         │   isDefined, …          │
          │                                  └─────────────────────────┘
          │ also depends on
          ▼
    twenty-shared (types)
    @remote-dom/* (runtime)
    @quilted/threads (runtime)
    react (runtime)
```

**Key points:**

- **`twenty-front`** depends on the renderer, **not** on `twenty-sdk`
directly (for rendering)
- **`twenty-front-component-renderer`** depends on `twenty-sdk` for
**types only** (function signatures, `FrontComponentExecutionContext`).
The runtime bridge (`frontComponentHostCommunicationApi`) is shared via
`globalThis` keys, not module imports
- **`twenty-sdk`** has no dependency on the renderer — clean one-way
dependency
- The renderer owns all remote-dom infrastructure (element schemas,
event mappings, custom element tags) that was previously leaking through
the SDK's public API
- The SDK's `./build` entry point was removed entirely (unused)
2026-03-30 17:06:06 +00:00
github-actions[bot]
369ae2862f
i18n - docs translations (#19135)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 18:40:35 +02:00
github-actions[bot]
5bde41ebbb
i18n - translations (#19134)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 18:30:38 +02:00
Félix Malfait
8fa3962e1c
feat: add resumable stream support for agent chat (#19107)
## Overview
Add resumable stream support for agent chat to allow clients to
reconnect and resume streaming responses if the connection is
interrupted (e.g., during page refresh).

## Changes

### Backend (Twenty Server)
- Add `activeStreamId` column to `AgentChatThreadEntity` to track
ongoing streams
- Create `AgentChatResumableStreamService` to manage Redis-backed
resumable streams using the `resumable-stream` library with ioredis
- Extend `AgentChatController` with:
  - `GET /:threadId/stream` endpoint to resume an existing stream
  - `DELETE /:threadId/stream` endpoint to stop an active stream
- Update `AgentChatStreamingService` to store streams in Redis and track
active stream IDs
- Add `resumable-stream@^2.2.12` dependency to package.json

### Frontend (Twenty Front)
- Update `useAgentChat` hook to:
- Use a persistent transport with `prepareReconnectToStreamRequest` for
resumable streams
  - Export `resumeStream` function from useChat
  - Add `handleStop` callback to clear active stream on DELETE endpoint
- Use thread ID as stable message ID instead of including message count
- Add stream resumption logic in `AgentChatAiSdkStreamEffect` component
to automatically call `resumeStream()` when switching threads

## Database Migration
New migration `1774003611071-add-active-stream-id-to-agent-chat-thread`
adds the `activeStreamId` column to store the current resumable stream
identifier.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 18:19:25 +02:00
Weiko
ca00e8dece
Fix databaseSchema migration (#19132) 2026-03-30 18:01:19 +02:00
Thomas des Francs
2263e14394
clean paddings in favorites section (#19112)
## Summary
- hide the Favorites section wrapper when there are no top-level
favorites and no folder creation in progress
- remove the extra top padding from the Favorites list so the section
title-to-content gap matches other navigation sections
- prevent the empty animated container from adding stray space in the
navbar

## screens

Before
<img width="926" height="820" alt="CleanShot 2026-03-30 at 12 02 11@2x"
src="https://github.com/user-attachments/assets/eac53f86-1108-42ac-986f-328327fbe947"
/>

<img width="1602" height="1168" alt="CleanShot 2026-03-30 at 12 02
29@2x"
src="https://github.com/user-attachments/assets/6249c7f9-9ac5-46c8-a709-dfc1904b37c7"
/>

(arrows = paddings too big)

After
<img width="1270" height="1074" alt="CleanShot 2026-03-30 at 12 01
24@2x"
src="https://github.com/user-attachments/assets/16e4155a-5591-4387-9675-f5c7be58ce32"
/>
2026-03-30 15:37:35 +00:00
Etienne
ecf8161d0e
Fix - Remove signFileUrl method (#19121)
`signFileUrl` is the old way to resolve file url. Replaced by
`signFileByIdUrl` which needs `FileFolder` and `fileId`.

Fix also avatar for person and workspaceMember. To be continued with
imageIdentifier refactor.


Fix : 
- https://discord.com/channels/1130383047699738754/1486723854910099576
- https://discord.com/channels/1130383047699738754/1484566445584285870
- https://discord.com/channels/1130383047699738754/1487124612889313420

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-30 15:14:54 +00:00
Etienne
3d1c53ec9d
Gql direct execution - Improvements (#18972)
#### Direct Execution  __typename & null backfill
##### __typename filling
The direct execution path now correctly derives __typename at every
level of the GraphQL response:
Connection types — CompanyConnection, CompanyEdge, Company, PageInfo
GroupBy types — TaskGroupByConnection (was incorrectly producing
TaskConnection)
Composite fields — Links, FullName, Currency, etc. handled by a
dedicated formatter (was inheriting the parent object's typename)
Previously, __typename was derived from the object's universal
identifier (a UUID), producing broken values like
20202020B3744779A56180086Cb2E17FConnection.
##### Null backfill
Selected fields missing from the resolver result are backfilled with
null, matching the standard Yoga schema behavior.
##### Integration test
A new test runs the same findMany query (with __typename at all
structural levels) through both paths — standard Yoga schema and direct
execution — and asserts identical output via toStrictEqual.
2026-03-30 17:20:25 +02:00
github-actions[bot]
2a5aab0c44
i18n - translations (#19129)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 17:19:21 +02:00
martmull
8985dfbc5d
Improve apps (#19120)
fixes
https://discord.com/channels/1130383047699738754/1488094970241089586
2026-03-30 15:03:23 +00:00
github-actions[bot]
40abe1e6d0
i18n - docs translations (#19128)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 16:49:57 +02:00
Raphaël Bosi
6c128a35dd
Fix cropped calendar event name (#19126)
## Before
<img width="3024" height="1480" alt="CleanShot 2026-03-30 at 16 17 13
2@2x"
src="https://github.com/user-attachments/assets/0ec774a6-5e28-440a-93d5-1e91ea9c4c5a"
/>

## After
<img width="3024" height="1482" alt="CleanShot 2026-03-30 at 16 16
56@2x"
src="https://github.com/user-attachments/assets/0a15a6a4-be6f-4e22-b00b-548df9e73e2d"
/>
2026-03-30 14:35:39 +00:00
Paul Rastoin
c0086646fd
[APP] Stricter API assertions (#19114)
# Introduction

Improved the ci assertions to be sure the default logic function worked
fully

## what's next
About to introduce a s3 + lambda e2e test to cover the lambda driver too
in addition of the local driver
2026-03-30 14:18:45 +00:00
Charles Bochet
08b041633a
chore: remove 1-19 upgrade commands and navigation menu item feature flags (#19083)
## Summary
- Deletes the entire `1-19` upgrade version command directory (7 files,
~1500 lines) and removes all references from the upgrade module and
command runner.
- Removes `IS_NAVIGATION_MENU_ITEM_ENABLED` and
`IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED` feature flags from the enum,
seed data, generated schema files, and test mocks. These flags had no
remaining feature-gated code — navigation menu items are now always
enabled.
2026-03-30 14:15:46 +00:00
neo773
2fccd29ec6
messaging cleanup (#19124) 2026-03-30 16:21:00 +02:00
github-actions[bot]
30bdc24bf8
i18n - translations (#19125)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 16:08:36 +02:00
Etienne
d2cf05f4f4
Fix - Not shared message on record index (#19103)
quality-feedbacks :
https://discord.com/channels/1130383047699738754/1486711185436053514/1486711185436053514
<img width="1000" height="500" alt="Screenshot 2026-03-30 at 09 42 48"
src="https://github.com/user-attachments/assets/fe9027fe-0056-4fec-af51-5b39bb467bb5"
/>
<img width="1000" height="500" alt="Screenshot 2026-03-30 at 09 42 34"
src="https://github.com/user-attachments/assets/8cd05ea0-7eb2-4490-b87e-3a7dcd57add2"
/>

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-03-30 13:53:35 +00:00
Raphaël Bosi
696a202bb9
Fix navigation menu edition (#19115)
# Description

Fix "Already in navbar" false positive in the sidebar object picker:
`getObjectMetadataIdsInDraft` was collecting `objectMetadataId` from all
navigation menu item types (OBJECT, VIEW, RECORD), so having a pinned
view or record for an object type would incorrectly block re-adding that
object. Now only OBJECT-type items contribute to the duplicate check.

# Video QA

## Before


https://github.com/user-attachments/assets/7b853a55-6d7a-444c-92ee-bf6a75d9927c

## After


https://github.com/user-attachments/assets/65e651e2-9df8-45fd-872d-00782d6b6490

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-30 13:50:52 +00:00
Raphaël Bosi
b8374f5531
Fix export view (#19117)
# PR Description

**Export view** command sent { id: { in: [] } } to the server, which
rejects empty arrays. It now falls back to the view's filters instead
when there is no selection, which is the intended behavior.

# Video QA

## Before


https://github.com/user-attachments/assets/3e6263f9-9c66-4f67-a81e-e0571652147d

## After


https://github.com/user-attachments/assets/977a366d-1936-457f-96a1-a5d4fb8ac98e
2026-03-30 13:07:24 +00:00
Charles Bochet
1109b89cd7
fix: use Redis metadata version for GraphQL response cache key (#19111)
## Summary

- Fix stale `ObjectMetadataItems` GraphQL response cache after field
creation by using `request.workspaceMetadataVersion` (sourced from
Redis) instead of `workspace.metadataVersion` (from the potentially
stale CoreEntityCacheService)
- Make the E2E kanban view test selector more robust with a regex match

## Root Cause

The `useCachedMetadata` GraphQL plugin keys cached responses using
`workspace.metadataVersion` from the `CoreEntityCacheService`. When a
field is created:

1. The migration runner increments `metadataVersion` in DB and Redis
2. But the `CoreEntityCacheService` for `WorkspaceEntity` is **not**
invalidated
3. So `request.workspace.metadataVersion` still has the old version
4. The cache key resolves to the old cached response
5. The frontend gets stale metadata without the newly created field

This breaks E2E tests (and likely affects users) - after creating a
custom field, the metadata isn't visible until the workspace entity
cache refreshes.

## Fix

Use `request.workspaceMetadataVersion` (populated from Redis by the
middleware, always up-to-date) as the primary version for cache keys,
falling back to the entity cache version.

## Test plan

- [ ] E2E `create-kanban-view` tests should pass (creating a Select
field and immediately using it in a Kanban view)
- [ ] Verify `ObjectMetadataItems` returns fresh data after field
creation (no stale cache)


Made with [Cursor](https://cursor.com)
2026-03-30 15:01:54 +02:00
github-actions[bot]
107914b437
i18n - docs translations (#19119)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 14:39:38 +02:00
Raphaël Bosi
cb85a1b5a3
Fix record name hover (#19109)
## Before


https://github.com/user-attachments/assets/1972f3ac-17b2-4531-a64e-22f2672de71c

## After


https://github.com/user-attachments/assets/0fffdac6-0fcb-4446-9b7f-1791ec08e2df
2026-03-30 13:15:02 +02:00
neo773
ef8789fad6
fix: convert empty parentFolderId to null in messageFolder core dual-write (#19110) 2026-03-30 13:11:38 +02:00
Raphaël Bosi
ee479001a1
Fix workspace dropdown (#19106)
## Before


https://github.com/user-attachments/assets/2ebc682d-f868-4a8b-8a1a-fe304222652e

## After


https://github.com/user-attachments/assets/e6fddab2-b5a1-4941-b60f-f45ad2bfaadd
2026-03-30 12:56:23 +02:00
github-actions[bot]
6af7e32c54
i18n - docs translations (#19113)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 12:45:45 +02:00
Abdullah.
d246b16063
More parts of the new website. (#19094)
This PR brings in a few more components and some refactor of the
existing components for the website due to design requirements.
2026-03-30 12:00:32 +02:00
github-actions[bot]
0b2f435b3e
i18n - translations (#19108)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 11:00:52 +02:00
martmull
039a04bc2f
Fix warning about ../../tsconfig.base.json not found (#19105)
as title
2026-03-30 10:56:59 +02:00
martmull
fe1377f18b
Provide applicatiion assets (#18973)
- improve backend
- improve frontend

<img width="1293" height="824" alt="image"
src="https://github.com/user-attachments/assets/7a4633f1-85cd-4126-b058-dbeae6ba2218"
/>
2026-03-30 10:53:31 +02:00
github-actions[bot]
58189e1c05
chore: sync AI model catalog from models.dev (#19100)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-03-30 08:31:25 +02:00
Charles Bochet
a3f468ef98
chore: remove IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED and IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED feature flags (#19082)
## Summary
- Removes `IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED` feature flag,
making row-level permission predicates always enabled. Removes
early-return guards from query builders (select, update, insert) and the
shared utility, the public feature flag metadata entry, and
`updateFeatureFlag` calls from integration tests.
- Removes `IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED` feature flag, making
whole-day datetime filtering always enabled. Simplifies filter input
components and hooks to always use date-only format for `IS` operand on
`DATE_TIME` fields.
- Cleans up enum definitions, seed data, generated schema files, and
test mocks for both flags.
2026-03-29 12:26:08 +02:00
Charles Bochet
4d74cc0a28
chore: remove IS_APPLICATION_ENABLED feature flag (#19081)
## Summary
- Remove the `IS_APPLICATION_ENABLED` feature flag — application
features are now always enabled
- Remove `@RequireFeatureFlag` decorators and `FeatureFlagGuard` from 7
application resolvers (registration, development, manifest, install,
upgrade, marketplace, oauth)
- Remove frontend feature flag checks: settings navigation visibility,
route protection wrapper, side panel widget type gating, and role
permission flag filtering
- Delete the integration test for disabled-flag behavior
- Clean up seed data, test mocks, and generated schema files
2026-03-29 11:43:35 +02:00
github-actions[bot]
05c0713474
i18n - translations (#19080)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-29 10:51:48 +02:00
Charles Bochet
28de34cf20
chore: remove IS_DASHBOARD_V2_ENABLED feature flag (#19079)
## Summary
- Remove the `IS_DASHBOARD_V2_ENABLED` feature flag from the codebase
- Dashboard V2 features (gauge charts, line charts, pie charts) are now
always enabled
- Remove the validator gate that blocked gauge chart creation/update
without the flag
- Clean up all related code: seed data, dev-seeder service, widget
seeds, and test mocks
2026-03-29 10:45:54 +02:00
Charles Bochet
e8cb086b64
chore: remove completed migration feature flags and upgrade commands <= 1.18 (#19074)
## Summary

- Remove 3 completed migration feature flags: `IS_ATTACHMENT_MIGRATED`,
`IS_NOTE_TARGET_MIGRATED`, `IS_TASK_TARGET_MIGRATED` — these were
already enabled by default for all new workspaces via
`DEFAULT_FEATURE_FLAGS`
- Delete all upgrade command directories for versions <= 1.18 (`1-16/`,
`1-17/`, `1-18/`) along with their module registrations, removing ~6,600
lines of dead migration code
- Simplify frontend utility functions
(`getActivityTargetObjectFieldIdName`, `getActivityTargetsFilter`,
`getActivityTargetFieldNameForObject`,
`generateActivityTargetMorphFieldKeys`,
`findActivitiesOperationSignatureFactory`) by removing the
`isMorphRelation` parameter and always using the morph relation path
- Remove feature flag checks from 7 frontend hooks/components that were
gating attachment and activity target behavior behind the removed flags
- Simplify `buildDefaultRelationFlatFieldMetadatasForCustomObject`
server util to always treat attachment, noteTarget, and taskTarget as
morph relations without checking feature flags
2026-03-29 09:15:26 +02:00
github-actions[bot]
f651413297
chore: sync AI model catalog from models.dev (#19078)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-03-29 08:20:40 +02:00
github-actions[bot]
ccaea1ba36
i18n - translations (#19076)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-29 07:37:29 +02:00
Félix Malfait
a51b5ed589
fix: resolve settings/usage chart crash and add ClickHouse usage event seeds (#19039)
## Summary

- **Fix settings/usage page crash**: The `GraphWidgetLineChart`
component used on `settings/usage` was crashing with "Instance id is not
provided and cannot be found in context" because it requires
`WidgetComponentInstanceContext` (for tooltip/crosshair component
states) which is only provided inside the widget system. Wraps the
standalone chart usages with the required context provider.
- **Avoid mounting `GraphWidgetLegend` when hidden**: The legend
component calls `useIsPageLayoutInEditMode()` which requires
`PageLayoutEditModeProviderContext` — another context only available
inside the widget system. Since the settings page passes
`showLegend={false}`, the fix conditionally unmounts the legend instead
of always mounting it with a `show` prop. Applied consistently across
all four chart types (line, bar, pie, gauge).
- **Add ClickHouse usage event seeds**: Generates ~400 realistic
`usageEvent` rows spanning the past 35 days with weighted user activity,
weekday/weekend patterns, and gradual ramp-up. Enables developers to see
the usage analytics page with data locally.

## Test plan

- [ ] Navigate to `settings/usage` — page should render without errors
- [ ] Verify the daily usage line chart displays correctly
- [ ] Navigate to a user detail page from the usage list
- [ ] Verify the user detail chart renders without errors
- [ ] Run `npx nx clickhouse:seed twenty-server` and confirm usage
events are seeded
- [ ] Verify chart legend still works correctly on dashboard widgets (no
regression)


Made with [Cursor](https://cursor.com)
2026-03-29 07:31:36 +02:00
github-actions[bot]
6efd9ad26e
i18n - translations (#19073)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-28 22:59:16 +01:00
Charles Bochet
c407341912
feat: optimize hot database queries with multi-layer caching (#19068)
## Summary

Introduces multi-layer caching for the 5 most frequent database queries
identified in production (Sentry data), targeting the JWT authentication
hot path and cron job logic.

### Problem
Our database is under heavy load from uncached queries on the auth hot
path:
- `WorkspaceEntity` lookups: **638 queries/min**
- `ApiKeyEntity` lookups: **491 queries/min**
- `UserEntity` lookups: **147 queries/min**
- `UserWorkspaceEntity` lookups: **143 queries/min**
- `LogicFunctionEntity` lookups: **1800 queries/min** (cron job)

### Solution

**1. New `CoreEntityCacheService`** for non-workspace-scoped entities
(Workspace, User, UserWorkspace):
- Mirrors `WorkspaceCacheService` architecture (in-process Map + Redis
with hash validation)
- Provider pattern with `@CoreEntityCache` decorator
- Keyed by entity primary key (not workspaceId)
- 100ms local TTL, Redis-backed hash validation for cross-instance
consistency
- Three providers: `WorkspaceEntityCacheProviderService`,
`UserEntityCacheProviderService`,
`UserWorkspaceEntityCacheProviderService`

**2. New `apiKeyMap` WorkspaceCache** for workspace-scoped API key
lookups:
- `WorkspaceApiKeyMapCacheService` loads all API keys for a workspace
into a map by ID
- Leverages existing `WorkspaceCacheService` infrastructure
- Cache invalidation on API key create/update/revoke

**3. `CronTriggerCronJob` refactored** to use existing
`flatLogicFunctionMaps` workspace cache:
- Eliminates per-workspace `LogicFunctionEntity` repository queries
(~1800/min)
- Filters cached data in-memory instead

**4. `JwtAuthStrategy` refactored** to use caches for all entity
lookups:
- Workspace, User, UserWorkspace → `CoreEntityCacheService`
- ApiKey → `WorkspaceCacheService` (`apiKeyMap`)
- Impersonation queries kept as direct DB queries (rare path, requires
relations)

**5. Cache invalidation** wired into mutation paths:
- `WorkspaceService` → invalidates `workspaceEntity` on
save/update/delete
- `ApiKeyService` → invalidates `apiKeyMap` on create/update/revoke

### Architecture

```
Request → JwtAuthStrategy
  ├── Workspace lookup → CoreEntityCacheService (in-process → Redis → DB)
  ├── User lookup → CoreEntityCacheService (in-process → Redis → DB)
  ├── UserWorkspace lookup → CoreEntityCacheService (in-process → Redis → DB)
  └── ApiKey lookup → WorkspaceCacheService (in-process → Redis → DB)

CronTriggerCronJob
  └── LogicFunction lookup → WorkspaceCacheService (flatLogicFunctionMaps)
```

### Expected Impact
| Query | Before | After |
|-------|--------|-------|
| WorkspaceEntity | 638/min | ~0 (cached) |
| ApiKeyEntity | 491/min | ~0 (cached) |
| UserEntity | 147/min | ~0 (cached) |
| UserWorkspaceEntity | 143/min | ~0 (cached) |
| LogicFunctionEntity | 1800/min | ~0 (cached) |

### Not included (ongoing separately)
- DataSourceEntity query optimization (IS_DATASOURCE_MIGRATED migration)
- ObjectMetadataEntity query optimization (already partially cached)
2026-03-28 22:53:34 +01:00
Charles Bochet
81fc960712
Deprecate dataSource table with dual-write to workspace.databaseSchema (#19059)
## Summary
- Starts deprecation of the `core.dataSource` table by introducing a
dual-write system: `DataSourceService.createDataSourceMetadata` now
writes to both `core.dataSource` and `core.workspace.databaseSchema`
- Migrates read sites (`WorkspaceDataSourceService.checkSchemaExists`,
`WorkspaceSchemaFactory`, `MiddlewareService`,
`WorkspacesMigrationCommandRunner`) to read from
`workspace.databaseSchema` instead of querying the `dataSource` table
- Removes the unused `databaseUrl` field from `WorkspaceEntity` and
drops the column via migration
- Adds a 1.20 upgrade command to backfill `workspace.databaseSchema`
from `dataSource.schema` for existing workspaces
2026-03-28 11:29:19 +01:00
Charles Bochet
cb44b22e15
Fix INDEX view showing labelPlural instead of resolved view name in nav (#19049)
## Summary
- Navigation sidebar was displaying "Notes" instead of "All Notes" for
INDEX views
- `getNavigationMenuItemLabel` had a special case for INDEX views that
returned `objectMetadataItem.labelPlural` instead of the
already-resolved `view.name`
- Since `viewsSelector` already resolves `{objectLabelPlural}` templates
via `resolveViewNamePlaceholders`, the INDEX special case was redundant
and incorrect — removed it from both `getNavigationMenuItemLabel` and
`getViewNavigationMenuItemLabel`
- Added unit tests covering all navigation menu item type branches

<img width="222" height="479" alt="image"
src="https://github.com/user-attachments/assets/de6f92b1-e0c2-445a-a145-cf2820434af7"
/>
2026-03-27 17:25:52 +00:00
github-actions[bot]
9498a60f74
i18n - translations (#19048)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-27 17:39:22 +01:00
Charles Bochet
191a277ddf
fix: invalidate rolesPermissions cache + add Docker Hub auth to CI (#19044)
## Summary

### Cache invalidation fix
- After migrating object/field permissions to syncable entities (#18609,
#18751, #18567), changes to `flatObjectPermissionMaps`,
`flatFieldPermissionMaps`, or `flatPermissionFlagMaps` no longer
triggered `rolesPermissions` cache invalidation
- This caused stale permission data to be served, leading to flaky
`permissions-on-relations` integration tests and potentially incorrect
permission enforcement in production after object permission upserts
- Adds the three permission-related flat map keys to the condition that
triggers `rolesPermissions` cache recomputation in
`WorkspaceMigrationRunnerService.getLegacyCacheInvalidationPromises`
- Clears memoizer after recomputation to prevent concurrent
`getOrRecompute` calls from caching stale data

### Docker Hub rate limit fix
- CI service containers (postgres, redis, clickhouse) and `docker
run`/`docker build` steps were pulling from Docker Hub
**unauthenticated**, hitting the 100-pull-per-6-hour rate limit on
shared GitHub-hosted runner IPs
- Adds `credentials` blocks to all service container definitions and
`docker/login-action` steps before `docker run`/`docker compose`
commands
- Uses `vars.DOCKERHUB_USERNAME` + `secrets.DOCKERHUB_PASSWORD`
(matching the existing twenty-infra convention)
- Affected workflows: ci-server, ci-merge-queue, ci-breaking-changes,
ci-zapier, ci-sdk, ci-create-app-e2e, ci-website,
ci-test-docker-compose, preview-env-keepalive, spawn-twenty-docker-image
action
2026-03-27 17:32:53 +01:00
Baptiste Devessier
7f1814805d
Fix backfill record page layout command (#19043)
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-27 16:56:30 +01:00
github-actions[bot]
34b81adce8
i18n - translations (#19046)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-27 16:38:08 +01:00
Félix Malfait
cd2e08b912
Enforce workspace count limit for multi-workspace setups (#19036)
## Summary
- Limits workspace creation to 5 workspaces per server when no valid
enterprise key is configured
- Enterprise key validity is checked first (synchronous, in-memory
cached) to avoid unnecessary DB queries on enterprise deployments
- Adds 4 unit tests covering: limit enforcement, enterprise key bypass,
below-limit creation, and performance (no enterprise check when
workspace count is zero)

## Test plan
- [x] All 11 unit tests pass (8 existing + 3 new behavioral + 1
performance assertion)
- [ ] Manual: verify workspace creation blocked at limit=5 without
enterprise key
- [ ] Manual: verify workspace creation allowed beyond limit with valid
enterprise key
- [ ] Manual: verify first workspace (bootstrap) creation is unaffected


Made with [Cursor](https://cursor.com)
2026-03-27 16:31:27 +01:00
Charles Bochet
fb21f3ccf5
fix: resolve availabilityObjectMetadataUniversalIdentifier in backfill command (#19041)
## Summary

- Fixes workflow manual trigger command menu items losing their
`availabilityObjectMetadataId` during the 1-20 backfill upgrade command
- The migration runner's `transpileUniversalActionToFlatAction` resolves
`availabilityObjectMetadataId` **from**
`availabilityObjectMetadataUniversalIdentifier`, overwriting the
original value. The backfill was hardcoding the universal identifier to
`null`, causing all `RECORD_SELECTION` items to end up with `NULL`
`availabilityObjectMetadataId` after persistence.
- Now properly resolves `availabilityObjectMetadataUniversalIdentifier`
from `flatObjectMetadataMaps.universalIdentifierById` so the runner can
correctly derive the FK during INSERT.
2026-03-27 16:01:31 +01:00
Paul Rastoin
281bb6d783
Guard yarn database:migrate:prod (#19008)
## Motivations
A lot of self hosters hands up using the `yarn database:migrated:prod`
either manually or through AI assisted debug while they try to upgrade
an instance while their workspace is still blocked in a previous one
Leading to their whole database permanent corruption

## What happened
Replaced the direct call the the typeorm cli to a command calling it
programmatically, adding a layer of security in case a workspace seems
to be blocked in a previous version than the one just before the one
being installed ( e.g 1.0 when you try to upgrade from 1.1 to 1.2 )

For our cloud we still need a way to bypass this security explaining the
-f flag

## Remark
Centralized this logic and refactored creating new services
`WorkspaceVersionService` and `CoreEngineVersionService` that will
become useful for the upcoming upgrade refactor

Related to https://github.com/twentyhq/twenty-infra/pull/529
2026-03-27 14:39:18 +00:00
github-actions[bot]
c96c034908
i18n - translations (#19040)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-27 14:53:33 +01:00
Abdul Rahman
5efe69f8d3
Migrate field permission to syncable entity (#18751)
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-27 14:47:27 +01:00
neo773
c2b058a6a7
fix: use workspace-generated id for core dual-write in message folder save (#19038)
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-27 13:53:30 +01:00
Abdullah.
22c9693ce5
First PR to bring in the new twenty website. (#19035)
This PR contains Menu, Hero, TrustedBy, Problem, ThreeCards and Footer
sections of the new website.

Most components in there match the Figma designs, except for two things.
- Zoom levels on 3D illustrations from Endless Tools.
- Menu needs to have the same color as Hero - it's not happening at the
moment since Menu is in the layout, not nested inside pages or Hero.

Images are placeholders (same as Figma).
2026-03-27 13:48:03 +01:00
Thomas des Francs
50ea560e57
Seed company workflow for email upserts (#18909)
@thomtrp

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-27 13:47:07 +01:00
Etienne
56056b885d
refacto - remove hello-pangea/dnd from navigation-menu-item module (#19033)
Removed all @hello-pangea/dnd imports from the navigation-menu-item/
module by replacing the type bridge layer with a native
NavigationMenuItemDropResult type. The dnd-kit events were already
powering all DnD — hello-pangea was only used as a type contract and one
dead <Droppable> component.
3 files deleted (dead <Droppable> wrapper, DROP_RESULT_OPTIONS shim,
toDropResult bridge), 4 files edited (handler signatures simplified,
bridge removed from orchestrator, utility type narrowed), 1 type
created. Zero behavioral changes, typecheck and tests pass.
2026-03-27 11:23:25 +00:00
Paul Rastoin
db9da3194f
Refactor client auto heal (#19032) 2026-03-27 09:23:26 +00:00
BugIsGod
68f5e70ade
fix: sign file URLs in timeline and file loss on click outside (#19001)
Fixes: #18943
## Problem

  Two bugs related to the Files field:

1. **File loss on click outside**: When `MultiItemFieldInput` was not in
edit mode (input hidden), clicking outside would still call
`validateInputAndComputeUpdatedItems()` with an empty `inputValue` and
`itemToEditIndex = 0`, causing the first file to be silently deleted.
<img width="624" height="332" alt="image"
src="https://github.com/user-attachments/assets/657aff2c-e497-4685-b8b7-fa22aba772f8"
/>


2. **Unsigned file URLs in timeline activity**: FileId, FileName stored
in `timelineActivity.properties.diff` (before/after values) were not
being signed (timeActivity.properties is stored as json in database),
making them inaccessible from the
frontend.

<img width="1092" height="103" alt="image"
src="https://github.com/user-attachments/assets/23d83ea3-4eb9-41ef-a99c-c4515d1cfb35"
/>


 ## Reproduction


https://github.com/user-attachments/assets/e75b842b-5cbb-46e8-a923-ac9df62deb98

## Changes
- `MultiItemFieldInput.tsx`: Wrap the validate + onChange logic in `if
(isInputDisplayed)` so it only runs when the input is actually open.
- `timeline-activity-query-result-getter.handler.ts`: New handler that
iterates over `properties.diff` fields and signs any file arrays found
in `before`/`after` values (call same method:
`fileUrlService.signFileByIdUrl` as table field view
`FilesFieldQueryResultGetterHandler`.
- `common-result-getters.service.ts`: Register the new handler for
`timelineActivity`.


## After 


https://github.com/user-attachments/assets/368c7be1-3101-43a2-ac93-fe1b0d8a7a37
2026-03-27 08:28:30 +00:00
Félix Malfait
08077476f3
fix: remove remaining direct cookie writes that make tokenPair a session cookie on renewal (#19031)
## Summary

- Removes two remaining direct `cookieStorage.setItem('tokenPair', ...)`
calls that were overwriting the Jotai-managed cookie (180-day expiry)
with a session cookie (no expiry) during **token renewal**
- Followup to #18795 which fixed the same issue in `handleSetAuthTokens`
but missed the renewal code paths

## Root cause

Two token renewal paths still had direct cookie writes without
`expires`:

1. **`apollo.factory.ts`** — `attemptTokenRenewal()` fires on every
`UNAUTHENTICATED` GraphQL error after a successful token refresh
2. **`useAgentChat.ts`** — `retryFetchWithRenewedToken()` fires on 401
from the AI chat endpoint

Both called `cookieStorage.setItem('tokenPair', JSON.stringify(tokens))`
without an `expires` attribute, creating a session cookie that overwrote
the Jotai-managed one. This is why the bug was **intermittent after
#18795**: it only appeared after a token renewal, not on fresh login.

The `onTokenPairChange` / `setTokenPair` calls already write through
Jotai's `atomWithStorage` → `createJotaiCookieStorage`, which always
sets `expires: 180 days`.

## Test plan

- Log in to the app
- Wait for a token renewal to occur (or force one by letting the access
token expire)
- Inspect the `tokenPair` cookie in DevTools → Application → Cookies
- Verify the cookie retains an expiration date ~180 days from now (not
"Session")
- Close and reopen the browser — confirm you remain logged in


Made with [Cursor](https://cursor.com)
2026-03-27 08:21:26 +00:00
Rayan Salhab
6f0ac88e20
fix: batch viewGroup mutations sequentially to prevent race conditions (#19027)
## Bug Description

When reordering stages in the Kanban board, the frontend fires all
viewGroup update mutations concurrently via Promise.all, causing race
conditions in the workspace migration runner's cache invalidation,
database contention, and a thundering herd effect that stalls the
server.

## Changes

Changed `usePerformViewGroupAPIPersist` to execute viewGroup update
mutations sequentially instead of concurrently. The `Promise.all`
pattern fired all N mutations simultaneously, each triggering a full
workspace migration runner pipeline (transaction + cache invalidation).
The sequential `for...of` loop ensures each mutation completes
(including its cache invalidation) before the next begins, eliminating
the race condition.

## Related Issue

Fixes #18865

## Testing

This fix addresses the root cause identified in the Sonarly analysis on
the issue. The concurrent mutation pattern was causing:
- PostgreSQL row-level lock contention on viewGroup rows
- Cache thundering herd from repeated invalidation/recomputation cycles
- Server stalls requiring container restarts

The sequential approach ensures proper ordering and prevents these race
conditions.

---------

Co-authored-by: Rayan <rayan@example.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-27 08:13:51 +00:00
6073 changed files with 352715 additions and 206100 deletions

22
.claude-pr/.mcp.json Normal file
View file

@ -0,0 +1,22 @@
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "bash",
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
"env": {}
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["@playwright/mcp@latest", "--no-sandbox", "--headless"],
"env": {}
},
"context7": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"],
"env": {}
}
}
}

223
.claude-pr/CLAUDE.md Normal file
View file

@ -0,0 +1,223 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Twenty is an open-source CRM built with modern technologies in a monorepo structure. The codebase is organized as an Nx workspace with multiple packages.
## Key Commands
### Development
```bash
# Start development environment (frontend + backend + worker)
yarn start
# Individual package development
npx nx start twenty-front # Start frontend dev server
npx nx start twenty-server # Start backend server
npx nx run twenty-server:worker # Start background worker
```
### Testing
```bash
# Preferred: run a single test file (fast)
npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
# Run all tests for a package
npx nx test twenty-front # Frontend unit tests
npx nx test twenty-server # Backend unit tests
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
# To run an indivual test or a pattern of tests, use the following command:
cd packages/{workspace} && npx jest "pattern or filename"
# Storybook
npx nx storybook:build twenty-front
npx nx storybook:test twenty-front
# When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
```
### Code Quality
```bash
# Linting (diff with main - fastest, always prefer this)
npx nx lint:diff-with-main twenty-front
npx nx lint:diff-with-main twenty-server
npx nx lint:diff-with-main twenty-front --configuration=fix # Auto-fix
# Linting (full project - slower, use only when needed)
npx nx lint twenty-front
npx nx lint twenty-server
# Type checking
npx nx typecheck twenty-front
npx nx typecheck twenty-server
# Format code
npx nx fmt twenty-front
npx nx fmt twenty-server
```
### Build
```bash
# Build packages (twenty-shared must be built first)
npx nx build twenty-shared
npx nx build twenty-front
npx nx build twenty-server
```
### Database Operations
```bash
# Database management
npx nx database:reset twenty-server # Reset database
npx nx run twenty-server:database:init:prod # Initialize database
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
# Generate an instance command (fast or slow)
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
```
### Database Inspection (Postgres MCP)
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
- Inspect workspace data, metadata, and object definitions while developing
- Verify migration results (columns, types, constraints) after running migrations
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
- Inspect metadata tables to debug GraphQL schema generation issues
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
### GraphQL
```bash
# Generate GraphQL types (run after schema changes)
npx nx run twenty-front:graphql:generate
npx nx run twenty-front:graphql:generate --configuration=metadata
```
## Architecture Overview
### Tech Stack
- **Frontend**: React 18, TypeScript, Jotai (state management), Linaria (styling), Vite
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
- **Monorepo**: Nx workspace managed with Yarn 4
### Package Structure
```
packages/
├── twenty-front/ # React frontend application
├── twenty-server/ # NestJS backend API
├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email
├── twenty-website/ # Next.js documentation website
├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests
```
### Key Development Principles
- **Functional components only** (no class components)
- **Named exports only** (no default exports)
- **Types over interfaces** (except when extending third-party interfaces)
- **String literals over enums** (except for GraphQL enums)
- **No 'any' type allowed** — strict TypeScript enforced
- **Event handlers preferred over useEffect** for state updates
- **Props down, events up** — unidirectional data flow
- **Composition over inheritance**
- **No abbreviations** in variable names (`user` not `u`, `fieldMetadata` not `fm`)
### Naming Conventions
- **Variables/functions**: camelCase
- **Constants**: SCREAMING_SNAKE_CASE
- **Types/Classes**: PascalCase (suffix component props with `Props`, e.g. `ButtonProps`)
- **Files/directories**: kebab-case with descriptive suffixes (`.component.tsx`, `.service.ts`, `.entity.ts`, `.dto.ts`, `.module.ts`)
- **TypeScript generics**: descriptive names (`TData` not `T`)
### File Structure
- Components under 300 lines, services under 500 lines
- Components in their own directories with tests and stories
- Use `index.ts` barrel exports for clean imports
- Import order: external libraries first, then internal (`@/`), then relative
### Comments
- Use short-form comments (`//`), not JSDoc blocks
- Explain WHY (business logic), not WHAT
- Do not comment obvious code
- Multi-line comments use multiple `//` lines, not `/** */`
### State Management
- **Jotai** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
- Component-specific state with React hooks (`useState`, `useReducer` for complex logic)
- GraphQL cache managed by Apollo Client
- Use functional state updates: `setState(prev => prev + 1)`
### Backend Architecture
- **NestJS modules** for feature organization
- **TypeORM** for database ORM with PostgreSQL
- **GraphQL** API with code-first approach
- **Redis** for caching and session management
- **BullMQ** for background job processing
### Database & Upgrade Commands
- **PostgreSQL** as primary database
- **Redis** for caching and sessions
- **ClickHouse** for analytics (when enabled)
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
- Include both `up` and `down` logic in instance commands
- Never delete or rewrite committed instance command `up`/`down` logic
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
### Utility Helpers
Use existing helpers from `twenty-shared` instead of manual type guards:
- `isDefined()`, `isNonEmptyString()`, `isNonEmptyArray()`
## Development Workflow
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
### Before Making Changes
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
2. Test changes with relevant test suites (prefer single-file test runs)
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
4. Check that GraphQL schema changes are backward compatible
5. Run `graphql:generate` after any GraphQL schema changes
### Code Style Notes
- Use **Linaria** for styling with zero-runtime CSS-in-JS (styled-components pattern)
- Follow **Nx** workspace conventions for imports
- Use **Lingui** for internationalization
- Apply security first, then formatting (sanitize before format)
### Testing Strategy
- **Test behavior, not implementation** — focus on user perspective
- **Test pyramid**: 70% unit, 20% integration, 10% E2E
- Query by user-visible elements (text, roles, labels) over test IDs
- Use `@testing-library/user-event` for realistic interactions
- Descriptive test names: "should [behavior] when [condition]"
- Clear mocks between tests with `jest.clearAllMocks()`
## Dev Environment Setup
All dev environments (Claude Code web, Cursor, local) use one script:
```bash
bash packages/twenty-utils/setup-dev-env.sh
```
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times.
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
- `--down` — stop services
- `--reset` — wipe data and restart fresh
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script.
## Important Files
- `nx.json` - Nx workspace configuration with task definitions
- `tsconfig.base.json` - Base TypeScript configuration
- `package.json` - Root package with workspace definitions
- `.cursor/rules/` - Detailed development guidelines and best practices

View file

@ -12,7 +12,7 @@ This directory contains Twenty's development guidelines and best practices in th
### Core Guidelines
- **architecture.mdc** - Project overview, technology stack, and infrastructure setup (Always Applied)
- **nx-rules.mdc** - Nx workspace guidelines and best practices (Auto-attached to Nx files)
- **server-migrations.mdc** - Backend migration and TypeORM guidelines for `twenty-server` (Auto-attached to server entities and migration files)
- **server-migrations.mdc** - Upgrade command guidelines (instance commands and workspace commands) for `twenty-server` (Auto-attached to server entities and upgrade command files)
- **creating-syncable-entity.mdc** - Comprehensive guide for creating new syncable entities (with universalIdentifier and applicationId) in the workspace migration system (Agent-requested for metadata-modules and workspace-migration files)
### Code Quality
@ -81,11 +81,8 @@ npx nx run twenty-server:typecheck # Type checking
npx nx run twenty-server:test # Run unit tests
npx nx run twenty-server:test:integration:with-db-reset # Run integration tests
# Migrations
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts
# Workspace
npx nx run twenty-server:command workspace:sync-metadata -f # Sync metadata
# Upgrade commands (instance + workspace)
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
```
## Usage Guidelines

View file

@ -55,7 +55,8 @@ If feature descriptions are not provided or need enhancement, research the codeb
- Services: Look for `*.service.ts` files
**For Database/ORM Changes:**
- Migrations: `packages/twenty-server/src/database/typeorm/`
- Instance commands (fast/slow): `packages/twenty-server/src/database/commands/upgrade-version-command/`
- Legacy TypeORM migrations: `packages/twenty-server/src/database/typeorm/`
- Entities: `packages/twenty-server/src/entities/`
### Research Commands

View file

@ -22,7 +22,7 @@ This main guide provides a high-level overview and navigation hub.
A syncable entity is a metadata entity that:
- Has a **`universalIdentifier`**: A unique identifier used for syncing entities across workspaces/applications
- Has an **`applicationId`**: Links the entity to an application (Twenty Standard or Custom applications)
- Has an **`applicationId`**: Links the entity to an application (Standard or Custom applications)
- Participates in the **workspace migration system**: Can be created, updated, and deleted through the migration pipeline
- Is **cached as a flat entity**: Denormalized representation for efficient validation and change detection

View file

@ -1,29 +1,46 @@
---
description: Guidelines for generating and managing TypeORM migrations in twenty-server
description: Guidelines for generating and managing upgrade commands (instance commands and workspace commands) in twenty-server
globs: [
"packages/twenty-server/src/**/*.entity.ts",
"packages/twenty-server/src/database/typeorm/**/*.ts"
"packages/twenty-server/src/database/commands/upgrade-version-command/**/*.ts"
]
alwaysApply: false
---
## Server Migrations (twenty-server)
## Upgrade Commands (twenty-server)
- **When changing an entity, always generate a migration**
- If you modify a `*.entity.ts` file in `packages/twenty-server/src`, you **must** generate a corresponding TypeORM migration instead of manually editing the database schema.
- Use the Nx + TypeORM command from the project root:
The upgrade system uses two types of commands instead of raw TypeORM migrations:
- **Instance commands** — schema and data migrations that run once at the instance level.
- **Workspace commands** — commands that iterate over all active/suspended workspaces.
See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation.
### Instance Commands
- **When changing a `*.entity.ts` file**, generate an instance command:
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
```
- Replace `[name]` with a descriptive, kebab-case migration name that reflects the change (for example, `add-agent-turn-evaluation`).
- **Fast commands** (`--type fast`, default) are for schema-only changes that must run immediately. They implement `FastInstanceCommand` with `up`/`down` methods and use the `@RegisteredInstanceCommand` decorator.
- **Prefer generated migrations over manual edits**
- Let TypeORM infer schema changes from the updated entities; only adjust the generated migration file manually if absolutely necessary (for example, for data backfills or complex constraints).
- Keep schema changes (DDL) in these generated migrations and avoid mixing in heavy data migrations unless there is a strong reason and clear comments.
- **Slow commands** (`--type slow`) add a `runDataMigration` method for potentially long-running data backfills that execute before `up`. They only run when `--include-slow` is passed. Use the decorator with `{ type: 'slow' }`.
- **Keep migrations consistent and reversible**
- Ensure the generated migration includes both `up` and `down` logic that correctly applies and reverts the entity change when possible.
- Do not delete or rewrite existing, committed migrations unless you are explicitly working on a pre-release branch where history rewrites are allowed by team conventions.
- The generator auto-registers the command in `instance-commands.constant.ts` — do not edit that file manually.
- **Keep commands consistent and reversible**: include both `up` and `down` logic. Do not delete or rewrite existing, committed commands unless on a pre-release branch.
### Workspace Commands
- Use the `@RegisteredWorkspaceCommand` decorator alongside nest-commander's `@Command` decorator.
- Extend `ActiveOrSuspendedWorkspaceCommandRunner` and implement `runOnWorkspace`.
- The base class provides `--dry-run`, `--verbose`, and workspace filter options automatically.
### Execution Order
Within a given version, commands run in this order (timestamp-sorted within each group):
1. Instance fast commands
2. Instance slow commands (only with `--include-slow`)
3. Workspace commands

View file

@ -0,0 +1,53 @@
name: Deploy Twenty App
description: Build and deploy a Twenty app to a remote instance
inputs:
api-url:
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
required: true
api-key:
description: API key or access token for the target instance
required: true
app-path:
description: Path to the app directory (relative to repo root). Defaults to repo root.
required: false
default: '.'
runs:
using: composite
steps:
- name: Enable Corepack
shell: bash
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn install --immutable
- name: Configure remote
shell: bash
run: |
mkdir -p ~/.twenty
node -e "
const fs = require('fs'), path = require('path'), os = require('os');
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
version: 1,
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY } }
}, null, 2));
"
env:
API_URL: ${{ inputs.api-url }}
API_KEY: ${{ inputs.api-key }}
- name: Deploy
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty deploy --remote target

View file

@ -0,0 +1,53 @@
name: Install Twenty App
description: Install (or upgrade) a Twenty app on a specific workspace
inputs:
api-url:
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
required: true
api-key:
description: API key or access token for the target workspace
required: true
app-path:
description: Path to the app directory (relative to repo root). Defaults to repo root.
required: false
default: '.'
runs:
using: composite
steps:
- name: Enable Corepack
shell: bash
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn install --immutable
- name: Configure remote
shell: bash
run: |
mkdir -p ~/.twenty
node -e "
const fs = require('fs'), path = require('path'), os = require('os');
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
version: 1,
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY } }
}, null, 2));
"
env:
API_URL: ${{ inputs.api-url }}
API_KEY: ${{ inputs.api-key }}
- name: Install
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty install --remote target

View file

@ -0,0 +1,47 @@
name: Spawn Twenty App Dev Test
description: >
Starts a Twenty all-in-one test instance (server, worker, database, redis)
using the twentycrm/twenty-app-dev Docker image on port 2021.
The server is available at http://localhost:2021 with seeded demo data.
inputs:
twenty-version:
description: 'Twenty Docker Hub image tag for twenty-app-dev (e.g., "latest" or "v1.20.0").'
required: false
default: 'latest'
outputs:
server-url:
description: 'URL where the Twenty test server can be reached'
value: http://localhost:2021
api-key:
description: 'API key (type: API_KEY) for the seeded Twenty dev workspace'
value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc
runs:
using: 'composite'
steps:
- name: Start twenty-app-dev-test container
shell: bash
run: |
docker run -d \
--name twenty-app-dev-test \
-p 2021:2021 \
-e NODE_PORT=2021 \
-e SERVER_URL=http://localhost:2021 \
twentycrm/twenty-app-dev:${{ inputs.twenty-version }}
echo "Waiting for Twenty test instance to become healthy…"
TIMEOUT=180
ELAPSED=0
until curl -sf http://localhost:2021/healthz > /dev/null 2>&1; do
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
echo "::error::Twenty did not become healthy within ${TIMEOUT}s"
docker logs twenty-app-dev-test 2>&1 | tail -80
exit 1
fi
sleep 3
ELAPSED=$((ELAPSED + 3))
echo " … waited ${ELAPSED}s"
done
echo "Twenty test instance is ready at http://localhost:2021 (took ~${ELAPSED}s)"

View file

@ -35,12 +35,10 @@ jobs:
runs-on: ubuntu-latest
services:
postgres:
image: twentycrm/twenty-postgres-spilo
image: postgres:18
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
@ -77,6 +75,8 @@ jobs:
run: |
echo "Attempting to merge main into current branch..."
git config user.email "ci@twenty.com"
git config user.name "CI"
git fetch origin main
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
@ -90,8 +90,8 @@ jobs:
echo "❌ Merge failed due to conflicts"
echo "⚠️ Falling back to comparing current branch against main without merge"
# Abort the failed merge
git merge --abort
# Abort the failed merge (may not exist if merge never started)
git merge --abort 2>/dev/null || true
echo "merged=false" >> $GITHUB_OUTPUT
echo "BRANCH_STATE=conflicts" >> $GITHUB_ENV
@ -143,7 +143,6 @@ jobs:
set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword"
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod
- name: Seed current branch database with test data
run: |
@ -252,6 +251,14 @@ jobs:
rm -f /tmp/current-server.pid
fi
- name: Flush Redis between server runs
run: |
# Clear all Redis caches to prevent stale data from the current branch
# server contaminating the main branch server. Both servers share the
# same Redis instance, and CoreEntityCacheService/WorkspaceCacheService
# persist cached entities across process restarts.
redis-cli -h localhost -p 6379 FLUSHALL || echo "::warning::Failed to flush Redis"
- name: Checkout main branch
run: |
git stash
@ -296,7 +303,6 @@ jobs:
set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword"
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod
- name: Seed main branch database with test data
run: |
@ -395,12 +401,30 @@ jobs:
# Clean up temp directory
rm -rf /tmp/current-branch-files
- name: Validate downloaded schema files
id: validate-schemas
run: |
valid=true
for file in main-schema-introspection.json current-schema-introspection.json \
main-metadata-schema-introspection.json current-metadata-schema-introspection.json \
main-rest-api.json current-rest-api.json \
main-rest-metadata-api.json current-rest-metadata-api.json; do
if [ ! -f "$file" ] || ! jq empty "$file" 2>/dev/null; then
echo "::warning::Invalid or missing schema file: $file"
valid=false
fi
done
echo "valid=$valid" >> $GITHUB_OUTPUT
- name: Install OpenAPI Diff Tool
run: |
# Using the Java-based OpenAPITools/openapi-diff via Docker
echo "Using OpenAPITools/openapi-diff via Docker"
- name: Generate GraphQL Schema Diff Reports
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== INSTALLING GRAPHQL INSPECTOR CLI ==="
npm install -g @graphql-inspector/cli
@ -411,7 +435,6 @@ jobs:
echo "Checking GraphQL schema for changes..."
if graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >/dev/null 2>&1; then
echo "✅ No changes in GraphQL schema"
# Don't create a diff file for no changes
else
echo "⚠️ Changes detected in GraphQL schema, generating report..."
echo "# GraphQL Schema Changes" > graphql-schema-diff.md
@ -429,7 +452,6 @@ jobs:
echo "Checking GraphQL metadata schema for changes..."
if graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >/dev/null 2>&1; then
echo "✅ No changes in GraphQL metadata schema"
# Don't create a diff file for no changes
else
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md
@ -448,6 +470,7 @@ jobs:
ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)"
- name: Check REST API Breaking Changes
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== CHECKING REST API FOR BREAKING CHANGES ==="
@ -516,6 +539,7 @@ jobs:
fi
- name: Check REST Metadata API Breaking Changes
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ==="

View file

@ -0,0 +1,201 @@
name: CI Hello world App E2E
on:
# Temporarily disabled — will be re-enabled when example apps are published.
# push:
# branches:
# - main
#
# pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
create-app-e2e-hello-world:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- 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: Set CI version and prepare packages for publish
run: |
CI_VERSION="0.0.0-ci.$(date +%s)"
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx run $pkg:set-local-version --releaseVersion=$CI_VERSION
done
- name: Build packages
run: |
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx build $pkg
done
- name: Install and start Verdaccio
run: |
npx verdaccio --config .github/verdaccio-config.yaml &
for i in $(seq 1 30); do
if curl -s http://localhost:4873 > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
echo "Waiting for Verdaccio... ($i/30)"
sleep 1
done
- name: Publish packages to local registry
run: |
yarn config set npmRegistryServer http://localhost:4873
yarn config set unsafeHttpWhitelist --json '["localhost"]'
yarn config set npmAuthToken ci-auth-token
for pkg in $PUBLISHABLE_PACKAGES; do
cd packages/$pkg
yarn npm publish --tag ci
cd ../..
done
- name: Scaffold app using published create-twenty-app
run: |
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --example hello-world --display-name "Test hello-world app" --description "E2E test hello-world app" --skip-local-instance
- name: Install scaffolded app dependencies
run: |
cd /tmp/e2e-test-workspace/test-app
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
echo "--- Installing last SDK versions ---"
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn add twenty-sdk twenty-client-sdk
- name: Verify installed app versions
run: |
cd /tmp/e2e-test-workspace/test-app
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.dependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
}
console.log('SDK version in scaffolded app:', sdkVersion);
"
- name: Verify SDK CLI is available
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty deploy
- name: Install scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty install
- name: Execute hello-world logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName hello-world-logic-function)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
- name: Execute create-hello-world-company logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-hello-world-company)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
ci-create-app-e2e-hello-world-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-e2e-hello-world]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View file

@ -0,0 +1,171 @@
name: CI Create App E2E minimal
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
create-app-e2e-minimal:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- 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: Set CI version and prepare packages for publish
run: |
CI_VERSION="0.0.0-ci.$(date +%s)"
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
npx nx run-many -t set-local-version -p $PUBLISHABLE_PACKAGES --releaseVersion=$CI_VERSION
- name: Build packages
run: |
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx build $pkg
done
- name: Install and start Verdaccio
run: |
npx verdaccio --config .github/verdaccio-config.yaml &
for i in $(seq 1 30); do
if curl -s http://localhost:4873 > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
echo "Waiting for Verdaccio... ($i/30)"
sleep 1
done
- name: Publish packages to local registry
run: |
yarn config set npmRegistryServer http://localhost:4873
yarn config set unsafeHttpWhitelist --json '["localhost"]'
yarn config set npmAuthToken ci-auth-token
for pkg in $PUBLISHABLE_PACKAGES; do
cd packages/$pkg
yarn npm publish --tag ci
cd ../..
done
- name: Scaffold app using published create-twenty-app
run: |
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --skip-local-instance
- name: Install scaffolded app dependencies
run: |
cd /tmp/e2e-test-workspace/test-app
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
- name: Verify installed app versions
run: |
cd /tmp/e2e-test-workspace/test-app
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.dependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
}
console.log('SDK version in scaffolded app:', sdkVersion);
"
- name: Verify SDK CLI is available
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Run scaffolded app integration test (deploys, installs, and verifies the app)
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
ci-create-app-e2e-minimal-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-e2e-minimal]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View file

@ -0,0 +1,199 @@
name: CI Postcard App E2E
on:
# Temporarily disabled — will be re-enabled when example apps are published.
# push:
# branches:
# - main
#
# pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
create-app-e2e-postcard:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- 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: Set CI version and prepare packages for publish
run: |
CI_VERSION="0.0.0-ci.$(date +%s)"
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
npx nx run-many -t set-local-version -p $PUBLISHABLE_PACKAGES --releaseVersion=$CI_VERSION
- name: Build packages
run: |
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx build $pkg
done
- name: Install and start Verdaccio
run: |
npx verdaccio --config .github/verdaccio-config.yaml &
for i in $(seq 1 30); do
if curl -s http://localhost:4873 > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
echo "Waiting for Verdaccio... ($i/30)"
sleep 1
done
- name: Publish packages to local registry
run: |
yarn config set npmRegistryServer http://localhost:4873
yarn config set unsafeHttpWhitelist --json '["localhost"]'
yarn config set npmAuthToken ci-auth-token
for pkg in $PUBLISHABLE_PACKAGES; do
cd packages/$pkg
yarn npm publish --tag ci
cd ../..
done
- name: Scaffold app using published create-twenty-app
run: |
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --example postcard --display-name "Test postcard app" --description "E2E test postcard app" --skip-local-instance
- name: Install scaffolded app dependencies
run: |
cd /tmp/e2e-test-workspace/test-app
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
echo "--- Installing last SDK versions ---"
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn add twenty-sdk twenty-client-sdk
- name: Verify installed app versions
run: |
cd /tmp/e2e-test-workspace/test-app
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.dependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
}
console.log('SDK version in scaffolded app:', sdkVersion);
"
- name: Verify SDK CLI is available
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty deploy
- name: Install scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty install
- name: Execute postcard logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName postcard-logic-function)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
- name: Execute create-postcard-company logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-postcard-company)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
ci-create-app-e2e-postcard-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-e2e-postcard]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View file

@ -1,202 +0,0 @@
name: CI Create App E2E
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
packages/twenty-server/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
!packages/twenty-server/package.json
create-app-e2e:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
steps:
- 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: Set CI version and prepare packages for publish
run: |
CI_VERSION="0.0.0-ci.$(date +%s)"
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
npx nx run-many -t set-local-version -p $PUBLISHABLE_PACKAGES --releaseVersion=$CI_VERSION
- name: Build packages
run: |
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx build $pkg
done
- name: Install and start Verdaccio
run: |
npx verdaccio --config .github/verdaccio-config.yaml &
for i in $(seq 1 30); do
if curl -s http://localhost:4873 > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
echo "Waiting for Verdaccio... ($i/30)"
sleep 1
done
- name: Publish packages to local registry
run: |
yarn config set npmRegistryServer http://localhost:4873
yarn config set unsafeHttpWhitelist --json '["localhost"]'
yarn config set npmAuthToken ci-auth-token
for pkg in $PUBLISHABLE_PACKAGES; do
cd packages/$pkg
yarn npm publish --tag ci
cd ../..
done
- name: Scaffold app using published create-twenty-app
run: |
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --exhaustive --display-name "Test App" --description "E2E test app" --skip-local-instance
- name: Install scaffolded app dependencies
run: |
cd /tmp/e2e-test-workspace/test-app
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
- name: Verify installed app versions
run: |
cd /tmp/e2e-test-workspace/test-app
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.devDependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
}
console.log('SDK version in scaffolded app:', sdkVersion);
"
- name: Verify SDK CLI is available
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Build server
run: npx nx build twenty-server
- name: Create and setup database
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Start server
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Authenticate with twenty-server
env:
SEED_API_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik'
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --token $SEED_API_KEY --url http://localhost:3000
- name: Deploy scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty deploy
- name: Install scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty install
- name: Execute hello-world logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName hello-world-logic-function)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
- name: Execute create-hello-world-company logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-hello-world-company)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q "Created company"
- name: Run scaffolded app integration test
env:
TWENTY_API_URL: http://localhost:3000
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
ci-create-app-e2e-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-e2e]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View file

@ -0,0 +1,18 @@
name: CI Cross-Version Upgrade
permissions:
contents: read
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch:
jobs:
cross-version-upgrade:
timeout-minutes: 45
runs-on: ubuntu-latest
steps:
- name: Placeholder
run: echo "Cross-version upgrade CI — not yet implemented"

View file

@ -0,0 +1,94 @@
name: CI Example App Hello World
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-apps/examples/hello-world/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
example-app-hello-world:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build SDK packages
run: npx nx build twenty-sdk
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Run integration tests
working-directory: packages/twenty-apps/examples/hello-world
run: npx vitest run
ci-example-app-hello-world-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, example-app-hello-world]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View file

@ -0,0 +1,94 @@
name: CI Example App Postcard
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-apps/examples/postcard/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
example-app-postcard:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build SDK packages
run: npx nx build twenty-sdk
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Run integration tests
working-directory: packages/twenty-apps/examples/postcard
run: npx vitest run
ci-example-app-postcard-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, example-app-postcard]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View file

@ -0,0 +1,114 @@
name: CI Front Component Renderer
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
yarn.lock
packages/twenty-front-component-renderer/**
packages/twenty-sdk/**
packages/twenty-shared/**
!packages/twenty-sdk/package.json
renderer-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [build, typecheck, lint]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- 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: Run ${{ matrix.task }}
run: npx nx ${{ matrix.task }} twenty-front-component-renderer
renderer-sb-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- 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 storybook
run: npx nx storybook:build twenty-front-component-renderer
- name: Upload storybook build
uses: actions/upload-artifact@v4
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
retention-days: 1
renderer-sb-test:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: renderer-sb-build
env:
STORYBOOK_URL: http://localhost:6008
steps:
- 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 dependencies
run: npx nx build twenty-sdk
- name: Download storybook build
uses: actions/download-artifact@v4
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
- name: Install Playwright
run: |
cd packages/twenty-front-component-renderer
npx playwright install
- name: Serve storybook & run tests
run: |
npx http-server packages/twenty-front-component-renderer/storybook-static --port 6008 --silent &
timeout 30 bash -c 'until curl -sf http://localhost:6008 > /dev/null 2>&1; do sleep 1; done'
npx nx storybook:test twenty-front-component-renderer
ci-front-component-renderer-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs:
[
changed-files-check,
renderer-task,
renderer-sb-build,
renderer-sb-test,
]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View file

@ -25,6 +25,7 @@ jobs:
package.json
yarn.lock
packages/twenty-front/**
packages/twenty-front-component-renderer/**
packages/twenty-ui/**
packages/twenty-shared/**
packages/twenty-sdk/**
@ -93,7 +94,7 @@ jobs:
run: |
npx nx build twenty-shared
npx nx build twenty-ui
npx nx build twenty-sdk
npx nx build twenty-front-component-renderer
- name: Download storybook build
uses: actions/download-artifact@v4
with:
@ -157,7 +158,7 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max-old-space-size=4096'
NODE_OPTIONS: '--max-old-space-size=6144'
TASK_CACHE_KEY: front-task-${{ matrix.task }}
strategy:
matrix:

View file

@ -25,12 +25,10 @@ jobs:
NODE_OPTIONS: "--max-old-space-size=10240"
services:
postgres:
image: twentycrm/twenty-postgres-spilo
image: postgres:18
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: "true"
SPILO_PROVIDER: "local"
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-

View file

@ -19,6 +19,7 @@ jobs:
files: |
packages/twenty-sdk/**
packages/twenty-server/**
.github/workflows/ci-sdk.yaml
!packages/twenty-sdk/package.json
sdk-test:
needs: changed-files-check
@ -27,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test:unit, storybook:build, storybook:test, test:integration]
task: [lint, typecheck, test:unit, test:integration]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
@ -41,9 +42,6 @@ jobs:
uses: ./.github/actions/yarn-install
- name: Build
run: npx nx build twenty-sdk
- name: Install Playwright
if: contains(matrix.task, 'storybook')
run: npx playwright install chromium
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
@ -56,12 +54,10 @@ jobs:
if: needs.changed-files-check.outputs.any_changed == 'true'
services:
postgres:
image: twentycrm/twenty-postgres-spilo
image: postgres:18
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
@ -74,7 +70,8 @@ jobs:
ports:
- 6379:6379
env:
NODE_ENV: test
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
@ -82,16 +79,26 @@ jobs:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
- name: Build SDK
run: npx nx build twenty-sdk
- name: Server / Create Test DB
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server > /tmp/twenty-server.log 2>&1 &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: SDK / Run e2e Tests
uses: ./.github/actions/nx-affected
with:
tag: scope:sdk
tasks: test:e2e
run: NODE_ENV=test npx vitest run --config ./vitest.e2e.config.ts
working-directory: packages/twenty-sdk
- name: Server / Dump logs on failure
if: failure()
run: tail -100 /tmp/twenty-server.log || echo "No server log file found"
ci-sdk-status-check:
if: always() && !cancelled()
timeout-minutes: 5

View file

@ -25,6 +25,7 @@ jobs:
packages/twenty-server/**
packages/twenty-front/src/generated/**
packages/twenty-front/src/generated-metadata/**
packages/twenty-front/src/generated-admin/**
packages/twenty-client-sdk/**
packages/twenty-emails/**
packages/twenty-shared/**
@ -83,12 +84,10 @@ jobs:
runs-on: ubuntu-latest
services:
postgres:
image: twentycrm/twenty-postgres-spilo
image: postgres:18
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
@ -122,7 +121,6 @@ jobs:
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod
- name: Worker / Run
run: |
timeout 30s npx nx run twenty-server:worker || exit_code=$?
@ -147,15 +145,18 @@ jobs:
exit 1
- name: Server / Check for Pending Migrations
run: |
CORE_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate core-migration-check -d src/database/typeorm/core/core.datasource.ts || true)
npx nx database:migrate:generate twenty-server -- --name pending-migration-check || true
CORE_MIGRATION_FILE=$(ls packages/twenty-server/*core-migration-check.ts 2>/dev/null || echo "")
if ! git diff --quiet; then
echo "::error::Unexpected migration files were generated. Please run 'npx nx database:migrate:generate twenty-server -- --name <migration-name>' and commit the result."
echo ""
echo "The following migration changes were detected:"
echo "==================================================="
git diff
echo "==================================================="
echo ""
if [ -n "$CORE_MIGRATION_FILE" ]; then
echo "::error::Unexpected migration files were generated. Please create a proper migration manually."
echo "$CORE_MIGRATION_OUTPUT"
rm -f packages/twenty-server/*core-migration-check.ts
git checkout -- .
exit 1
fi
@ -165,13 +166,14 @@ jobs:
npx nx run twenty-front:graphql:generate
npx nx run twenty-front:graphql:generate --configuration=metadata
npx nx run twenty-front:graphql:generate --configuration=admin
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."
if ! git diff --quiet -- packages/twenty-front/src/generated packages/twenty-front/src/generated-metadata packages/twenty-front/src/generated-admin; then
echo "::error::GraphQL schema changes detected. Please run the three graphql:generate configurations ('data', 'metadata', 'admin') and commit the changes."
echo ""
echo "The following GraphQL schema changes were detected:"
echo "==================================================="
git diff -- packages/twenty-front/src/generated packages/twenty-front/src/generated-metadata
git diff -- packages/twenty-front/src/generated packages/twenty-front/src/generated-metadata packages/twenty-front/src/generated-admin
echo "==================================================="
echo ""
HAS_ERRORS=true
@ -226,12 +228,10 @@ jobs:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
services:
postgres:
image: twentycrm/twenty-postgres-spilo
image: postgres:18
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-

View file

@ -27,6 +27,11 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Run compose
run: |
echo "Patching docker-compose.yml..."
@ -97,6 +102,11 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Create frontend placeholder
run: |
mkdir -p packages/twenty-front/build
@ -111,7 +121,7 @@ jobs:
- name: Start container
run: |
docker run -d --name twenty-app-dev \
-p 3000:3000 \
-p 2020:2020 \
twenty-app-dev-ci
docker logs twenty-app-dev -f &
- name: Wait for server health
@ -119,10 +129,10 @@ jobs:
echo "Waiting for twenty-app-dev to become healthy..."
count=0
while true; do
status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/healthz 2>/dev/null || echo "000")
status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:2020/healthz 2>/dev/null || echo "000")
if [ "$status" = "200" ]; then
echo "Server is healthy!"
curl -s http://localhost:3000/healthz
curl -s http://localhost:2020/healthz
break
fi

View file

@ -26,12 +26,10 @@ jobs:
runs-on: ubuntu-latest
services:
postgres:
image: twentycrm/twenty-postgres-spilo
image: postgres:18
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-

View file

@ -29,12 +29,10 @@ jobs:
runs-on: ubuntu-latest
services:
postgres:
image: twentycrm/twenty-postgres-spilo
image: postgres:18
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
@ -71,13 +69,14 @@ jobs:
- name: Server / Start
run: |
npx nx start twenty-server &
npx nx run twenty-server:start:ci &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
timeout 60 bash -c 'until curl -sf http://localhost:3000/healthz; do sleep 2; done'
- name: Start worker
working-directory: packages/twenty-server
run: |
npx nx run twenty-server:worker &
NODE_ENV=development node dist/queue-worker/queue-worker.js &
echo "Worker started"
- name: Zapier / Build

View file

@ -17,6 +17,12 @@ jobs:
with:
ref: ${{ github.event.client_payload.pr_head_sha }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Run compose setup
run: |
echo "Patching docker-compose.yml..."

5
.gitignore vendored
View file

@ -1,7 +1,8 @@
**/**/.env
.DS_Store
/.idea
.claude/settings.json
.claude/
.cursor/debug-*.log
**/**/node_modules/
.cache
@ -52,3 +53,5 @@ mcp.json
/.junie/
TRANSLATION_QA_REPORT.md
.playwright-mcp/
.playwright-cli/
output/playwright/

View file

@ -71,13 +71,10 @@ npx nx build twenty-server
# Database management
npx nx database:reset twenty-server # Reset database
npx nx run twenty-server:database:init:prod # Initialize database
npx nx run twenty-server:database:migrate:prod # Run migrations
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
# Generate migration (replace [name] with kebab-case descriptive name)
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
# Sync metadata
npx nx run twenty-server:command workspace:sync-metadata
# Generate an instance command (fast or slow)
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
```
### Database Inspection (Postgres MCP)
@ -87,7 +84,7 @@ A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
- Verify migration results (columns, types, constraints) after running migrations
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
- Inspect metadata tables to debug GraphQL schema generation or `workspace:sync-metadata` issues
- Inspect metadata tables to debug GraphQL schema generation issues
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
@ -161,14 +158,17 @@ packages/
- **Redis** for caching and session management
- **BullMQ** for background job processing
### Database & Migrations
### Database & Upgrade Commands
- **PostgreSQL** as primary database
- **Redis** for caching and sessions
- **ClickHouse** for analytics (when enabled)
- Always generate migrations when changing entity files
- Migration names must be kebab-case (e.g. `add-agent-turn-evaluation`)
- Include both `up` and `down` logic in migrations
- Never delete or rewrite committed migrations
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
- Include both `up` and `down` logic in instance commands
- Never delete or rewrite committed instance command `up`/`down` logic
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
### Utility Helpers
Use existing helpers from `twenty-shared` instead of manual type guards:
@ -181,7 +181,7 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
### Before Making Changes
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
2. Test changes with relevant test suites (prefer single-file test runs)
3. Ensure database migrations are generated for entity changes
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
4. Check that GraphQL schema changes are backward compatible
5. Run `graphql:generate` after any GraphQL schema changes

203
README.md
View file

@ -1,126 +1,170 @@
<p align="center">
<a href="https://www.producthunt.com/products/twenty-crm?launch=twenty-2-0">
<img src="./packages/twenty-website/public/images/readme/product-hunt-banner.png" alt="We're live on Product Hunt — Support us" />
</a>
</p>
<p align="center">
<a href="https://www.twenty.com">
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
</a>
</p>
<h2 align="center" >The #1 Open-Source CRM </h2>
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://docs.twenty.com">📚 Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
<br />
<h2 align="center" >The #1 Open-Source CRM</h2>
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
<p align="center">
<a href="https://www.twenty.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/github-cover-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/github-cover-light.png" />
<img src="./packages/twenty-website/public/images/readme/github-cover-light.png" alt="Cover" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/github-cover-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/github-cover-light.png" />
<img src="./packages/twenty-website/public/images/readme/github-cover-light.png" alt="Twenty banner" />
</picture>
</a>
</p>
<br />
# Installation
See:
🚀 [Self-hosting](https://docs.twenty.com/developers/self-host/capabilities/docker-compose)
🖥️ [Local Setup](https://docs.twenty.com/developers/contribute/capabilities/local-setup)
# Why Twenty
We built Twenty for three reasons:
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
**CRMs are too expensive, and users are trapped.** Companies use locked-in customer data to hike prices. It shouldn't be that way.
**A fresh start is required to build a better experience.** We can learn from past mistakes and craft a cohesive experience inspired by new UX patterns from tools like Notion, Airtable or Linear.
**We believe in open-source and community.** Hundreds of developers are already building Twenty together. Once we have plugin capabilities, a whole ecosystem will grow around it.
<a href="https://twenty.com/why-twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
<br />
# What You Can Do With Twenty
# Installation
Please feel free to flag any specific needs you have by creating an issue.
### <img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
Below are a few features we have implemented to date:
The fastest way to get started. Sign up at [twenty.com](https://twenty.com) and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
+ [Personalize layouts with filters, sort, group by, kanban and table views](#personalize-layouts-with-filters-sort-group-by-kanban-and-table-views)
+ [Customize your objects and fields](#customize-your-objects-and-fields)
+ [Create and manage permissions with custom roles](#create-and-manage-permissions-with-custom-roles)
+ [Automate workflow with triggers and actions](#automate-workflow-with-triggers-and-actions)
+ [Emails, calendar events, files, and more](#emails-calendar-events-files-and-more)
### <img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
Scaffold a new app with the Twenty CLI:
## Personalize layouts with filters, sort, group by, kanban and table views
```bash
npx create-twenty-app my-app
```
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/views-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/views-light.png" />
<img src="./packages/twenty-website/public/images/readme/views-light.png" alt="Companies Kanban Views" />
</picture>
</p>
Define objects, fields, and views as code:
## Customize your objects and fields
```ts
import { defineObject, FieldType } from 'twenty-sdk/define';
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/data-model-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/data-model-light.png" />
<img src="./packages/twenty-website/public/images/readme/data-model-light.png" alt="Setting Custom Objects" />
</picture>
</p>
export default defineObject({
nameSingular: 'deal',
namePlural: 'deals',
labelSingular: 'Deal',
labelPlural: 'Deals',
fields: [
{ name: 'name', label: 'Name', type: FieldType.TEXT },
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
],
});
```
## Create and manage permissions with custom roles
Then ship it to your workspace:
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/permissions-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/permissions-light.png" />
<img src="./packages/twenty-website/public/images/readme/permissions-light.png" alt="Permissions" />
</picture>
</p>
```bash
npx twenty deploy
```
## Automate workflow with triggers and actions
See the [app development guide](https://docs.twenty.com/developers/extend/apps/getting-started) for objects, views, agents, and logic functions.
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/workflows-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/workflows-light.png" />
<img src="./packages/twenty-website/public/images/readme/workflows-light.png" alt="Workflows" />
</picture>
</p>
### <img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
## Emails, calendar events, files, and more
Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.com/developers/self-host/capabilities/docker-compose), or contribute locally via the [local setup guide](https://docs.twenty.com/developers/contribute/capabilities/local-setup).
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/plus-other-features-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/plus-other-features-light.png" />
<img src="./packages/twenty-website/public/images/readme/plus-other-features-light.png" alt="Other Features" />
</picture>
</p>
<br />
<br />
# Everything you need
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
<table align="center">
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-build-apps-light.png" alt="Create your apps" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-version-control-light.png" alt="Stay on top with version control" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
</td>
</tr>
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-all-tools-light.png" alt="All the tools you need to build anything" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-tools-light.png" alt="Customize your layouts" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
</td>
</tr>
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-ai-agents-light.png" alt="AI agents and chats" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-crm-tools-light.png" alt="Plus all the tools of a good CRM" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
</td>
</tr>
</table>
<br />
# Stack
- [TypeScript](https://www.typescriptlang.org/)
- [Nx](https://nx.dev/)
- [NestJS](https://nestjs.com/), with [BullMQ](https://bullmq.io/), [PostgreSQL](https://www.postgresql.org/), [Redis](https://redis.io/)
- [React](https://reactjs.org/), with [Jotai](https://jotai.org/), [Linaria](https://linaria.dev/) and [Lingui](https://lingui.dev/)
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
- <a href="https://nx.dev/"><img src="./packages/twenty-website/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
- <a href="https://nestjs.com/"><img src="./packages/twenty-website/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
- <a href="https://reactjs.org/"><img src="./packages/twenty-website/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
# Thanks
<p align="center">
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.png" height="30" alt="Chromatic" /></a>
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.png" height="30" alt="Greptile" /></a>
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.png" height="30" alt="Sentry" /></a>
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.png" height="30" alt="Crowdin" /></a>
<a href="https://e2b.dev/"><img src="./packages/twenty-website/public/images/readme/e2b.svg" height="30" alt="E2B" /></a>
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.png" height="28" alt="Chromatic" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.png" height="28" alt="Greptile" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.png" height="28" alt="Sentry" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.png" height="28" alt="Crowdin" /></a>
</p>
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
@ -128,9 +172,4 @@ Below are a few features we have implemented to date:
# Join the Community
- Star the repo
- Subscribe to releases (watch -> custom -> releases)
- Follow us on [Twitter](https://twitter.com/twentycrm) or [LinkedIn](https://www.linkedin.com/company/twenty/)
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
- Improve translations on [Crowdin](https://twenty.crowdin.com/twenty)
- [Contributions](https://github.com/twentyhq/twenty/contribute) are, of course, most welcome!
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>

View file

@ -141,7 +141,7 @@
"cache": false,
"options": {
"cwd": "{projectRoot}",
"command": "npm pkg set version={args.releaseVersion}"
"command": "node -e \"const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='{args.releaseVersion}';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n');\""
}
},
"storybook:build": {

View file

@ -82,11 +82,11 @@
"@sentry/types": "^8",
"@storybook-community/storybook-addon-cookie": "^5.0.0",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "^10.2.13",
"@storybook/addon-links": "^10.2.13",
"@storybook/addon-vitest": "^10.2.13",
"@storybook/addon-docs": "^10.3.3",
"@storybook/addon-links": "^10.3.3",
"@storybook/addon-vitest": "^10.3.3",
"@storybook/icons": "^2.0.1",
"@storybook/react-vite": "^10.2.13",
"@storybook/react-vite": "^10.3.3",
"@storybook/test-runner": "^0.24.2",
"@swc-node/register": "^1.11.1",
"@swc/cli": "^0.7.10",
@ -154,9 +154,9 @@
"raw-loader": "^4.0.2",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.20",
"storybook": "^10.2.13",
"storybook": "^10.3.3",
"storybook-addon-mock-date": "2.0.0",
"storybook-addon-pseudo-states": "^10.2.13",
"storybook-addon-pseudo-states": "^10.3.3",
"supertest": "^6.1.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.2.3",
@ -180,6 +180,7 @@
"graphql": "16.8.1",
"type-fest": "4.10.1",
"typescript": "5.9.2",
"nodemailer": "8.0.4",
"graphql-redis-subscriptions/ioredis": "^5.6.0",
"@lingui/core": "5.1.2",
"@types/qs": "6.9.16",
@ -203,10 +204,12 @@
"packages/twenty-utils",
"packages/twenty-zapier",
"packages/twenty-website",
"packages/twenty-website-new",
"packages/twenty-docs",
"packages/twenty-e2e-testing",
"packages/twenty-shared",
"packages/twenty-sdk",
"packages/twenty-front-component-renderer",
"packages/twenty-client-sdk",
"packages/twenty-apps",
"packages/twenty-cli",

View file

@ -12,164 +12,52 @@
</div>
Create Twenty App is the official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). It sets up a readytorun project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
- Zeroconfig project bootstrap
- Preconfigured scripts for auth, dev mode (watch & sync), uninstall, and function management
- Strong TypeScript support and typed client generation
## Documentation
See Twenty application documentation https://docs.twenty.com/developers/extend/capabilities/apps
## Prerequisites
- Node.js 24+ (recommended) and Yarn 4
- Docker (for the local Twenty dev server)
The official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). Sets up a ready-to-run project with [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
## Quick start
```bash
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# The scaffolder can automatically:
# 1. Start a local Twenty server (Docker)
# 2. Open the browser to log in (tim@apple.dev / tim@apple.dev)
# 3. Authenticate your app via OAuth
# Or do it manually:
yarn twenty server start # Start local Twenty server
yarn twenty remote add http://localhost:2020 --as local # Authenticate via OAuth
# Start dev mode: watches, builds, and syncs local changes to your workspace
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built — both available via `twenty-client-sdk`)
yarn twenty dev
# Watch your application's function logs
yarn twenty logs
# Execute a function with a JSON payload
yarn twenty exec -n my-function -p '{"key": "value"}'
# Execute the pre-install function
yarn twenty exec --preInstall
# Execute the post-install function
yarn twenty exec --postInstall
# Build the app for distribution
yarn twenty build
# Publish the app to npm or directly to a Twenty server
yarn twenty publish
# Uninstall the application from the current workspace
yarn twenty uninstall
```
## Scaffolding modes
The scaffolder will:
Control which example files are included when creating a new app:
1. Create a new project with TypeScript, linting, tests, and a preconfigured `twenty` CLI
2. Optionally start a local Twenty server (Docker)
3. Open the browser for OAuth authentication
| Flag | Behavior |
| ------------------ | ----------------------------------------------------------------------- |
| `-e, --exhaustive` | **(default)** Creates all example files |
| `-m, --minimal` | Creates only core files (`application-config.ts` and `default-role.ts`) |
## Options
| Flag | Description |
| ------------------------------ | --------------------------------------- |
| `--example <name>` | Initialize from an example |
| `--name <name>` | Set the app name (skips the prompt) |
| `--display-name <displayName>` | Set the display name (skips the prompt) |
| `--description <description>` | Set the description (skips the prompt) |
| `--skip-local-instance` | Skip the local server setup prompt |
By default (no flags), a minimal app is generated with core files and an integration test. Use `--example` to start from a richer example:
```bash
# Default: all examples included
npx create-twenty-app@latest my-app
# Minimal: only core files
npx create-twenty-app@latest my-app -m
npx create-twenty-app@latest my-twenty-app --example hello-world
```
## What gets scaffolded
Examples are sourced from [twentyhq/twenty/packages/twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples).
**Core files (always created):**
## Documentation
- `application-config.ts` — Application metadata configuration
- `roles/default-role.ts` — Default role for logic functions
- `logic-functions/pre-install.ts` — Pre-install logic function (runs before app installation)
- `logic-functions/post-install.ts` — Post-install logic function (runs after app installation)
- TypeScript configuration, Oxlint, package.json, .gitignore
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started)**:
**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
- `front-components/hello-world.tsx` — Example front component
- `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)
## Local server
The scaffolder can start a local Twenty dev server for you (all-in-one Docker image with PostgreSQL, Redis, server, and worker). You can also manage it manually:
```bash
yarn twenty server start # Start (pulls image if needed)
yarn twenty server status # Check if it's healthy
yarn twenty server logs # Stream logs
yarn twenty server stop # Stop (data is preserved)
yarn twenty server reset # Wipe all data and start fresh
```
The server is pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`).
## Next steps
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty remote add <url>` to authenticate with your Twenty workspace via OAuth.
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- `CoreApiClient` is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient } from 'twenty-client-sdk/core'` and `import { MetadataApiClient } from 'twenty-client-sdk/metadata'`.
## Build and publish your application
Once your app is ready, build and publish it using the CLI:
```bash
# Build the app (output goes to .twenty/output/)
yarn twenty build
# Build and create a tarball (.tgz) for distribution
yarn twenty build --tarball
# Publish to npm (requires npm login)
yarn twenty publish
# Publish with a dist-tag (e.g. beta, next)
yarn twenty publish --tag beta
# Deploy directly to a Twenty server (builds, uploads, and installs in one step)
yarn twenty deploy
```
### Publish to the Twenty marketplace
You can also contribute your application to the curated marketplace:
```bash
git clone https://github.com/twentyhq/twenty.git
cd twenty
git checkout -b feature/my-awesome-app
```
- Copy your app folder into `twenty/packages/twenty-apps`.
- Commit your changes and open a pull request on https://github.com/twentyhq/twenty
Our team reviews contributions for quality, security, and reusability before merging.
- [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started) — step-by-step setup, project structure, server management, CI
- [Building Apps](https://docs.twenty.com/developers/extend/apps/building) — entity definitions, API clients, testing
- [Publishing](https://docs.twenty.com/developers/extend/apps/publishing) — deploy, npm publish, marketplace
## Troubleshooting
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
- Auth not working: make sure you're logged in to Twenty in the browser first, then run `yarn twenty remote add <url>`.
- Auth not working: make sure you are logged in to Twenty in the browser, then run `yarn twenty remote add`.
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
## Contributing

View file

@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.8.0-canary.5",
"version": "2.0.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",

View file

@ -2,7 +2,6 @@
import chalk from 'chalk';
import { Command, CommanderError } from 'commander';
import { CreateAppCommand } from '@/create-app.command';
import { type ScaffoldingMode } from '@/types/scaffolding-options';
import packageJson from '../package.json';
const program = new Command(packageJson.name)
@ -13,11 +12,7 @@ const program = new Command(packageJson.name)
'Output the current version of create-twenty-app.',
)
.argument('[directory]')
.option('-e, --exhaustive', 'Create all example entities (default)')
.option(
'-m, --minimal',
'Create only core entities (application-config and default-role)',
)
.option('--example <name>', 'Initialize from an example')
.option('-n, --name <name>', 'Application name (skips prompt)')
.option(
'-d, --display-name <displayName>',
@ -36,25 +31,13 @@ const program = new Command(packageJson.name)
async (
directory?: string,
options?: {
exhaustive?: boolean;
minimal?: boolean;
example?: string;
name?: string;
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
},
) => {
const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean);
if (modeFlags.length > 1) {
console.error(
chalk.red(
'Error: --exhaustive and --minimal are mutually exclusive.',
),
);
process.exit(1);
}
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
console.error(
chalk.red(
@ -69,11 +52,9 @@ const program = new Command(packageJson.name)
process.exit(1);
}
const mode: ScaffoldingMode = options?.minimal ? 'minimal' : 'exhaustive';
await new CreateAppCommand().execute({
directory,
mode,
example: options?.example,
name: options?.name,
displayName: options?.displayName,
description: options?.description,

View file

@ -1,14 +0,0 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/app-seeds/rich-app
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.

View file

@ -1,11 +0,0 @@
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
## Getting Started
Run `yarn twenty help` to list all available commands.
## Learn More
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/capabilities/apps)
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
- [Discord](https://discord.gg/cx5n4Jzs57)

View file

@ -1,37 +0,0 @@
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"allowUnreachableCode": false,
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictBindCallApply": false,
"target": "es2018",
"module": "esnext",
"lib": ["es2020", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true,
"paths": {
"src/*": ["./src/*"],
"~/*": ["./*"]
}
},
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/*.integration-test.ts"
]
}

View file

@ -0,0 +1,14 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.

View file

@ -0,0 +1,14 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.

View file

@ -0,0 +1,14 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.

View file

@ -0,0 +1,11 @@
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
## Getting Started
Run `yarn twenty help` to list all available commands.
## Learn More
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started)
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
- [Discord](https://discord.gg/cx5n4Jzs57)

View file

@ -0,0 +1,42 @@
name: CD
on:
push:
branches:
- main
pull_request:
types: [labeled]
permissions:
contents: read
env:
TWENTY_DEPLOY_URL: http://localhost:3000
concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy-and-install:
if: >-
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
- name: Install
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}

View file

@ -0,0 +1,48 @@
name: CI
on:
push:
branches:
- main
pull_request: {}
permissions:
contents: read
env:
TWENTY_VERSION: latest
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Spawn Twenty test instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Run integration tests
run: yarn test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}

View file

@ -0,0 +1,33 @@
{
"name": "TO-BE-GENERATED",
"version": "0.1.0",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"keywords": [],
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "TO-BE-GENERATED",
"twenty-sdk": "TO-BE-GENERATED"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^19.0.0",
"oxlint": "^0.16.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^3.1.1"
}
}

View file

@ -0,0 +1,87 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
function validateEnv(): { apiUrl: string; apiKey: string } {
const apiUrl = process.env.TWENTY_API_URL;
const apiKey = process.env.TWENTY_API_KEY;
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
return { apiUrl, apiKey };
}
async function checkServer(apiUrl: string) {
let response: Response;
try {
response = await fetch(`${apiUrl}/healthz`);
} catch {
throw new Error(
`Twenty server is not reachable at ${apiUrl}. ` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
}
}
function writeConfig(apiUrl: string, apiKey: string) {
const payload = JSON.stringify(
{
remotes: {
local: { apiUrl, apiKey, accessToken: apiKey },
},
defaultRemote: 'local',
},
null,
2,
);
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload);
}
export async function setup() {
const { apiUrl, apiKey } = validateEnv();
await checkServer(apiUrl);
writeConfig(apiUrl, apiKey);
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({
appPath: APP_PATH,
onProgress: (message: string) => console.log(`[dev] ${message}`),
});
if (!result.success) {
throw new Error(
`Dev sync failed: ${result.error?.message ?? 'Unknown error'}`,
);
}
}
export async function teardown() {
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
);
}
}

View file

@ -0,0 +1,46 @@
import { CoreApiClient } from 'twenty-client-sdk/core';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { describe, expect, it } from 'vitest';
describe('App installation', () => {
it('should find the installed app in the applications list', async () => {
const client = new MetadataApiClient();
const result = await client.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const app = result.findManyApplications.find(
(a: { universalIdentifier: string }) =>
a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(app).toBeDefined();
});
});
describe('CoreApiClient', () => {
it('should support CRUD on standard objects', async () => {
const client = new CoreApiClient();
const created = await client.mutation({
createNote: {
__args: { data: { title: 'Integration test note' } },
id: true,
},
});
expect(created.createNote.id).toBeDefined();
await client.mutation({
destroyNote: {
__args: { id: created.createNote.id },
id: true,
},
});
});
});

View file

@ -0,0 +1,15 @@
import { defineApplication } from 'twenty-sdk/define';
import {
APP_DESCRIPTION,
APP_DISPLAY_NAME,
APPLICATION_UNIVERSAL_IDENTIFIER,
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: APP_DISPLAY_NAME,
description: APP_DESCRIPTION,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});

View file

@ -0,0 +1,4 @@
export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
export const APP_DESCRIPTION = 'DESCRIPTION-TO-BE-GENERATED';
export const APPLICATION_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';

View file

@ -0,0 +1,16 @@
import { defineRole } from 'twenty-sdk/define';
import {
APP_DISPLAY_NAME,
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: `${APP_DISPLAY_NAME} default function role`,
description: `${APP_DISPLAY_NAME} default function role`,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
});

View file

@ -0,0 +1,42 @@
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"allowUnreachableCode": false,
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictBindCallApply": false,
"target": "es2018",
"module": "esnext",
"lib": ["es2020", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true,
"paths": {
"src/*": ["./src/*"],
"~/*": ["./*"]
}
},
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/*.integration-test.ts"
],
"references": [
{
"path": "./tsconfig.spec.json"
}
]
}

View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": true,
"types": ["vitest/globals", "node"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,31 @@
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY =
process.env.TWENTY_API_KEY ??
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});

View file

@ -0,0 +1,2 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

View file

@ -1,4 +1,5 @@
import { copyBaseApplicationProject } from '@/utils/app-template';
import { downloadExample } from '@/utils/download-example';
import { convertToLabel } from '@/utils/convert-to-label';
import { install } from '@/utils/install';
import { tryGitInit } from '@/utils/try-git-init';
@ -10,21 +11,18 @@ import * as path from 'path';
import { basename } from 'path';
import {
authLoginOAuth,
ConfigService,
detectLocalServer,
serverStart,
type ServerStartResult,
} from 'twenty-sdk/cli';
import { isDefined } from 'twenty-shared/utils';
import {
type ExampleOptions,
type ScaffoldingMode,
} from '@/types/scaffolding-options';
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
type CreateAppOptions = {
directory?: string;
mode?: ScaffoldingMode;
example?: string;
name?: string;
displayName?: string;
description?: string;
@ -37,23 +35,34 @@ export class CreateAppCommand {
await this.getAppInfos(options);
try {
const exampleOptions = this.resolveExampleOptions(
options.mode ?? 'exhaustive',
);
await this.validateDirectory(appDirectory);
this.logCreationInfo({ appDirectory, appName });
await fs.ensureDir(appDirectory);
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
exampleOptions,
});
if (options.example) {
const exampleSucceeded = await this.tryDownloadExample(
options.example,
appDirectory,
);
if (!exampleSucceeded) {
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
});
}
} else {
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
});
}
await install(appDirectory);
@ -62,15 +71,19 @@ export class CreateAppCommand {
let serverResult: ServerStartResult | undefined;
if (!options.skipLocalInstance) {
const startResult = await serverStart({
onProgress: (message: string) => console.log(chalk.gray(message)),
});
const shouldStartServer = await this.shouldStartServer();
if (startResult.success) {
serverResult = startResult.data;
await this.connectToLocal(serverResult.url);
} else {
console.log(chalk.yellow(`\n${startResult.error.message}`));
if (shouldStartServer) {
const startResult = await serverStart({
onProgress: (message: string) => console.log(chalk.gray(message)),
});
if (startResult.success) {
serverResult = startResult.data;
await this.promptConnectToLocal(serverResult.url);
} else {
console.log(chalk.yellow(`\n${startResult.error.message}`));
}
}
}
@ -95,13 +108,14 @@ export class CreateAppCommand {
const hasName = isDefined(options.name) || isDefined(directory);
const hasDisplayName = isDefined(options.displayName);
const hasDescription = isDefined(options.description);
const hasExample = isDefined(options.example);
const { name, displayName, description } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Application name:',
when: () => !hasName,
when: () => !hasName && !hasExample,
default: 'my-twenty-app',
validate: (input) => {
if (input.length === 0) return 'Application name is required';
@ -112,7 +126,7 @@ export class CreateAppCommand {
type: 'input',
name: 'displayName',
message: 'Application display name:',
when: () => !hasDisplayName,
when: () => !hasDisplayName && !hasExample,
default: (answers: { name?: string }) => {
return convertToLabel(
answers?.name ?? options.name ?? directory ?? '',
@ -123,7 +137,7 @@ export class CreateAppCommand {
type: 'input',
name: 'description',
message: 'Application description (optional):',
when: () => !hasDescription,
when: () => !hasDescription && !hasExample,
default: '',
},
]);
@ -132,6 +146,7 @@ export class CreateAppCommand {
options.name ??
name ??
directory ??
options.example ??
'my-twenty-app'
).trim();
@ -147,34 +162,6 @@ export class CreateAppCommand {
return { appName, appDisplayName, appDirectory, appDescription };
}
private resolveExampleOptions(mode: ScaffoldingMode): ExampleOptions {
if (mode === 'minimal') {
return {
includeExampleObject: false,
includeExampleField: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: false,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
includeExampleSkill: false,
includeExampleAgent: false,
includeExampleIntegrationTest: false,
};
}
return {
includeExampleObject: true,
includeExampleField: true,
includeExampleLogicFunction: true,
includeExampleFrontComponent: true,
includeExampleView: true,
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
includeExampleIntegrationTest: true,
includeExampleAgent: true,
};
}
private async validateDirectory(appDirectory: string): Promise<void> {
if (!(await fs.pathExists(appDirectory))) {
return;
@ -188,6 +175,41 @@ export class CreateAppCommand {
}
}
private async tryDownloadExample(
example: string,
appDirectory: string,
): Promise<boolean> {
try {
await downloadExample(example, appDirectory);
return true;
} catch (error) {
console.error(
chalk.red(
`\n${error instanceof Error ? error.message : 'Failed to download example.'}`,
),
);
const { useTemplate } = await inquirer.prompt([
{
type: 'confirm',
name: 'useTemplate',
message: 'Would you like to create a default template app instead?',
default: true,
},
]);
if (!useTemplate) {
process.exit(1);
}
// Clean up any partial files from the failed download
await fs.emptyDir(appDirectory);
return false;
}
}
private logCreationInfo({
appDirectory,
appName,
@ -201,24 +223,67 @@ export class CreateAppCommand {
);
}
private async connectToLocal(serverUrl: string): Promise<void> {
private async shouldStartServer(): Promise<boolean> {
const existingServerUrl = await detectLocalServer();
if (existingServerUrl) {
return true;
}
const { startDocker } = await inquirer.prompt([
{
type: 'confirm',
name: 'startDocker',
message:
'No running Twenty instance found. Would you like to start one using Docker?',
default: true,
},
]);
return startDocker;
}
private async promptConnectToLocal(serverUrl: string): Promise<void> {
const { shouldAuthenticate } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldAuthenticate',
message: `Would you like to authenticate to the local Twenty instance (${serverUrl})?`,
default: true,
},
]);
if (!shouldAuthenticate) {
console.log(
chalk.gray(
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
),
);
return;
}
try {
const result = await authLoginOAuth({
apiUrl: serverUrl,
remote: 'local',
});
if (!result.success) {
if (result.success) {
const configService = new ConfigService();
await configService.setDefaultRemote('local');
} else {
console.log(
chalk.yellow(
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
'Authentication failed. Run `yarn twenty remote add --local` manually.',
),
);
}
} catch {
console.log(
chalk.yellow(
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
'Authentication failed. Run `yarn twenty remote add` manually.',
),
);
}
@ -236,7 +301,7 @@ export class CreateAppCommand {
if (!serverResult) {
console.log(
chalk.gray(
'- yarn twenty remote add --local # Authenticate with Twenty',
'- yarn twenty remote add # Authenticate with Twenty',
),
);
}

View file

@ -1,13 +0,0 @@
export type ScaffoldingMode = 'exhaustive' | 'minimal';
export type ExampleOptions = {
includeExampleObject: boolean;
includeExampleField: boolean;
includeExampleLogicFunction: boolean;
includeExampleFrontComponent: boolean;
includeExampleView: boolean;
includeExampleNavigationMenuItem: boolean;
includeExampleSkill: boolean;
includeExampleAgent: boolean;
includeExampleIntegrationTest: boolean;
};

View file

@ -1,177 +0,0 @@
import { scaffoldIntegrationTest } from '@/utils/test-template';
import * as fs from 'fs-extra';
import { tmpdir } from 'os';
import { join } from 'path';
describe('scaffoldIntegrationTest', () => {
let testAppDirectory: string;
let sourceFolderPath: string;
beforeEach(async () => {
testAppDirectory = join(
tmpdir(),
`test-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
sourceFolderPath = join(testAppDirectory, 'src');
await fs.ensureDir(sourceFolderPath);
await fs.writeJson(join(testAppDirectory, 'tsconfig.json'), {
compilerOptions: {
paths: { 'src/*': ['./src/*'] },
},
exclude: ['node_modules', 'dist', '**/*.integration-test.ts'],
});
});
afterEach(async () => {
if (testAppDirectory && (await fs.pathExists(testAppDirectory))) {
await fs.remove(testAppDirectory);
}
});
describe('integration test file', () => {
it('should create app-install.integration-test.ts with correct structure', async () => {
await scaffoldIntegrationTest({
appDirectory: testAppDirectory,
sourceFolderPath,
});
const testPath = join(
sourceFolderPath,
'__tests__',
'app-install.integration-test.ts',
);
expect(await fs.pathExists(testPath)).toBe(true);
const content = await fs.readFile(testPath, 'utf8');
expect(content).toContain(
"import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'",
);
expect(content).toContain(
"import { MetadataApiClient } from 'twenty-client-sdk/metadata'",
);
expect(content).toContain(
"import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'",
);
expect(content).toContain('appBuild');
expect(content).toContain('appDeploy');
expect(content).toContain('appInstall');
expect(content).toContain('appUninstall');
expect(content).toContain('new MetadataApiClient()');
expect(content).toContain('findManyApplications');
expect(content).toContain('APPLICATION_UNIVERSAL_IDENTIFIER');
});
});
describe('setup-test file', () => {
it('should create setup-test.ts with SDK config bootstrap', async () => {
await scaffoldIntegrationTest({
appDirectory: testAppDirectory,
sourceFolderPath,
});
const setupTestPath = join(
sourceFolderPath,
'__tests__',
'setup-test.ts',
);
expect(await fs.pathExists(setupTestPath)).toBe(true);
const content = await fs.readFile(setupTestPath, 'utf8');
expect(content).toContain('.twenty-sdk-test');
expect(content).toContain('config.json');
expect(content).toContain('process.env.TWENTY_API_URL');
expect(content).toContain('process.env.TWENTY_API_KEY');
expect(content).toContain('assertServerIsReachable');
});
});
describe('vitest config', () => {
it('should create vitest.config.ts with env vars and setup file', async () => {
await scaffoldIntegrationTest({
appDirectory: testAppDirectory,
sourceFolderPath,
});
const vitestConfigPath = join(testAppDirectory, 'vitest.config.ts');
expect(await fs.pathExists(vitestConfigPath)).toBe(true);
const content = await fs.readFile(vitestConfigPath, 'utf8');
expect(content).toContain('TWENTY_API_KEY');
expect(content).not.toContain('TWENTY_TEST_API_KEY');
expect(content).toContain('setup-test.ts');
expect(content).toContain('tsconfig.spec.json');
expect(content).toContain('integration-test.ts');
});
});
describe('github workflow', () => {
it('should create .github/workflows/ci.yml with correct structure', async () => {
await scaffoldIntegrationTest({
appDirectory: testAppDirectory,
sourceFolderPath,
});
const workflowPath = join(
testAppDirectory,
'.github',
'workflows',
'ci.yml',
);
expect(await fs.pathExists(workflowPath)).toBe(true);
const content = await fs.readFile(workflowPath, 'utf8');
expect(content).toContain('name: CI');
expect(content).toContain('TWENTY_VERSION: latest');
expect(content).toContain('twenty-version: ${{ env.TWENTY_VERSION }}');
expect(content).toContain('actions/checkout@v4');
expect(content).toContain('spawn-twenty-docker-image@main');
expect(content).toContain('actions/setup-node@v4');
expect(content).toContain('yarn install --immutable');
expect(content).toContain('yarn test');
expect(content).toContain('TWENTY_API_URL');
expect(content).toContain('TWENTY_API_KEY');
});
});
describe('tsconfig.spec.json', () => {
it('should create tsconfig.spec.json extending the base tsconfig', async () => {
await scaffoldIntegrationTest({
appDirectory: testAppDirectory,
sourceFolderPath,
});
const tsconfigSpecPath = join(testAppDirectory, 'tsconfig.spec.json');
expect(await fs.pathExists(tsconfigSpecPath)).toBe(true);
const tsconfigSpec = await fs.readJson(tsconfigSpecPath);
expect(tsconfigSpec.extends).toBe('./tsconfig.json');
expect(tsconfigSpec.compilerOptions.composite).toBe(true);
expect(tsconfigSpec.include).toContain('src/**/*.ts');
expect(tsconfigSpec.exclude).not.toContain('**/*.integration-test.ts');
});
it('should add a reference to tsconfig.spec.json in tsconfig.json', async () => {
await scaffoldIntegrationTest({
appDirectory: testAppDirectory,
sourceFolderPath,
});
const tsconfig = await fs.readJson(
join(testAppDirectory, 'tsconfig.json'),
);
expect(tsconfig.references).toEqual([{ path: './tsconfig.spec.json' }]);
});
});
});

View file

@ -1,11 +1,9 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import { ASSETS_DIR } from 'twenty-shared/application';
import { v4 } from 'uuid';
import { type ExampleOptions } from '@/types/scaffolding-options';
import { scaffoldIntegrationTest } from '@/utils/test-template';
import createTwentyAppPackageJson from 'package.json';
import chalk from 'chalk';
const SRC_FOLDER = 'src';
@ -14,790 +12,96 @@ export const copyBaseApplicationProject = async ({
appDisplayName,
appDescription,
appDirectory,
exampleOptions,
}: {
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
exampleOptions: ExampleOptions;
}) => {
await fs.copy(join(__dirname, './constants/base-application'), appDirectory);
console.log(chalk.gray('Generating application project...'));
await fs.copy(join(__dirname, './constants/template'), appDirectory);
await createPackageJson({
appName,
await renameDotfiles({ appDirectory });
await addEmptyPublicDirectory({ appDirectory });
await generateUniversalIdentifiers({
appDisplayName,
appDescription,
appDirectory,
includeExampleIntegrationTest: exampleOptions.includeExampleIntegrationTest,
});
await createYarnLock(appDirectory);
await createGitignore(appDirectory);
await createNvmrc(appDirectory);
await createPublicAssetDirectory(appDirectory);
const sourceFolderPath = join(appDirectory, SRC_FOLDER);
await fs.ensureDir(sourceFolderPath);
await createDefaultRoleConfig({
displayName: appDisplayName,
appDirectory: sourceFolderPath,
fileFolder: 'roles',
fileName: 'default-role.ts',
});
if (exampleOptions.includeExampleObject) {
await createExampleObject({
appDirectory: sourceFolderPath,
fileFolder: 'objects',
fileName: 'example-object.ts',
});
}
if (exampleOptions.includeExampleField) {
await createExampleField({
appDirectory: sourceFolderPath,
fileFolder: 'fields',
fileName: 'example-field.ts',
});
}
if (exampleOptions.includeExampleLogicFunction) {
await createDefaultFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
fileName: 'hello-world.ts',
});
await createCreateCompanyFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
fileName: 'create-hello-world-company.ts',
});
}
if (exampleOptions.includeExampleFrontComponent) {
await createDefaultFrontComponent({
appDirectory: sourceFolderPath,
fileFolder: 'front-components',
fileName: 'hello-world.tsx',
});
await createExamplePageLayout({
appDirectory: sourceFolderPath,
fileFolder: 'page-layouts',
fileName: 'example-record-page-layout.ts',
});
}
if (exampleOptions.includeExampleView) {
await createExampleView({
appDirectory: sourceFolderPath,
fileFolder: 'views',
fileName: 'example-view.ts',
});
}
if (exampleOptions.includeExampleNavigationMenuItem) {
await createExampleNavigationMenuItem({
appDirectory: sourceFolderPath,
fileFolder: 'navigation-menu-items',
fileName: 'example-navigation-menu-item.ts',
});
}
if (exampleOptions.includeExampleSkill) {
await createExampleSkill({
appDirectory: sourceFolderPath,
fileFolder: 'skills',
fileName: 'example-skill.ts',
});
}
if (exampleOptions.includeExampleAgent) {
await createExampleAgent({
appDirectory: sourceFolderPath,
fileFolder: 'agents',
fileName: 'example-agent.ts',
});
}
if (exampleOptions.includeExampleIntegrationTest) {
await scaffoldIntegrationTest({
appDirectory,
sourceFolderPath,
});
}
await createDefaultPreInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
fileName: 'pre-install.ts',
});
await createDefaultPostInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
fileName: 'post-install.ts',
});
await createApplicationConfig({
displayName: appDisplayName,
description: appDescription,
appDirectory: sourceFolderPath,
fileName: 'application-config.ts',
});
await updatePackageJson({ appName, appDirectory });
};
const createPublicAssetDirectory = async (appDirectory: string) => {
await fs.ensureDir(join(appDirectory, ASSETS_DIR));
// npm strips dotfiles/dotdirs (.gitignore, .github/) from published packages,
// so we store them without the leading dot and rename after copying.
const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => {
const renames = [
{ from: 'gitignore', to: '.gitignore' },
{ from: 'github', to: '.github' },
];
for (const { from, to } of renames) {
const sourcePath = join(appDirectory, from);
if (await fs.pathExists(sourcePath)) {
await fs.rename(sourcePath, join(appDirectory, to));
}
}
};
const createGitignore = async (appDirectory: string) => {
const gitignoreContent = `# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn
# codegen
generated
# testing
/coverage
# dev
/dist/
.twenty
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# typescript
*.tsbuildinfo
*.d.ts
`;
await fs.writeFile(join(appDirectory, '.gitignore'), gitignoreContent);
};
const createNvmrc = async (appDirectory: string) => {
await fs.writeFile(join(appDirectory, '.nvmrc'), '24.5.0\n');
};
const createDefaultRoleConfig = async ({
displayName,
const addEmptyPublicDirectory = async ({
appDirectory,
fileFolder,
fileName,
}: {
displayName: string;
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineRole } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: '${displayName} default function role',
description: '${displayName} default function role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultFrontComponent = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
await fs.ensureDir(join(appDirectory, 'public'));
};
const content = `import { useEffect, useState } from 'react';
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { defineFrontComponent } from 'twenty-sdk';
const generateUniversalIdentifiers = async ({
appDisplayName,
appDescription,
appDirectory,
}: {
appDisplayName: string;
appDescription: string;
appDirectory: string;
}) => {
const universalIdentifiersPath = join(
appDirectory,
SRC_FOLDER,
'constants',
'universal-identifiers.ts',
);
export const HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
const universalIdentifiersFileContent = await fs.readFile(
universalIdentifiersPath,
'utf-8',
);
export const HelloWorld = () => {
const client = new CoreApiClient();
const [data, setData] = useState<
Pick<CoreSchema.Company, 'name' | 'id'> | undefined
>(undefined);
useEffect(() => {
const fetchData = async () => {
const response = await client.query({
company: {
name: true,
id: true,
__args: {
filter: {
position: {
eq: 1,
},
},
},
},
});
setData(response.company);
};
fetchData();
}, []);
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Hello, World!</h1>
<p>This is your first front component.</p>
{data ? (
<div>
<p>Company name: {data.name}</p>
<p>Company id: {data.id}</p>
</div>
) : (
<p>Company not found</p>
)}
</div>
await fs.writeFile(
universalIdentifiersPath,
universalIdentifiersFileContent
.replace('DISPLAY-NAME-TO-BE-GENERATED', appDisplayName)
.replace('DESCRIPTION-TO-BE-GENERATED', appDescription)
.replace(/UUID-TO-BE-GENERATED/g, () => v4()),
);
};
export default defineFrontComponent({
universalIdentifier: HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'hello-world-front-component',
description: 'A sample front component',
component: HelloWorld,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExamplePageLayout = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const pageLayoutUniversalIdentifier = v4();
const tabUniversalIdentifier = v4();
const widgetUniversalIdentifier = v4();
const content = `import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/front-components/hello-world';
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk';
export default definePageLayout({
universalIdentifier: '${pageLayoutUniversalIdentifier}',
name: 'Example Record Page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [
{
universalIdentifier: '${tabUniversalIdentifier}',
title: 'Hello World',
position: 50,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: '${widgetUniversalIdentifier}',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultFunction = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineLogicFunction } from 'twenty-sdk';
const handler = async (): Promise<{ message: string }> => {
return { message: 'Hello, World!' };
};
export default defineLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'hello-world-logic-function',
description: 'A simple logic function',
timeoutSeconds: 5,
handler,
httpRouteTriggerSettings: {
path: '/hello-world-logic-function',
httpMethod: 'GET',
isAuthRequired: false,
},
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createCreateCompanyFunction = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { CoreApiClient } from 'twenty-client-sdk/core';
import { defineLogicFunction } from 'twenty-sdk';
const handler = async (): Promise<{ message: string }> => {
const client = new CoreApiClient();
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Hello World',
},
},
id: true,
name: true,
},
});
return {
message: \`Created company "\${createCompany?.name}" with id \${createCompany?.id}\`,
};
};
export default defineLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'create-hello-world-company',
description: 'Creates a company called Hello World',
timeoutSeconds: 5,
handler,
httpRouteTriggerSettings: {
path: '/create-hello-world-company',
httpMethod: 'POST',
isAuthRequired: true,
},
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultPreInstallFunction = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultPostInstallFunction = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleObject = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const objectUniversalIdentifier = v4();
const nameFieldUniversalIdentifier = v4();
const content = `import { defineObject, FieldType } from 'twenty-sdk';
export const EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER =
'${objectUniversalIdentifier}';
export const NAME_FIELD_UNIVERSAL_IDENTIFIER =
'${nameFieldUniversalIdentifier}';
export default defineObject({
universalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'exampleItem',
namePlural: 'exampleItems',
labelSingular: 'Example item',
labelPlural: 'Example items',
description: 'A sample custom object',
icon: 'IconBox',
labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT,
name: 'name',
label: 'Name',
description: 'Name of the example item',
icon: 'IconAbc',
},
],
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleField = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineField, FieldType } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
export default defineField({
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
universalIdentifier: '${universalIdentifier}',
type: FieldType.NUMBER,
name: 'priority',
label: 'Priority',
description: 'Priority level for the example item (1-10)',
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleView = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const viewFieldUniversalIdentifier = v4();
const content = `import { defineView, ViewKey } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, NAME_FIELD_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
export const EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER = '${universalIdentifier}';
export default defineView({
universalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
key: ViewKey.INDEX,
position: 0,
fields: [
{
universalIdentifier: '${viewFieldUniversalIdentifier}',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleNavigationMenuItem = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineNavigationMenuItem } from 'twenty-sdk';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '${universalIdentifier}',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
type: 'VIEW',
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleSkill = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineSkill } from 'twenty-sdk';
export const EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineSkill({
universalIdentifier: EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER,
name: 'example-skill',
label: 'Example Skill',
description: 'A sample skill for your application',
icon: 'IconBrain',
content: 'Add your skill instructions here. Skills provide context and capabilities to AI agents.',
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleAgent = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineAgent } from 'twenty-sdk';
export const EXAMPLE_AGENT_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineAgent({
universalIdentifier: EXAMPLE_AGENT_UNIVERSAL_IDENTIFIER,
name: 'example-agent',
label: 'Example Agent',
description: 'A sample AI agent for your application',
icon: 'IconRobot',
prompt: 'You are a helpful assistant. Help users with their questions and tasks.',
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createApplicationConfig = async ({
displayName,
description,
appDirectory,
fileFolder,
fileName,
}: {
displayName: string;
description?: string;
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export const APPLICATION_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: '${displayName}',
description: '${description ?? ''}',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createYarnLock = async (appDirectory: string) => {
const yarnLockContent = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
`;
await fs.writeFile(join(appDirectory, 'yarn.lock'), yarnLockContent);
};
const createPackageJson = async ({
const updatePackageJson = async ({
appName,
appDirectory,
includeExampleIntegrationTest,
}: {
appName: string;
appDirectory: string;
includeExampleIntegrationTest: boolean;
}) => {
const scripts: Record<string, string> = {
twenty: 'twenty',
lint: 'oxlint -c .oxlintrc.json .',
'lint:fix': 'oxlint --fix -c .oxlintrc.json .',
};
const packageJson = await fs.readJson(join(appDirectory, 'package.json'));
const devDependencies: Record<string, string> = {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^19.0.0',
react: '^19.0.0',
'react-dom': '^19.0.0',
oxlint: '^0.16.0',
'twenty-sdk': createTwentyAppPackageJson.version,
'twenty-client-sdk': createTwentyAppPackageJson.version,
};
if (includeExampleIntegrationTest) {
scripts.test = 'vitest run';
scripts['test:watch'] = 'vitest';
devDependencies.vitest = '^3.1.1';
devDependencies['vite-tsconfig-paths'] = '^4.2.1';
}
const packageJson = {
name: appName,
version: '0.1.0',
license: 'MIT',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
scripts,
devDependencies,
};
packageJson.name = appName;
packageJson.dependencies['twenty-sdk'] = createTwentyAppPackageJson.version;
packageJson.dependencies['twenty-client-sdk'] =
createTwentyAppPackageJson.version;
await fs.writeFile(
join(appDirectory, 'package.json'),

View file

@ -0,0 +1,179 @@
import { execSync } from 'node:child_process';
import * as fs from 'fs-extra';
import { join } from 'path';
import { tmpdir } from 'node:os';
import chalk from 'chalk';
const TWENTY_REPO_OWNER = 'twentyhq';
const TWENTY_REPO_NAME = 'twenty';
const TWENTY_FALLBACK_REF = 'main';
const TWENTY_EXAMPLES_PATH = 'packages/twenty-apps/examples';
const TWENTY_EXAMPLES_URL = `https://github.com/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/tree/${TWENTY_FALLBACK_REF}/${TWENTY_EXAMPLES_PATH}`;
// Fetches the latest release tag from the repo, or falls back to main
const resolveRef = async (): Promise<string> => {
const response = await fetch(
`https://api.github.com/repos/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/releases/latest`,
{ headers: { Accept: 'application/vnd.github.v3+json' } },
);
if (response.ok) {
const release = (await response.json()) as { tag_name: string };
return release.tag_name;
}
return TWENTY_FALLBACK_REF;
};
// Uses the GitHub Contents API to list directories — fast and doesn't download the repo
const fetchGitHubDirectoryContents = async (
path: string,
ref: string,
): Promise<{ name: string; type: string }[] | null> => {
const apiUrl = `https://api.github.com/repos/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/contents/${path}?ref=${ref}`;
const response = await fetch(apiUrl, {
headers: { Accept: 'application/vnd.github.v3+json' },
});
if (!response.ok) {
return null;
}
const data = await response.json();
if (!Array.isArray(data)) {
return null;
}
return data as { name: string; type: string }[];
};
const listAvailableExamples = async (ref: string): Promise<string[]> => {
const contents = await fetchGitHubDirectoryContents(
TWENTY_EXAMPLES_PATH,
ref,
);
if (!contents) {
return [];
}
return contents
.filter((entry) => entry.type === 'dir')
.map((entry) => entry.name);
};
const validateExampleExists = async (
exampleName: string,
ref: string,
): Promise<void> => {
const examplePath = `${TWENTY_EXAMPLES_PATH}/${exampleName}`;
const contents = await fetchGitHubDirectoryContents(examplePath, ref);
if (contents !== null) {
return;
}
const availableExamples = await listAvailableExamples(ref);
throw new Error(
`Example "${exampleName}" not found.\n\n` +
(availableExamples.length > 0
? `Available examples:\n${availableExamples.map((name) => ` - ${name}`).join('\n')}\n\n`
: '') +
`Browse all examples: ${TWENTY_EXAMPLES_URL}`,
);
};
export const downloadExample = async (
exampleName: string,
targetDirectory: string,
): Promise<void> => {
if (
exampleName.includes('/') ||
exampleName.includes('\\') ||
exampleName.includes('..')
) {
throw new Error(
`Invalid example name: "${exampleName}". Example names must be simple directory names (e.g., "hello-world").`,
);
}
const ref = await resolveRef();
const examplePath = `${TWENTY_EXAMPLES_PATH}/${exampleName}`;
console.log(chalk.gray(`Resolving examples from ref '${ref}'...`));
await validateExampleExists(exampleName, ref);
console.log(chalk.gray(`Example '${examplePath}' validated successfully.`));
const tarballUrl = `https://codeload.github.com/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/tar.gz/${ref}`;
const tempDir = join(
tmpdir(),
`create-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
try {
await fs.ensureDir(tempDir);
console.log(chalk.gray(`Downloading tarball from ${tarballUrl}...`));
const response = await fetch(tarballUrl);
if (!response.ok) {
if (response.status === 404) {
throw new Error(
`Could not find repository: ${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME} (ref: ${ref})`,
);
}
throw new Error(
`Failed to download from GitHub: ${response.status} ${response.statusText}`,
);
}
console.log(chalk.gray('Tarball downloaded. Writing to disk...'));
const tarballPath = join(tempDir, 'archive.tar.gz');
const buffer = Buffer.from(await response.arrayBuffer());
await fs.writeFile(tarballPath, buffer);
console.log(
chalk.gray(
`Tarball saved (${(buffer.length / 1024 / 1024).toFixed(1)} MB). Extracting...`,
),
);
execSync(`tar xzf "${tarballPath}" -C "${tempDir}"`, {
stdio: 'pipe',
});
// GitHub tarballs extract to a directory named {repo}-{ref}/
const extractedEntries = await fs.readdir(tempDir);
const extractedDir = extractedEntries.find(
(entry) => entry !== 'archive.tar.gz',
);
if (!extractedDir) {
throw new Error('Failed to extract archive: no directory found');
}
const sourcePath = join(tempDir, extractedDir, examplePath);
if (!(await fs.pathExists(sourcePath))) {
throw new Error(
`Example directory not found in archive: "${examplePath}"`,
);
}
await fs.copy(sourcePath, targetDirectory);
} finally {
await fs.remove(tempDir);
}
};

View file

@ -1,279 +0,0 @@
import * as fs from 'fs-extra';
import { join } from 'path';
const SEED_API_KEY =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik';
export const scaffoldIntegrationTest = async ({
appDirectory,
sourceFolderPath,
}: {
appDirectory: string;
sourceFolderPath: string;
}) => {
await createIntegrationTest({
appDirectory: sourceFolderPath,
fileFolder: '__tests__',
fileName: 'app-install.integration-test.ts',
});
await createSetupTest({
appDirectory: sourceFolderPath,
fileFolder: '__tests__',
fileName: 'setup-test.ts',
});
await createVitestConfig(appDirectory);
await createTsconfigSpec(appDirectory);
await createGithubWorkflow(appDirectory);
};
const createVitestConfig = async (appDirectory: string) => {
const content = `import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_KEY:
'${SEED_API_KEY}',
},
},
});
`;
await fs.writeFile(join(appDirectory, 'vitest.config.ts'), content);
};
const createTsconfigSpec = async (appDirectory: string) => {
const tsconfigSpec = {
extends: './tsconfig.json',
compilerOptions: {
composite: true,
types: ['vitest/globals'],
},
include: ['src/**/*.ts', 'src/**/*.tsx'],
exclude: ['node_modules', 'dist'],
};
await fs.writeFile(
join(appDirectory, 'tsconfig.spec.json'),
JSON.stringify(tsconfigSpec, null, 2),
);
const tsconfigPath = join(appDirectory, 'tsconfig.json');
const tsconfig = await fs.readJson(tsconfigPath);
tsconfig.references = [{ path: './tsconfig.spec.json' }];
await fs.writeFile(tsconfigPath, JSON.stringify(tsconfig, null, 2));
};
const createSetupTest = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const content = `import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
const assertServerIsReachable = async () => {
let response: Response;
try {
response = await fetch(\`\${TWENTY_API_URL}/healthz\`);
} catch {
throw new Error(
\`Twenty server is not reachable at \${TWENTY_API_URL}. \` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(\`Server at \${TWENTY_API_URL} returned \${response.status}\`);
}
};
beforeAll(async () => {
await assertServerIsReachable();
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
const configFile = {
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
};
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify(configFile, null, 2),
);
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createIntegrationTest = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const content = `import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(\`[build] \${message}\`),
});
if (!buildResult.success) {
throw new Error(
\`Build failed: \${buildResult.error?.message ?? 'Unknown error'}\`,
);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(\`[deploy] \${message}\`),
});
if (!deployResult.success) {
throw new Error(
\`Deploy failed: \${deployResult.error?.message ?? 'Unknown error'}\`,
);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(
\`Install failed: \${installResult.error?.message ?? 'Unknown error'}\`,
);
}
});
afterAll(async () => {
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
console.warn(
\`App uninstall failed: \${uninstallResult.error?.message ?? 'Unknown error'}\`,
);
}
});
it('should find the installed app in the applications list', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(application: { universalIdentifier: string }) =>
application.universalIdentifier ===
APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const DEFAULT_TWENTY_VERSION = 'latest';
const createGithubWorkflow = async (appDirectory: string) => {
const content = `name: CI
on:
push:
branches:
- main
pull_request: {}
env:
TWENTY_VERSION: ${DEFAULT_TWENTY_VERSION}
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Spawn Twenty instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
with:
twenty-version: \${{ env.TWENTY_VERSION }}
github-token: \${{ secrets.GITHUB_TOKEN }}
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
- name: Run integration tests
run: yarn test
env:
TWENTY_API_URL: \${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: \${{ steps.twenty.outputs.access-token }}
`;
const workflowDir = join(appDirectory, '.github', 'workflows');
await fs.ensureDir(workflowDir);
await fs.writeFile(join(workflowDir, 'ci.yml'), content);
};

View file

@ -24,5 +24,9 @@
"vite.config.ts",
"jest.config.mjs"
],
"exclude": ["src/constants/base-application/vitest.config.ts"]
"exclude": [
"src/constants/template/vitest.config.ts",
"src/constants/template/src/**",
"src/constants/template/tsconfig.spec.json"
]
}

View file

@ -18,6 +18,9 @@
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/__tests__/**"
"**/__tests__/**",
"src/constants/template/src/**",
"src/constants/template/vitest.config.ts",
"src/constants/template/tsconfig.spec.json"
]
}

View file

@ -62,8 +62,8 @@ export default defineConfig(() => {
dts({ entryRoot: './src', tsconfigPath: tsConfigPath }),
copyAssetPlugin([
{
src: 'src/constants/base-application',
dest: 'dist/constants/base-application',
src: 'src/constants/template',
dest: 'dist/constants/template',
},
]),
],

View file

@ -1,7 +1,7 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/app-seeds/rich-app
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/fixtures/rich-app
## UUID requirement
- All generated UUIDs must be valid UUID v4.

View file

@ -1,5 +1,5 @@
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { defineApplication } from 'twenty-sdk';
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: 'ac1d2ed1-8835-4bd4-9043-28b46fdda465',

View file

@ -1,8 +1,4 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'da15cfc6-3657-457d-8757-4ba11b5bb6e1',

View file

@ -1,8 +1,4 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: '505532f5-1fc5-4a58-8074-ba9b48650dbc',

View file

@ -1,8 +1,4 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'be15e062-b065-48b4-979c-65b9a50e0cb1',

View file

@ -1,8 +1,4 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'c90ae72d-4ddf-4f22-882f-eef98c91e40e',

View file

@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import { OAuthApplicationVariables } from 'src/logic-functions/get-oauth-application-variables';
import { VERIFY_PAGE_PATH } from 'src/logic-functions/get-verify-page';
import { defineFrontComponent } from 'twenty-sdk';
import { defineFrontComponent } from 'twenty-sdk/define';
const StyledContainer = styled.div`
display: flex;

View file

@ -1,4 +1,4 @@
import { defineLogicFunction, RoutePayload } from "twenty-sdk";
import { defineLogicFunction, RoutePayload } from "twenty-sdk/define";
import { MetadataApiClient } from 'twenty-sdk/clients';
export const OAUTH_TOKEN_PAIRS_PATH = '/oauth/token-pairs';

View file

@ -1,4 +1,4 @@
import { defineLogicFunction } from 'twenty-sdk';
import { defineLogicFunction } from 'twenty-sdk/define';
export type OAuthApplicationVariables = {
apolloClientId: string;

View file

@ -1,4 +1,4 @@
import { defineLogicFunction } from "twenty-sdk";
import { defineLogicFunction } from "twenty-sdk/define";
export const VERIFY_PAGE_PATH = '/oauth/verify';

View file

@ -1,8 +1,4 @@
import {
defineLogicFunction,
type DatabaseEventPayload,
type ObjectRecordUpdateEvent,
} from 'twenty-sdk';
import { defineLogicFunction, type DatabaseEventPayload, type ObjectRecordUpdateEvent } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-sdk/clients';
type CompanyRecord = {

View file

@ -1,4 +1,4 @@
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);

View file

@ -1,4 +1,4 @@
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);

View file

@ -1,4 +1,4 @@
import { defineRole } from 'twenty-sdk';
import { defineRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b8faae3f-e174-43fa-ab94-715712ae26cb';

View file

@ -1,4 +1,4 @@
import { type ApplicationConfig } from 'twenty-sdk';
import { type ApplicationConfig } from 'twenty-sdk/define';
const config: ApplicationConfig = {
universalIdentifier: 'a4df0c0f-c65e-44e5-8436-24814182d4ac',

View file

@ -1,4 +1,4 @@
import { Object } from 'twenty-sdk';
import { Object } from 'twenty-sdk/define';
@Object({
universalIdentifier: 'd1831348-b4a4-4426-9c0b-0af19e7a9c27',

View file

@ -1,4 +1,4 @@
import { type FunctionConfig } from 'twenty-sdk';
import { type FunctionConfig } from 'twenty-sdk/define';
import type { ProcessResult } from './types';
import { WebhookHandler } from './webhook-handler';

View file

@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn
# codegen
generated
# testing
/coverage
# dev
/dist/
.twenty
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# typescript
*.tsbuildinfo
*.d.ts

View file

@ -2,22 +2,18 @@
Updates Last interaction and Interaction status fields based on last email date
## Requirements
- an `apiKey` - go to Settings > API & Webhooks to generate one
## Setup
1. Add and synchronize app
Add and synchronize app
```bash
cd packages/twenty-apps/community/last-email-interaction
yarn auth
yarn sync
yarn twenty remote add
yarn twenty install
```
2. Go to Settings > Integrations > Last email interaction > Settings and add required variables
## Flow
- Checks if fields are created, if not, creates them on fly
- Extracts the datetime of message and calculates the last interaction status
- Fetches all users and companies connected to them and updates their Last interaction and Interaction status fields
- Extracts the datetime of fetched message and calculates the last interaction status
- Fetches all users and companies connected to the message and updates their Last interaction and Interaction status fields
## Todo:
- update app with generated Twenty object once extending objects is possible
## Notes
- Upon install, creates fields to Person and Company objects
- Every day at midnight app goes through all companies and people records and updates their Interaction status based on Last interaction date

View file

@ -1,23 +0,0 @@
import { type ApplicationConfig } from 'twenty-sdk';
const config: ApplicationConfig = {
universalIdentifier: '718ed9ab-53fc-49c8-8deb-0cff78ecf0d2',
displayName: 'Last email interaction',
description:
'Updates Last interaction and Interaction status fields based on last received email',
icon: "IconMailFast",
applicationVariables: {
TWENTY_API_KEY: {
universalIdentifier: 'aae3f523-4c1f-4805-b3ee-afeb676c381e',
isSecret: true,
description: 'Required to send requests to Twenty',
},
TWENTY_API_URL: {
universalIdentifier: '6d19bb04-45bb-46aa-a4e5-4a2682c7b19d',
isSecret: false,
description: 'Optional, defaults to cloud API URL',
},
},
};
export default config;

View file

@ -9,19 +9,23 @@
},
"packageManager": "yarn@4.9.2",
"dependencies": {
"axios": "^1.12.2",
"twenty-sdk": "0.2.4"
},
"devDependencies": {
"@types/node": "^24.7.2"
"axios": "1.14.0",
"twenty-sdk": "0.8.0"
},
"scripts": {
"auth": "twenty auth login",
"dev": "twenty app dev",
"sync": "twenty app sync",
"uninstall": "twenty app uninstall",
"logs": "twenty app logs",
"create-entity": "twenty app add",
"help": "twenty --help"
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
"test": "vitest run",
"test:watch": "vitest"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^18.2.0",
"oxlint": "^0.16.0",
"react": "^18.2.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^3.1.1"
}
}

View file

@ -0,0 +1,10 @@
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '718ed9ab-53fc-49c8-8deb-0cff78ecf0d2',
displayName: 'Last email interaction',
description:
'Updates Last interaction and Interaction status fields based on last received email',
icon: "IconMailFast",
defaultRoleUniversalIdentifier: '7a66af97-5056-45b2-96a9-c89f0fd181d1',
});

View file

@ -0,0 +1,41 @@
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: '9378751e-c23b-4e84-887d-2905cb8359b4',
name: 'interactionStatus',
label: 'Interaction status',
type: FieldType.SELECT,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
description: 'Indicates the health of relation',
options: [
{
id: '39d54a6b-5a0e-4209-9a59-2a2d7b8c462b',
color: 'green',
label: 'Recent',
value: 'RECENT',
position: 1,
},
{
id: '7377d6c5-a75c-453e-a1a1-63fb9cba4e26',
color: 'yellow',
label: 'Active',
value: 'ACTIVE',
position: 2,
},
{
id: 'a8b99246-237f-4715-b21f-94a3ae14994e',
color: 'sky',
label: 'Cooling',
value: 'COOLING',
position: 3,
},
{
id: '1f05d528-eaab-4639-aba1-328050a87220',
color: 'gray',
label: 'Dormant',
value: 'DORMANT',
position: 4,
},
],
});

View file

@ -0,0 +1,10 @@
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: '2f195c4c-1db1-4bbe-80b6-25c2f63168b0',
name: 'lastInteraction',
label: 'Last interaction',
type: FieldType.DATE_TIME,
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
description: 'Date when the last interaction happened',
});

View file

@ -0,0 +1,41 @@
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'fa342e26-9742-4db8-85b4-4d78ba18482f',
name: 'interactionStatus',
label: 'Interaction status',
type: FieldType.SELECT,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
description: 'Indicates the health of relation',
options: [
{
id: '1dfbfa99-35fb-43af-8c14-74e682a8121b',
color: 'green',
label: 'Recent',
value: 'RECENT',
position: 1,
},
{
id: '955788e8-6d64-45ba-80ea-a1a5446a0ae7',
color: 'yellow',
label: 'Active',
value: 'ACTIVE',
position: 2,
},
{
id: '7b84ca72-fac5-4c6d-ab08-b148e4b3efdf',
color: 'sky',
label: 'Cooling',
value: 'COOLING',
position: 3,
},
{
id: '04dea3e5-ec26-41cf-b23f-37abab67827a',
color: 'gray',
label: 'Dormant',
value: 'DORMANT',
position: 4,
},
],
});

View file

@ -0,0 +1,10 @@
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'bec14de7-6683-4784-91ba-62d83b5f30f7',
name: 'lastInteraction',
label: 'Last interaction',
type: FieldType.DATE_TIME,
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
description: 'Date when the last interaction happened',
});

View file

@ -1,270 +0,0 @@
import axios from 'axios';
import { type FunctionConfig } from 'twenty-sdk';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '';
const TWENTY_URL =
process.env.TWENTY_API_URL !== '' && process.env.TWENTY_API_URL !== undefined
? `${process.env.TWENTY_API_URL}/rest`
: 'https://api.twenty.com/rest';
const create_last_interaction = (id: string) => {
return {
method: 'POST',
url: `${TWENTY_URL}/metadata/fields`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
data: {
type: 'DATE_TIME',
objectMetadataId: `${id}`,
name: 'lastInteraction',
label: 'Last interaction',
description: 'Date when the last interaction happened',
icon: 'IconCalendarClock',
defaultValue: null,
isNullable: true,
settings: {},
},
};
};
const create_interaction_status = (id: string) => {
return {
method: 'POST',
url: `${TWENTY_URL}/metadata/fields`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
data: {
type: 'SELECT',
objectMetadataId: `${id}`,
name: 'interactionStatus',
label: 'Interaction status',
description: 'Indicates the health of relation',
icon: 'IconProgress',
defaultValue: null,
isNullable: true,
settings: {},
options: [
{
color: 'green',
label: 'Recent',
value: 'RECENT',
position: 1,
},
{
color: 'yellow',
label: 'Active',
value: 'ACTIVE',
position: 2,
},
{
color: 'sky',
label: 'Cooling',
value: 'COOLING',
position: 3,
},
{
color: 'gray',
label: 'Dormant',
value: 'DORMANT',
position: 4,
},
],
},
};
};
const calculateStatus = (date: string) => {
const day = 1000 * 60 * 60 * 24;
const now = Date.now();
const messageDate = Date.parse(date);
const deltaTime = now - messageDate;
return deltaTime < 7 * day
? 'RECENT'
: deltaTime < 30 * day
? 'ACTIVE'
: deltaTime < 90 * day
? 'COOLING'
: 'DORMANT';
};
const updateInteractionStatus = async (objectName: string, id: string, messageDate: string, status: string) => {
const options = {
method: 'PATCH',
url: `${TWENTY_URL}/${objectName}/${id}`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
data: {
lastInteraction: messageDate,
interactionStatus: status
}
};
try {
const response = await axios.request(options);
if (response.status === 200) {
console.log('Successfully updated company last interaction field');
}
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
throw error;
}
}
const fetchRelatedCompanyId = async (id: string) => {
const options = {
method: 'GET',
url: `${TWENTY_URL}/people/${id}`,
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
};
try {
const req = await axios.request(options);
if (req.status === 200 && req.data.person.companyId !== null && req.data.person.companyId !== undefined) {
return req.data.person.companyId;
}
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
throw error;
}
}
export const main = async (params: {
properties: Record<string, any>;
recordId: string;
userId: string;
}): Promise<object | undefined> => {
if (TWENTY_API_KEY === '') {
console.log("Function exited as API key or URL hasn't been set properly");
return {};
}
const { properties, recordId } = params;
// Check if fields are created
const options = {
method: 'GET',
url: `${TWENTY_URL}/metadata/objects`,
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
};
try {
const response = await axios.request(options);
const objects = response.data.data.objects;
const company_object = objects.find(
(object: any) => object.nameSingular === 'company',
);
const company_last_interaction = company_object.fields.find(
(field: any) => field.name === 'lastInteraction',
);
const company_interaction_status = company_object.fields.find(
(field: any) => field.name === 'interactionStatus',
);
const person_object = objects.find(
(object: any) => object.nameSingular === 'person',
);
const person_last_interaction = person_object.fields.find(
(field: any) => field.name === 'lastInteraction',
);
const person_interaction_status = person_object.fields.find(
(field: any) => field.name === 'interactionStatus',
);
// If not, create them
if (company_last_interaction === undefined) {
const response2 = await axios.request(
create_last_interaction(company_object.id),
);
if (response2.status === 201) {
console.log('Successfully created company last interaction field');
}
}
if (company_interaction_status === undefined) {
const response2 = await axios.request(
create_interaction_status(company_object.id),
);
if (response2.status === 201) {
console.log('Successfully created company interaction status field');
}
}
if (person_last_interaction === undefined) {
const response2 = await axios.request(
create_last_interaction(person_object.id),
);
if (response2.status === 201) {
console.log('Successfully created person last interaction field');
}
}
if (person_interaction_status === undefined) {
const response2 = await axios.request(
create_interaction_status(person_object.id),
);
if (response2.status === 201) {
console.log('Successfully created person interaction status field');
}
}
// Extract the timestamp of message
const messageDate = properties.after.receivedAt;
const interactionStatus = calculateStatus(messageDate);
// Get the details of person and related company
const messageOptions = {
method: 'GET',
url: `${TWENTY_URL}/messages/${recordId}?depth=1`,
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
};
const messageDetails = await axios.request(messageOptions);
const peopleIds: string[] = [];
for (const participant of messageDetails.data.messages
.messageParticipants) {
peopleIds.push(participant.personId);
}
const companiesIds = [];
for (const id of peopleIds) {
companiesIds.push(await fetchRelatedCompanyId(id));
}
// Update the field value depending on the timestamp
for (const id of peopleIds) {
await updateInteractionStatus("people", id, messageDate, interactionStatus);
}
for (const id of companiesIds) {
await updateInteractionStatus("companies", id, messageDate, interactionStatus);
}
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(error.message);
return {};
}
console.error(error);
return {};
}
};
export const config: FunctionConfig = {
universalIdentifier: '683966a0-b60a-424e-86b1-7448c9191bde',
name: 'test',
triggers: [
{
universalIdentifier: 'f4f1e127-87f0-4dcf-99fe-8061adf5cbe6',
type: 'databaseEvent',
eventName: 'message.created',
},
{
universalIdentifier: '4c17878f-b6b3-4d0a-8de6-967b1cb55002',
type: 'databaseEvent',
eventName: 'message.updated',
},
],
};

View file

@ -0,0 +1,126 @@
import { defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { calculateStatus } from '../shared/calculate-status';
const fetchAllPeople = async () => {
const client = new CoreApiClient();
const result = await client.query({
people: {
__args: {
filter: {
/*
lastInteraction: {
is: 'NOT_NULL',
},
*/
},
},
edges: {
node: {
id: true,
lastInteraction: true,
interactionStatus: true,
},
},
},
});
if (!result.people) {
throw new Error('Could not find any people');
}
return result.people.edges;
}
const fetchAllCompanies = async () => {
const client = new CoreApiClient();
const result = await client.query({
companies: {
__args: {
filter: {
/* how to fetch fields added in fields folder?
lastInteraction: {
is: 'NOT_NULL',
},
*/
},
},
edges: {
node: {
id: true,
lastInteraction: true,
interactionStatus: true,
},
},
},
});
if (!result.companies) {
throw new Error('Could not find any companies');
}
return result.companies.edges;
}
const updateCompany = async (
companyId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updateCompany: {
__args: {
id: companyId,
data: updateData,
},
id: true,
},
});
if (!result.updateCompany) {
throw new Error(`Failed to update company ${companyId}`);
}
};
const updatePerson = async (
personId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updatePerson: {
__args: {
id: personId,
data: updateData,
},
id: true,
},
});
if (!result.updatePerson) {
throw new Error(`Failed to update person ${personId}`);
}
};
const handler = async () => {
const people = await fetchAllPeople();
for (const person of people) {
const interactionStatus = calculateStatus(person.node.lastInteraction as string);
if (interactionStatus !== person.node.interactionStatus) {
await updatePerson(person.node.id, {interactionStatus: interactionStatus});
}
}
const companies = await fetchAllCompanies();
for (const company of companies) {
const interactionStatus = calculateStatus(company.node.lastInteraction as string);
if (interactionStatus !== company.node.interactionStatus) {
await updateCompany(company.node.id, {interactionStatus: interactionStatus});
}
}
};
export default defineLogicFunction({
universalIdentifier: 'c79f1f30-f369-4264-9e5b-c183577bc709',
name: 'on-cron-job',
description:
'Runs daily at midnight and updates all companies and people with correct interaction status',
timeoutSeconds: 5,
handler,
cronTriggerSettings: {
pattern: '0 0 * * *', // runs daily at midnight
},
});

View file

@ -0,0 +1,139 @@
import { DatabaseEventPayload, defineLogicFunction, ObjectRecordCreateEvent } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { calculateStatus } from '../shared/calculate-status';
const fetchMessageParticipants = async (messageId: string) => {
const client = new CoreApiClient();
const result = await client.query({
messageParticipants: {
__args: {
filter: {
messageId: {
eq: messageId,
},
},
},
edges: {
node: {
personId: true,
},
},
},
});
let people: string[] = [];
if (result.messageParticipants === undefined) {
return people;
}
for (const person of result.messageParticipants.edges) {
if (person.node.personId !== undefined) {
people.push(person.node.personId);
}
}
return people;
};
const fetchRelatedCompany = async (personId: string) => {
const client = new CoreApiClient();
const result = await client.query({
people: {
__args: {
filter: {
id: {
eq: personId,
},
},
},
edges: {
node: {
company: {
id: true,
},
},
},
},
});
if (
result.people === undefined ||
result.people.edges[0].node.company === undefined
) {
throw new Error(`Failed to fetch related company of person ${personId}`);
}
return result.people.edges[0].node.company.id;
};
const updateCompany = async (
companyId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updateCompany: {
__args: {
id: companyId,
data: updateData,
},
id: true,
},
});
if (!result.updateCompany) {
throw new Error(`Failed to update company ${companyId}`);
}
};
const updatePerson = async (
personId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updatePerson: {
__args: {
id: personId,
data: updateData,
},
id: true,
},
});
if (!result.updatePerson) {
throw new Error(`Failed to update person ${personId}`);
}
};
type Message = {
receivedAt: string;
};
type MessageCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Message>
>;
const handler = async (
event: MessageCreatedEvent,
): Promise<object | undefined> => {
const { properties, recordId } = event;
const interactionStatus = calculateStatus(properties.after.receivedAt);
const peopleIds: string[] = [];
peopleIds.push(...(await fetchMessageParticipants(recordId)));
const updateData = {
lastInteraction: properties.after.receivedAt,
interactionStatus: interactionStatus,
};
for (const person of peopleIds) {
const companyId = await fetchRelatedCompany(person);
await updatePerson(person, updateData);
await updateCompany(companyId, updateData);
}
return {};
};
export default defineLogicFunction({
universalIdentifier: '543432c2-6509-4ee9-90ec-15ffb4d7abfc',
name: 'on-message-created',
description: 'Triggered when new message is created',
timeoutSeconds: 5,
handler,
databaseEventTriggerSettings: {
eventName: 'message.created',
},
});

View file

@ -0,0 +1,139 @@
import { DatabaseEventPayload, defineLogicFunction, ObjectRecordCreateEvent } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { calculateStatus } from '../shared/calculate-status';
const fetchMessageParticipants = async (messageId: string) => {
const client = new CoreApiClient();
const result = await client.query({
messageParticipants: {
__args: {
filter: {
messageId: {
eq: messageId,
},
},
},
edges: {
node: {
personId: true,
},
},
},
});
let people: string[] = [];
if (result.messageParticipants === undefined) {
return people;
}
for (const person of result.messageParticipants.edges) {
if (person.node.personId !== undefined) {
people.push(person.node.personId);
}
}
return people;
};
const fetchRelatedCompany = async (personId: string) => {
const client = new CoreApiClient();
const result = await client.query({
people: {
__args: {
filter: {
id: {
eq: personId,
},
},
},
edges: {
node: {
company: {
id: true,
},
},
},
},
});
if (
result.people === undefined ||
result.people.edges[0].node.company === undefined
) {
throw new Error(`Failed to fetch related company of person ${personId}`);
}
return result.people.edges[0].node.company.id;
};
const updateCompany = async (
companyId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updateCompany: {
__args: {
id: companyId,
data: updateData,
},
id: true,
},
});
if (!result.updateCompany) {
throw new Error(`Failed to update company ${companyId}`);
}
};
const updatePerson = async (
personId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updatePerson: {
__args: {
id: personId,
data: updateData,
},
id: true,
},
});
if (!result.updatePerson) {
throw new Error(`Failed to update person ${personId}`);
}
};
type Message = {
receivedAt: string;
};
type MessageCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Message>
>;
const handler = async (
event: MessageCreatedEvent,
): Promise<object | undefined> => {
const { properties, recordId } = event;
const interactionStatus = calculateStatus(properties.after.receivedAt);
const peopleIds: string[] = [];
peopleIds.push(...(await fetchMessageParticipants(recordId)));
const updateData = {
lastInteraction: properties.after.receivedAt,
interactionStatus: interactionStatus,
};
for (const person of peopleIds) {
const companyId = await fetchRelatedCompany(person);
await updatePerson(person, updateData);
await updateCompany(companyId, updateData);
}
return {};
};
export default defineLogicFunction({
universalIdentifier: '9bfcb3e4-9119-4b65-b6e9-d395d0764ce5',
name: 'on-message-updated',
description: 'Triggered when message is updated',
timeoutSeconds: 5,
handler,
databaseEventTriggerSettings: {
eventName: 'message.updated',
},
});

View file

@ -0,0 +1,13 @@
export const calculateStatus = (date: string) => {
const day = 1000 * 60 * 60 * 24;
const now = Date.now();
const messageDate = Date.parse(date);
const deltaTime = now - messageDate;
return deltaTime < 7 * day
? 'RECENT'
: deltaTime < 30 * day
? 'ACTIVE'
: deltaTime < 90 * day
? 'COOLING'
: 'DORMANT';
};

Some files were not shown because too many files have changed in this diff Show more