Compare commits

..
Author SHA1 Message Date
Devessier 133c01923d feat: recreating app 2026-03-18 17:06:58 +01:00
91d6a69bf8 i18n - translations (#18738)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-18 16:18:52 +01:00
Félix MalfaitandGitHub c6f11d8adb fix: migrate driver modules to DriverFactoryBase lazy-loading pattern (#18731)
## Summary

- Migrates `LogicFunctionModule`, `CodeInterpreterModule`, and
`CaptchaModule` from the `forRootAsync` + injection token pattern to the
`DriverFactoryBase` lazy-loading pattern (matching `EmailModule` and
`FileStorageModule`)
- Fixes #18724 where `LOGIC_FUNCTION_TYPE` was not respected in worker
processes because the driver was created at module boot time before the
DB config cache was loaded
- Removes `isEnvOnly` from `LOGIC_FUNCTION_TYPE`,
`CODE_INTERPRETER_TYPE`, `CAPTCHA_DRIVER`, `IS_MULTIWORKSPACE_ENABLED`,
and `FRONTEND_URL` — these can now be safely configured via the database
at runtime

## How it works

Each migrated module now uses a `DriverFactory` (extending
`DriverFactoryBase`) instead of a module-level async factory + Symbol
injection token:

1. **Lazy creation**: `getCurrentDriver()` creates the driver on first
call, after `DatabaseConfigDriver.onModuleInit()` has loaded the DB
cache
2. **Auto-recreation**: If config changes in the DB, the next
`getCurrentDriver()` call detects the key mismatch and creates a new
driver instance
3. **Unified config**: Both server and worker read from the same
database — driver config only needs to be set once

### Files deleted (old pattern)
- `logic-function-module.factory.ts`,
`logic-function-drivers.module.ts`, `logic-function-driver.constants.ts`
- `code-interpreter-module.factory.ts`
- `captcha.module-factory.ts`, `captcha-driver.constants.ts`

### Files created (new pattern)
- `logic-function-driver.factory.ts`
- `code-interpreter-driver.factory.ts`
- `captcha-driver.factory.ts`

Net: **-150 lines**

## Test plan

- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [ ] Integration tests pass (`npx nx run
twenty-server:test:integration:with-db-reset`)
- [ ] Verify logic functions execute in workflow runs (the original bug)
- [ ] Verify code interpreter works in workflow code steps
- [ ] Verify captcha validation works on sign-up (when captcha is
configured)


Made with [Cursor](https://cursor.com)
2026-03-18 16:00:45 +01:00
fe4512f80c Add record page layout tabs editing (#18702)
- Fix duplication of tabs in dashboards that didn't open the duplicated
tab in the side panel: replaced the logic that used Math.round with an
algorithm that switches the positions of the elements. We will keep
relying on integers, but it will work well in all cases.
- Make dnd work with record page layouts, where there is a pinned tab
- Make tab movements work with pinned tab, too
- Allow user to set a tab as the pinned tab
- Make backend changes to be able to save tabs with `layoutMode`



https://github.com/user-attachments/assets/ce1130fa-71df-49ba-ba2f-6f971e15dd49

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 14:48:23 +00:00
Raphaël BosiandGitHub f2fa958f20 Add progress tracking to command menu items (#18732)
Introduces a progress indicator for command menu items (both engine
commands and front components). When a command is running, the menu item
now displays a percentage alongside the loader spinner instead of just a
spinner.

- Added `commandMenuItemProgressFamilyState` to track per-item progress
and `CommandListItemLoader` to render it
- Exposed `updateProgress` in the twenty-sdk public API so front
components can report execution progress back to the host
- Wired progress reporting into `ExportMultipleRecordsCommand` as the
first consumer, showing CSV export progress
- Progress state is cleaned up on unmount for both engine commands and
headless front components
- Refactor
2026-03-18 14:37:27 +00:00
Raphaël BosiandGitHub eb51f8e6da Remove front component and command menu item old seeds (#18737)
Deprecated seeds
2026-03-18 14:07:56 +00:00
Félix MalfaitandGitHub b6c62b3812 fix(auth): use dynamic SSE headers and add token renewal retry logic (#18706)
## Summary

- **Dynamic SSE headers**: The `graphql-sse` client was created with a
static `Authorization` header captured at creation time. When the access
token refreshed, the SSE client kept using the expired token on every
reconnection attempt, causing up to 10 wasted retries before the client
was disposed and recreated. Now `headers` is passed as a function that
reads the latest token from the Jotai store on each connection attempt.

- **Token renewal retry with error classification**:
`handleTokenRenewal` previously had zero retry tolerance — any failure
during `renewToken` (including transient network errors, server 500s, or
timeouts) triggered an immediate full logout via
`onUnauthenticatedError()`. Now the renewal retries up to 3 times with
linear backoff for transient errors. Only explicit server rejections
(`CombinedGraphQLErrors`, e.g. expired/revoked refresh token) skip
retries and proceed to logout immediately.

- **Preserved error types in `renewTokenMutation`**: The old code caught
all errors and re-threw them as a generic `new Error('Something went
wrong...')`, destroying the original error type. Callers couldn't
distinguish a GraphQL auth rejection from a network failure. Now errors
propagate with their original type.

- **Simplified SSE retry handler**: With dynamic headers handling token
freshness automatically, the retry handler no longer needs the
`initialTokenForSseClient` comparison to detect token mismatches. It now
only resets the SSE client when the user has logged out (no token) or
after 10+ consecutive failures.

## Test plan

- [ ] Log in, wait for access token to expire (~30 min or configure
shorter expiry), verify no unexpected logout occurs
- [ ] Simulate transient network failure during token renewal (e.g.
throttle network in devtools), verify the retry logic recovers without
logging out
- [ ] Verify SSE real-time updates continue working after a token
refresh
- [ ] Verify genuine logout still works when refresh token is actually
invalid/expired
- [ ] Open multiple browser tabs, verify token refresh works correctly
across all tabs without "suspicious activity" revocation


Made with [Cursor](https://cursor.com)
2026-03-18 15:08:30 +01:00
c676e46a1d i18n - translations (#18736)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-18 14:48:17 +01:00
Abdul RahmanandGitHub c7e89a18f0 Migrate permission flag to syncable entity (#18567)
Closes [#2225](https://github.com/twentyhq/core-team-issues/issues/2225)
2026-03-18 13:25:52 +00:00
Charles BochetandGitHub 58336fb70f fix: navigation menu item type backfill and frontend loading (#18730)
## Summary

- Move `BackfillNavigationMenuItemTypeCommand` from the 1-19 to the 1-20
upgrade path and split the DB transaction into two phases (data
backfill, then schema changes) to avoid the PostgreSQL error "cannot
ALTER TABLE because it has pending trigger events."
- Fix backfill logic to prefer `OBJECT` over `VIEW` for navigation menu
items that have `targetObjectMetadataId`, and correct already mis-typed
items. Tighten the `CHECK` constraint to enforce `viewId IS NULL` for
`OBJECT` type items.
- On the frontend, force `navigationMenuItems` into `staleEntityKeys`
when the server's `minimalMetadata` response omits the collection hash
(happens when the Redis cache hasn't been warmed after an upgrade),
ensuring the sidebar loads navigation items.

## Test plan

- [ ] Upgrade from 1.18 or 1.19 to 1.20 and verify the migration
completes without errors
- [ ] Verify navigation menu items of type `OBJECT` do not have a
`viewId` set in the database
- [ ] Sign out and sign in — confirm navigation menu items appear in the
sidebar on first load
- [ ] Verify `VIEW`-typed items also appear correctly in the sidebar

Made with [Cursor](https://cursor.com)
2026-03-18 11:56:19 +01:00
neo773andGitHub 87efaf2ff8 workspace:export command (#18695)
This PR adds a `workspace:export` server command for ongoing debug
tooling/administrative work

Demo Video shows
1. Exporting a sample workspace YC
2. Restoring Local DB Docker volume snapshot to Dropped YC Workspace
3. Importing exported SQL
4. Booting and navigating the imported Workspace



https://github.com/user-attachments/assets/0e1ac6cb-8ce1-440b-8b56-f81dcb27a9c8
2026-03-18 10:40:12 +00:00
Raphaël BosiandGitHub dedcf4e9b9 Support template variables in command menu item labels (#18707)
- Adds template variable interpolation (`${...}`) to command menu item
labels and short labels, enabling dynamic text like `Create new
${capitalize(objectMetadataItem.labelSingular)}` instead of static
`Create new record`.
- Supports `capitalize` and `lowercase` transform functions within
template expressions.
2026-03-18 10:26:58 +00:00
neo773andGitHub 928194cee4 fix Draft Email stuck Callout (#18719) 2026-03-18 10:19:33 +00:00
martmullandGitHub 13a357ab9f Publish new version (#18727)
as title
2026-03-18 09:54:54 +00:00
neo773andGitHub b1a7c5c4d6 Fix null connectedAccount crash in blocklist message deletion job (#18723)
Fixes TWENTY-SERVER-DRP
2026-03-18 10:41:56 +01:00
neo773andGitHub a8c445a1c2 Treat Microsoft Graph 400 with empty body as transient error (#18726)
Graph SDK occasionally returns a 400 with a null error message which is
not a real bad request but a transient hiccup.
Classify these as temporary errors so they get retried instead of
flooding Sentry.

Fixes TWENTY-SERVER-D3X
2026-03-18 10:41:10 +01:00
Charles BochetandGitHub a74edbf715 fix: route object color through standardOverrides for standard objects (#18717)
## Summary

- Fixes object `color` to use the `standardOverrides` mechanism for
standard objects, matching how `label`, `description`, and `icon`
already work
- Previously, color was written directly to the `objectMetadata.color`
column for **both** standard and custom objects, which meant user color
customizations on standard objects could be overwritten during metadata
syncs
- Custom objects continue to have `color` updated directly on the entity
(no change)

## Changes

| File | What changed |
|------|-------------|
| `object-metadata-standard-overrides-properties.constant.ts` | Added
`'color'` to `OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES` so
`sanitizeRawUpdateObjectInput` routes color into `standardOverrides` for
standard objects |
| `resolve-object-metadata-standard-override.util.ts` | Extended to
support `'color'` as a key — handled like `icon` (no i18n/translation,
just direct override check) |
| `object-metadata.resolver.ts` | Added `@ResolveField` for `color` that
resolves through `resolveObjectMetadataStandardOverride`, matching the
existing `labelSingular`/`labelPlural`/`description`/`icon` resolve
fields |
| `flat-object-metadata-validator.service.ts` | Removed `'color'` from
`allowedOverrideKeys` for system objects since it now flows through
`standardOverrides` |
| `resolve-object-metadata-standard-override.util.spec.ts` | Added test
cases for custom object color, standard object color override, and
standard object color fallback |
| `successful-update-one-standard-object-metadata.integration-spec.ts` |
Added `'when updating color'` test case, included `color` in GraphQL
queries and `standardOverrides` fragment, reset color in `afterEach`
cleanup |

## How it works now

| Object type | Color update flow |
|---|---|
| **Custom** | Written directly to `objectMetadata.color` column |
| **Standard** | Stored in `objectMetadata.standardOverrides.color`,
resolved via `@ResolveField` at query time |

This is identical to how `label`, `description`, and `icon` have always
worked.

## Test plan

- [x] Unit tests pass
(`resolve-object-metadata-standard-override.util.spec.ts` — 21 tests)
- [x] Typecheck passes (`npx nx typecheck twenty-server`)
- [x] Lint passes (`npx nx lint:diff-with-main twenty-server`)
- [ ] Integration test snapshot regenerates correctly
(`successful-update-one-standard-object-metadata`)
- [ ] Verify standard object color editing from sidebar persists via
`standardOverrides`
- [ ] Verify custom object color editing from sidebar persists directly
on entity

Made with [Cursor](https://cursor.com)
2026-03-18 10:40:57 +01:00
Charles BochetandGitHub 0ae62898a3 fix: restructure navigationMenuItem type migration for safe upgrade path (#18722)
## Summary

- Restructures the `navigationMenuItem.type` column migration to follow
the established 3-step safe upgrade pattern (used for webhooks, files,
etc.)
- The previous migration added `type` as `NOT NULL DEFAULT 'VIEW'` with
a CHECK constraint in one step, which incorrectly assigned `VIEW` to all
existing rows regardless of their actual type (folders, records, links,
objects)
- Now: (1) migration adds column as nullable, (2) upgrade command
backfills correct type from existing columns (`viewId`,
`targetRecordId`, `targetObjectMetadataId`, `link`) and cleans
conflicting columns, (3) shared utility applies `NOT NULL` + `CHECK`
constraint

### Changes

**Modified:**
- `1773681736596-add-type-to-navigation-menu-item.ts` -- adds column as
nullable, no DEFAULT, no CHECK
- `navigation-menu-item.entity.ts` -- removed `default:
NavigationMenuItemType.VIEW` from column decorator
- `upgrade.command.ts` / `1-19-upgrade-version-command.module.ts` --
wired new command

**Created:**
- `1773681736596-makeNavigationMenuItemTypeNotNull.util.ts` -- shared
utility applying NOT NULL + CHECK constraint
- `1773822077682-make-navigation-menu-item-type-not-null.ts` --
migration calling the util with savepoint (succeeds on fresh installs,
swallows error on upgrades with NULL data)
- `1-19-backfill-navigation-menu-item-type.command.ts` -- upgrade
command that backfills type, cleans conflicting columns, then applies
constraints via the shared utility

### Execution flow

**Existing deployments (upgrade):**
1. TypeORM migration adds nullable `type` column, drops old CHECK
2. Savepoint migration fails gracefully (existing rows have NULL type)
3. 1-18 favorites migration creates items WITH correct type
4. 1-19 backfill command infers type for remaining NULL rows, cleans
conflicting columns, applies NOT NULL + CHECK

**Fresh installs:**
1. TypeORM migration adds nullable `type` column
2. Savepoint migration succeeds immediately (no data, constraints apply
cleanly)

## Test plan

- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [x] `npx nx test twenty-server` passes (477 suites, 4297 tests)
- [ ] Verify fresh database setup with `npx nx database:reset
twenty-server` applies both migrations and constraints correctly
- [ ] Verify upgrade path: existing navigation menu items get correct
type backfilled based on their columns

Made with [Cursor](https://cursor.com)
2026-03-18 10:39:50 +01:00
neo773andGitHub fb5b68f1d8 Skip threads with no participants in timeline formatting (#18725)
Threads with no FROM participants have no entry in the participants map,
causing extractParticipantSummary to receive undefined and crash

Fixes TWENTY-SERVER-FB8
2026-03-18 10:32:49 +01:00
Charles BochetandGitHub 2d4f6f8f6f fix: make 1.19 upgrade commands resilient to pre-existing data (#18716)
## Summary

- **BackfillMissingStandardViewsCommand**: When view validation fails
(e.g. a viewField references a field metadata that doesn't exist in the
workspace), log a warning and skip instead of throwing — so the
workspace upgrade continues with the remaining commands.
- **AddMissingSystemFieldsToStandardObjectsCommand**: Wrap both the
non-tsVector batch migration and each individual tsVector migration in
try-catch. If a field already exists (e.g. duplicate key on `name +
objectMetadataId + workspaceId`), the error is logged as a warning and
the command moves on to the next field.

These errors were observed during the 1.18 → 1.19 production upgrade for
workspaces with non-standard state (missing "owner" field metadata on
Opportunity, or searchVector fields already present with a different
universalIdentifier).

## Test plan

- [ ] Re-run upgrade on the affected production workspaces
- [ ] Verify upgrade completes successfully with warnings instead of
failures
- [ ] Confirm that workspaces which were already upgrading cleanly are
unaffected


Made with [Cursor](https://cursor.com)
2026-03-17 23:57:40 +01:00
Charles BochetandGitHub a82739cdb1 feat: optimistic metadata store updates for navigation menu items (#18710)
## Summary

- **Optimistic metadata store updates**: Replace `refetchQueries` with
direct `addToDraft`/`applyChanges` calls in create, update, and delete
navigation menu item mutation hooks for instant UI feedback. Client-side
UUID generation enables optimistic creates before the server responds.
- **SSE event enrichment with `targetRecordIdentifier`**: Introduce
`NavigationMenuItemRecordIdentifierService` to resolve record display
info (label, image) and enrich SSE metadata events at emission time, so
the sidebar shows record names immediately without a page refresh.
- **Centralized role permission resolution**: Add
`resolveRolePermissionConfigFromAuthContext` to `PermissionsService`,
removing duplicated role resolution logic from individual services.
- **Mutation fragments include `targetRecordIdentifier`**: Switch
create/update/delete mutations from `NavigationMenuItemFields` to
`NavigationMenuItemQueryFields` so the mutation response includes
`targetRecordIdentifier`, preventing a brief gap where RECORD favorites
are invisible in the sidebar.
- **Folder UI fixes**: Remove transparent border on
`StyledFolderContainer` that caused a 1px size inconsistency between
folder and non-folder items in Favorites. Make the folder kebab menu
hover-only instead of always visible.
2026-03-17 23:18:07 +01:00
Charles BochetandGitHub 058414fae5 Upgrade Ink to v6 and pause TUI on idle/error (#18705)
## Summary
- Upgrade `ink` from 5.1.1 to 6.8.0 in twenty-sdk (React 19 required, no
API breaking changes)
- Upgrade `react`/`react-dom` from 18 to 19 and
`@types/react`/`@types/react-dom` to 19 in twenty-sdk
- Enable `incrementalRendering` — only redraws changed lines instead of
full output, reducing flickering
- Pause the animation timer when the pipeline is not actively building
or syncing, so Ink stops re-rendering and the terminal becomes
scrollable (fixes inability to scroll up to read errors)
- Remove `AnimationProvider` context — derive animation frames from
`Date.now()` directly in `useStatusIcon`
- Export `NavigationMenuItemType` from `twenty-sdk` (re-exported from
`twenty-shared/types`)
- Add dedicated NavigationMenuItem integration tests to postcard-app
(unique positions, unique identifiers, valid object references)

## Test plan
- [ ] Run `twenty dev` and verify the TUI renders normally during
build/sync
- [ ] Trigger an error and verify the terminal output freezes and
becomes scrollable
- [ ] Verify that after fixing the error, the TUI resumes animating on
next build cycle
- [ ] Verify `import { NavigationMenuItemType } from "twenty-sdk"` works
- [ ] Run postcard-app integration tests and verify new
NavigationMenuItem tests pass
2026-03-17 20:24:39 +01:00
Thomas TrompetteGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>github-actionsCharles Bochet
994215e0dc Update store on data model mutation (#18684)
- enrich SSE events with relations
- remove queries from sse metadata events
- on sse event, manage store
- on object/field metadata changes, manage store

TODO left:
- fix other metadata items

---------

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: Charles Bochet <charles@twenty.com>
2026-03-17 19:32:19 +01:00
e62dfae741 i18n - translations (#18709)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-17 19:08:21 +01:00
Charles BochetandGitHub 9f7c29bce8 refactor(twenty-front): unify Favorites and Workspace navigation menu item code (#18697)
## Summary

- Consolidate duplicated Favorites and Workspace navigation menu item
frontend code into a single unified codebase within
`navigation-menu-item/`
- Move all DnD-related code from `navigation/` module into
`navigation-menu-item/display/dnd/`, renaming `workspaceDndKit*` files
to `navigationMenuItemDndKit*`
- Unify duplicated components (DnD providers, DnD hooks, folder
components, orphan items, section shell) using a `NavigationSections`
enum to parameterize section-specific behavior
- Rename residual workspace-prefixed symbols
(`WorkspaceDndKitSortableItem`, `WorkspaceDndKitDroppableSlot`,
`useWorkspaceSectionItems`, etc.) to `navigationMenuItem`-prefixed
equivalents
- Clean `navigation/` module to only contain app-level concerns (drawer
layout, settings, routing)

### Key changes

| Before (duplicated) | After (unified) |
|---|---|
| `FavoritesDndKitProvider` + `WorkspaceDndKitProvider` |
`NavigationMenuItemDndKitProvider` with `section` prop |
| `useFavoritesDndKit` + `useWorkspaceDndKit` |
`useNavigationMenuItemDndKit(section)` |
| `FavoritesFolderItem` + `WorkspaceNavigationMenuItemsFolder` |
`NavigationMenuItemFolder` with `section` prop |
| `FavoritesOrphanItems` | `NavigationMenuItemOrphanItems` with
`section` prop |
| Separate section shells | Shared `NavigationMenuItemSection` with thin
wrappers |
| `WorkspaceDndKitSortableItem` | `NavigationMenuItemSortableItem` |
| `WorkspaceDndKitDroppableSlot` | `NavigationMenuItemDroppableSlot` |
| `useWorkspaceSectionItems` | `useNavigationMenuItemSectionItems` |
| `useWorkspaceFolderOpenState` | `useNavigationMenuItemFolderOpenState`
|

Net result: **~1650 lines deleted** across 51 files.

## Test plan

- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx lint twenty-front` passes (0 errors)
- [x] `npx nx test twenty-front` passes (763 suites, 4467 tests)
- [ ] Smoke test: Favorites section renders, DnD reorder works, folder
create/rename/delete works
- [ ] Smoke test: Workspace section renders, edit mode works, DnD
reorder works
- [ ] Smoke test: Add-to-navigation from side panel works for both
sections

Made with [Cursor](https://cursor.com)
2026-03-17 19:01:09 +01:00
5a51764b8e i18n - translations (#18704)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-17 17:38:17 +01:00
Raphaël BosiandGitHub abdab2fb7e [Command menu items] Create engine commands (#18681)
## Description

- Introduces a new engine command execution model that replaces the
previous approach of mapping `EngineComponentKey` to React components.
Instead, engine commands are now mounted headlessly via
`HeadlessEngineCommandMountRoot`, with their execution context populated
synchronously before mounting.
- Creates new headless command components
- Moves error handling from the SDK layer to the host app by wrapping
all mounted commands with a new `CommandMenuItemErrorBoundary`

The new flow works as follows:
- When a command menu item with an `engineComponentKey` is clicked,
`useCommandMenuItemFrontComponentCommands` calls
`useMountEngineCommand`, which synchronously reads the current context
store (object metadata, selected records, filters, view ID, etc.) and
writes a `MountedEngineCommandContext` into
`mountedEngineCommandsState`.
- The command is then mounted into `mountedEngineCommandsState`, which
triggers `HeadlessEngineCommandMountRoot` to render the corresponding
headless component from `ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP`,
wrapped in `CommandMenuItemErrorBoundary`,
`ContextStoreComponentInstanceContext.Provider`, and
`EngineCommandComponentInstanceContext.Provider`.
- Each command component reads its execution context and delegates to
one of the 4 execution patterns: `HeadlessEngineCommandWrapperEffect`
(simple actions), `HeadlessConfirmationModalEngineCommandEffect`
(destructive actions needing confirmation),
`HeadlessNavigateEngineCommand` (GO_TO_* commands), or
`HeadlessOpenSidePanelPageEngineCommand` (SEARCH_RECORDS, ASK_AI,
VIEW_PREVIOUS_AI_CHATS).
- After execution, the command self-unmounts via
`useUnmountEngineCommand`, which removes the entry from
`mountedEngineCommandsState` and stops rendering the component.
2026-03-17 17:25:18 +01:00
bee474afdf i18n - translations (#18703)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-17 16:47:38 +01:00
nitinandGitHub ab881350b2 refactor: unify layout customization mode (record pages + navigation) (#18640)
### What

Unifies record page layout editing and navigation menu editing into a
single global "layout customization" session. Dashboard editing stays
separate.

  ### How it works

  **Two edit mode systems, one context-based read:**
- `isLayoutCustomizationModeEnabledState` -- global atom for record
pages + navigation
- `isDashboardInEditModeComponentState` -- dashboard-only, independent
per-component atom
- `PageLayoutEditModeProvider` -- context that dispatches to
`RecordPageLayoutEditModeProvider` (reads global atom) or
`DashboardPageLayoutEditModeProvider` (reads component atom), one
component per file

  **Session registry + independent atoms:**
- `activeCustomizationPageLayoutIdsState` -- accumulates page layout IDs
as user navigates during customization (`string[]`)
- Save/cancel iterate the ID list and read each layout's draft/persisted
atoms independently
- Follows the same pattern as `settingsRoleIdsState` +
`settingsDraftRoleFamilyState`

  **Unified UI:**
- `LayoutCustomizationBar` replaces the old `NavigationMenuEditModeBar`
- Enter once -- edit record layouts + navigation -- save/cancel
everything together
- `useSaveLayoutCustomization` orchestrates sequential save: navigation
draft -- page layouts -- field widget groups
- Error snackbar on partial save failure (with TODO for future atomic
server mutation)

  **Draft protection during customization:**
- `PageLayoutRelationWidgetsSyncEffect` guarded -- only updates
persisted state from server, skips draft/currentLayouts while
customization is active
- `useExecuteTasksOnAnyLocationChange` skips draft reset when
customization mode is enabled
  - Command execution blocked during layout customization

  ### Cleanup
- Deleted `NavigationMenuEditModeBar`,
`isNavigationMenuInEditModeState`,
`isPageLayoutInEditModeComponentState`,
`useIsGlobalLayoutCustomizationActive`
- `DraftPageLayout` type changed from `Omit` to `Pick` (explicit fields)
- Removed save/cancel from `DefaultRecordCommandMenuItemsConfig` (bar
handles it now)
  - Extracted `useSaveFieldsWidgetGroups` from save orchestration
- Split `PageLayoutEditModeProvider` into 3 separate files (one
component per file, Twenty convention)

  ### Known issues
- **Stale deleted widget after save (pre-existing on `main`)**: Delete
widget -- save -- exit customization -- Apollo cache stale -- sync
effect overwrites Jotai from stale data -- widget reappears until
refresh. Separate PR needed, likely tied to the planned server-side
`saveLayoutCustomization` atomic endpoint.

  ### Open questions
- **Module location**: Layout customization hooks/states live in `/app`
-- should they move to their own `modules/layout-customization/`?
- **Atomic server mutation**: All save mutations are on metadata schema
(`createNavigationMenuItem`, `deleteNavigationMenuItem`,
`updateNavigationMenuItem`, `updatePageLayoutWithTabsAndWidgets`,
`upsertFieldsWidget`). A single `saveLayoutCustomization` endpoint could
make saves truly atomic.


https://github.com/user-attachments/assets/036ef542-97f3-485b-a68f-3726002c81fb
2026-03-17 16:24:09 +01:00
Thomas TrompetteandGitHub f38d72a4c2 Setup local instance on app creation (#18184)
Needs for `generate-api-key` command to be available on docker
2026-03-17 15:23:43 +01:00
3204175065 [FIx]: using the dynamic {objectLabelPlural} instead of hardcoded name (#18700)
Fixes #18607 

So previously i followed the frontend approach which led to
architectural mismatch.


- We already have the correct things implemented in the
`processViewNameWithTemplate` and in object metadata service. Found that
the seeder files had the hardcoded names instead of {objectLabelPlural}.
So changed all the hardcoded string to contain {objectLabelPlural} to
seed the new workspaces accurately.



- it will have a follow up PR for typeORM migration to migrate the
existing workspaces





https://github.com/user-attachments/assets/3b460f9f-c59d-49d1-8baa-17322d696ecc



I hope i am correct this time :)

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
2026-03-17 14:16:23 +01:00
05a81c82a9 Add hotkeys to command menu items (#18682)
Add hotkeys column to the entity and plug it to the front end command
menu item display

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-17 14:09:19 +01:00
731e297147 Twenty sdk cli oauth (#18638)
<img width="1418" height="804" alt="image"
src="https://github.com/user-attachments/assets/de6c8222-6496-4a71-bc21-7e5e1269d5cb"
/>

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
2026-03-17 11:43:17 +01:00
111debc1ce i18n - docs translations (#18692)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-17 11:14:55 +01:00
770a685556 i18n - translations (#18696)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-17 07:01:07 +01:00
Charles BochetandGitHub f1dfcfd163 refactor(twenty-front): reorganize NavigationMenuItem module into common/display/edit subfolders (#18691)
## Summary

- Reorganizes the `navigation-menu-item` frontend module from a flat
structure into `common/`, `display/`, and `edit/` subfolders with
type-specific subdirectories (`link/`, `folder/`, `object/`, `view/`,
`record/`)
- Every file is now in a leaf folder describing its type: `components/`,
`hooks/`, `utils/`, `types/`, or `constants/`
- Moves 14 NavigationMenuItem-related components out of
`object-metadata/` and `side-panel/pages/` into the
`navigation-menu-item` module where they belong
- Creates type-specific display utility functions (e.g.,
`getLinkNavigationMenuItemLabel`,
`getObjectNavigationMenuItemComputedLink`) to replace generic
switch-based functions
- Unifies the Favorites section drag-and-drop from `@hello-pangea/dnd`
to `@dnd-kit/react`, matching the Workspace section's DnD library
- Renames Favorites section components from `CurrentWorkspaceMember*` to
`Favorites*` for clarity
- Deletes unused `FavoritesDragDropProviderContent` and
`NavbarDragProvider`

## Test plan

- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx lint:diff-with-main twenty-front` passes (0 warnings, 0
errors)
- [x] `npx nx test twenty-front` passes (763 suites, 4467 tests)
- [ ] Verify favorites drag-and-drop still works in the UI (reorder
items, move between folders)
- [ ] Verify workspace edit mode drag-and-drop still works
- [ ] Verify "add to navigation" drag from command menu/side panel still
works
2026-03-17 06:46:50 +01:00
Charles BochetandGitHub 1be87eb97b chore: frontend dead code removal and naming cleanup (#18690)
## Summary

- **Delete 10 unused files**: 7 hooks (`useWorkflowRunUnsafe`,
`useGetViewById`, `useCreateViewFieldGroup`, `useDeleteViewFieldGroup`,
`useUpdateViewFieldGroup`, `useCreateManyViewFieldGroups`,
`useMoveViewColumns` + test), 1 component (`SettingsSummaryCard`), 1
utility (`createEventContext`)
- **Rename `objectMetadataItemsState` → `objectMetadataItemsSelector`**
across ~85 files to accurately reflect it is a derived selector (via
`createAtomSelector`), not a base Jotai atom

## Details

### Dead code removed

| Type | Name | Reason |
|------|------|--------|
| Hook | `useWorkflowRunUnsafe` | Never imported — duplicate of
`useWorkflowRun` without schema validation |
| Hook | `useGetViewById` | Never imported — `useViewById` is used
instead |
| Hook | `useCreateViewFieldGroup` | Never imported — CRUD done via
`usePerformViewFieldGroupAPIPersist` |
| Hook | `useDeleteViewFieldGroup` | Same as above |
| Hook | `useUpdateViewFieldGroup` | Same as above |
| Hook | `useCreateManyViewFieldGroups` | Same as above |
| Hook | `useMoveViewColumns` | Only imported by its own test — no
production usage |
| Component | `SettingsSummaryCard` | Never imported anywhere |
| Utility | `createEventContext` | Never imported anywhere |

### Rename

`objectMetadataItemsState` is created via `createAtomSelector` (it
derives from `objectMetadataItemsWithFieldsSelector`), so naming it
`*State` is misleading. Renamed to `objectMetadataItemsSelector` for
consistency with sibling selectors like
`objectMetadataItemsByNamePluralMapSelector`.
2026-03-17 00:49:37 +01:00
neo773andGitHub 1735c7527c fix 2FA UI layout (#18688)
/closes #18685
2026-03-17 00:33:16 +01:00
Charles BochetandGitHub 1bb642d7cd refactor: remove ProcessedNavigationMenuItem, derive display fields at point of use (#18687)
## Summary

- Removes `ProcessedNavigationMenuItem` type and the
`sortNavigationMenuItems` enrichment pipeline that pre-computed display
fields (label, link, icon, avatarUrl, etc.) for every navigation menu
item upfront
- Replaces with `filterAndSortNavigationMenuItems` (pure filter+sort
returning raw `NavigationMenuItem[]`) and small utility functions
(`getNavigationMenuItemLabel`, `getNavigationMenuItemComputedLink`,
`getNavigationMenuItemObjectNameSingular`) that components call on
demand
- Eliminates the redundant `itemType` field (was identical to the raw
`type` field) across ~30 consumer files
- Each type-specific renderer
(`NavigationDrawerItemForObjectMetadataItem`, `NavigationMenuItemIcon`,
link/folder components) now derives only the 1-2 display fields it
actually needs from the raw item + globally available Jotai atoms

Net result: -1199 / +1177 lines, 7 files deleted, 4 new utility files.
2026-03-17 00:33:06 +01:00
48285be164 i18n - translations (#18686)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-17 00:09:04 +01:00
Charles BochetandGitHub a121d00ddd feat: add color property to ObjectMetadata for object icon customization (#18672)
## Summary

- Adds a `color` column to `ObjectMetadataEntity` with full GraphQL
support so object icon colors are persisted at the metadata level
- Adds a `type` column to `NavigationMenuItemEntity` (enum: `OBJECT`,
`VIEW`, `FOLDER`, `LINK`, `RECORD`) replacing field-based type inference
- Updates frontend to read object colors from `objectMetadata.color`
(falling back to standard defaults) in the sidebar nav, record index
header, and record show breadcrumb
- Simplifies `NavigationMenuItemIcon` color resolution via
`getEffectiveNavigationMenuItemColor` util

## Color rules

| Item type | Color source | Editable in sidebar? |
|-----------|-------------|---------------------|
| **Object** | `objectMetadata.color` | Yes — persisted to
`objectMetadata.color` on Save |
| **Folder** | `navigationMenuItem.color` | Yes |
| **Link** | Fixed default (`DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK`) |
No |
| **View** | `objectMetadata.color` (from the parent object) | No |
| **Record** | None | No |

- **Object** items represent the whole object (e.g. "Companies") and
point to the INDEX view. Changing their color updates
`objectMetadata.color` via `useSaveObjectMetadataColorsFromDraft`.
- **View** items represent specific non-INDEX views. Their color comes
from the parent object's metadata (read-only).
- Only **folders** store their color on `navigationMenuItem.color` —
enforced by `hasNavigationMenuItemOwnColor` util.
- `getEffectiveNavigationMenuItemColor` returns `objectColor` for both
OBJECT and VIEW items, folder's own color for folders, and the fixed
default for links.

## NavigationMenuItemType enum

- Shared enum created in `twenty-shared` with values: `OBJECT`, `VIEW`,
`FOLDER`, `LINK`, `RECORD`
- Registered as a GraphQL enum on the backend
- Replaces string literals across entity, DTOs, input, converters, and
frontend hooks
- Migration backfills existing rows: INDEX views → `OBJECT`, non-INDEX
views → `VIEW`, based on join with the view table

## Design decisions

- **OBJECT vs VIEW distinction**: Items pointing to INDEX views are
typed as `OBJECT` (represent the whole object, color editable). Items
pointing to non-INDEX views are typed as `VIEW` (specific view, color
read-only from parent object).
- **Dual color storage**: `navigationMenuItem.color` is preserved for
folders only. Objects use `objectMetadata.color` as their source of
truth.
- **Type discriminator**: The `type` column replaces field-based
inference (checking `viewId`, `link`, `targetRecordId` presence) with an
explicit enum, simplifying `isNavigationMenuItemLink` /
`isNavigationMenuItemFolder` to simple `item.type ===` checks.
- **No settings page color picker**: Object color editing is done from
the sidebar edit panel, not the data model settings page.

## Test plan

- [ ] Verify objects display their default standard colors in the
sidebar
- [ ] Verify object color editing works in the sidebar edit panel
(persists to objectMetadata.color)
- [ ] Verify folder color editing works in the sidebar edit panel
- [ ] Verify views, links, and records do NOT show a color picker in the
sidebar edit panel
- [ ] Run `npx nx typecheck twenty-front` and `npx nx typecheck
twenty-server`
- [ ] Verify the database migrations add `color` to `objectMetadata` and
`type` to `navigationMenuItem`


Made with [Cursor](https://cursor.com)
2026-03-16 23:54:56 +01:00
087ee19807 i18n - translations (#18683)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-16 18:17:39 +01:00
Félix MalfaitandGitHub c4e55d08ff fix: allow identical singular and plural labels for objects (#18678)
## Summary

Closes #18673

Some languages (e.g., German "Unternehmen") and even English words
(sheep, deer, aircraft, series) have identical singular and plural
forms. Twenty previously blocked saving when labels matched, making it
impossible to correctly name objects in these cases.

- **Labels** are purely display strings — removed the equality
validation from both the frontend Zod schema and backend validator
- **API names** (nameSingular/namePlural) must stay different since they
generate distinct GraphQL resolvers (`findOne` vs `findMany`,
`createOne` vs `createMany`, etc.) and REST endpoints — this validation
is preserved
- Added a shared `computeMetadataNamesFromLabels` util in
`twenty-shared` that auto-appends `'s'` to the plural API name when both
labels produce the same camelCase name (e.g., "Unternehmen" →
`unternehmen` / `unternehmens`)
- Both the frontend form and backend sync-check use the same shared util
— single source of truth, no duplicated logic

**No retroactive impact**: since the old code prevented identical labels
from ever being saved, no existing workspace has `labelSingular ===
labelPlural`.

## Test plan

- [x] New unit tests for `computeMetadataNamesFromLabels` (7 tests:
standard labels, Sheep, Unternehmen, Aircraft, empty labels, different
labels, applyCustomSuffix)
- [x] Updated frontend schema validation tests (identical labels with
different names now passes; identical names still fails)
- [x] Updated backend integration test cases (removed identical-label
failing cases)
- [ ] Manual: create a new object with identical singular/plural labels
(e.g. "Sheep" / "Sheep") — should save successfully with API names
`sheep` / `sheeps`
- [ ] Manual: verify existing objects with different labels still work
unchanged


Made with [Cursor](https://cursor.com)
2026-03-16 18:07:34 +01:00
13ff7af297 Update sidebar section toggle to match Figma chevron (#18631)
Summary
- thicken the sidebar toggle chevron to align with the Figma design
- add animation for the chevron when sections open or close



https://github.com/user-attachments/assets/67bff9e1-4df5-4ff2-a48d-b761030ada51

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
2026-03-16 18:04:26 +01:00
WeikoandGitHub b6b96be603 Fix custom object view fields creation (#18629) 2026-03-16 17:24:44 +01:00
Baptiste DevessierandGitHub 93bd4960e3 Fix styles side column (#18632)
## Before



https://github.com/user-attachments/assets/7abe2131-52b8-4254-a50e-0043f6d5fbe8



## After


https://github.com/user-attachments/assets/5c350dbe-cd57-4952-ad12-29a7d3cd33fe
2026-03-16 17:17:07 +01:00
Assad RoblesandGitHub cb4efb1d0e fix: respect standard object rename across all locales (Closes #18650) (#18652)
## Fix: Standard object rename ignored when UI language is not English
(Closes #18650)

### Problem
When a user renames a standard object (for example, changing **"People"
→ "Contacts"**), the custom name is only respected when the UI language
is set to **English**.

For any other locale, the resolver ignores the user-defined override and
falls back to the i18n translation of the original default label.

As a result, the custom name defined by the user is not displayed when
the UI language changes.

### Root Cause
`resolveObjectMetadataStandardOverride` only applied direct overrides
when the locale matched `SOURCE_LOCALE` (English):

```ts
if (
  safeLocale === SOURCE_LOCALE &&
  isNonEmptyString(objectMetadata.standardOverrides?.[labelKey])
) {
  return objectMetadata.standardOverrides[labelKey] ?? '';
}
2026-03-16 17:01:04 +01:00
Charles BochetandGitHub 5dfdc1d81d refactor: consolidate database query timeout config variables (#18670)
## Summary

- Removes the redundant `DATABASE_STATEMENT_TIMEOUT_MS` config variable
(default 15s) from `ConfigVariables`
- Updates the core TypeORM datasource to use
`PG_DATABASE_PRIMARY_TIMEOUT_MS` (default 10s) for its `query_timeout`,
aligning it with the workspace datasource which already uses this
variable
- This consolidates two separate env vars that controlled the same
concern (database query timeout) into a single one
2026-03-16 15:10:38 +01:00
Thomas TrompetteandGitHub 32d7fa09a3 Fix timeline activities + breadcrumb (#18626)
Before
<img width="412" height="223" alt="Capture d’écran 2026-03-13 à 17 42
37"
src="https://github.com/user-attachments/assets/350edca5-53a3-4ff9-8c81-80d012bf2170"
/>

After
<img width="412" height="251" alt="Capture d’écran 2026-03-13 à 17 43
06"
src="https://github.com/user-attachments/assets/75a86a10-b551-416b-9103-6db03a6ca395"
/>
2026-03-16 15:10:11 +01:00
nitinandGitHub e552704201 fix: add viewFieldGroupId to ViewField fragment and connect page layout selectors to live metadata store (#18676) 2026-03-16 14:24:41 +01:00
ddedecbb36 i18n - docs translations (#18677)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-16 14:14:29 +01:00
5011e1d77b i18n - docs translations (#18674)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
2026-03-16 13:02:42 +01:00
a07337fea0 fix: return method-specific MCP responses (#18671)
## Summary

Fixes #18524 

Fixes the MCP response contract for non-`initialize` methods.

Previously, `/mcp` returned initialize-style metadata for methods like
`tools/list`, which caused strict MCP clients to reject the response
shape. The endpoint also returned `201 Created` for RPC calls even
though no resource was being created.

## Changes

- return only method-specific payloads for MCP list methods
  - `tools/list` -> `{ tools: [...] }`
  - `prompts/list` -> `{ prompts: [] }`
  - `resources/list` -> `{ resources: [] }`
- keep MCP server metadata only on `initialize`
- make `/mcp` return `200 OK` instead of `201 Created`
- add regression tests for:
  - `tools/list` response shape
  - `prompts/list` response shape
  - `resources/list` response shape

## Why

Strict MCP clients expect:
- standard RPC transport semantics over HTTP
- method-specific JSON-RPC result payloads

Returning initialize metadata for non-`initialize` methods breaks that
expectation and can cause client deserialization or protocol validation
failures.

## Verification

- reproduced the issue locally against `/mcp`
- verified `tools/list` was previously returning initialize-style fields
- verified `tools/list` now returns only `result.tools`
- verified `/mcp` now returns `200 OK`
- ran targeted Jest tests:

```bash
cd /Users/apple/MyProjects/OpenSource/twenty/packages/twenty-server
npx jest --runInBand src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts src/engine/api/mcp/services/__tests__/mcp-tool-executor.service.spec.ts

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2026-03-16 12:46:35 +01:00
Abdullah.andGitHub 6e36ad9fa2 fix: mailparser vulnerable to cross-site scripting (#18664)
Resolves [Dependabot Alert
595](https://github.com/twentyhq/twenty/security/dependabot/595) and
[Dependabot Alert
596](https://github.com/twentyhq/twenty/security/dependabot/596).
2026-03-16 09:59:36 +01:00
Charles BochetandGitHub 5c745059ad refactor: remove "core" naming from views and eliminate converter layer (#18667)
## Summary

- **Remove all "core" prefixes** from the views system — the
metadata-based storage migration is complete, so `CoreView`,
`coreViewsSelector`, `getCoreViews`, etc. are now just `View`,
`viewsSelector`, `getViews`
- **Eliminate the entire converter layer** (15 files, ~850 lines
deleted) — `convertCoreViewToView` and all sub-converters were either
no-ops or trivially adding `__typename` / mapping identical enum values.
Local enums now re-export from generated GraphQL types directly (single
source of truth)
- **Unify `View` and `ViewWithRelations`** into one type —
`ViewWithRelations` is now a type alias for `View`, selectors return
data directly without conversion

### Backend
- Rename `@ObjectType('CoreView')` → `@ObjectType('View')` (and all
sub-entities)
- Rename resolver methods: `getCoreViews` → `getViews`, `createCoreView`
→ `createView`, etc.
- Rename `FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION` →
`FIND_ALL_VIEWS_GRAPHQL_OPERATION`

### Frontend
- Delete 15 converter files (`convertGqlView*ToView*`,
`convertView*ToGql`, `convertViewWithRelationsToView`)
- Re-export `ViewType`, `ViewKey`, `ViewFilterGroupLogicalOperator` from
generated enums (no more duplicate enum definitions with different
casing)
- Replace `ViewOpenRecordInType` with `ViewOpenRecordIn` from generated
- Remove `__typename` from all local view sub-types
- Remove unused `variant` from `ViewFilter`, make `displayValue` and
`definition` optional
- Rename ~45 GraphQL query/mutation files and all selectors to drop
"core" prefix
- Delete unused `viewsWithRelationsSelector`
2026-03-16 09:57:18 +01:00
6340b9e2f7 i18n - translations (#18669)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-16 09:49:16 +01:00
Abdullah.andGitHub 2c2f66b584 fix: DOMPurify contains a cross-site scripting vulnerability (#18665)
Resolves [Dependabot Alert
597](https://github.com/twentyhq/twenty/security/dependabot/597),
[Dependabot Alert
598](https://github.com/twentyhq/twenty/security/dependabot/598),
[Dependabot Alert
599](https://github.com/twentyhq/twenty/security/dependabot/599) and
[Dependabot Alert
600](https://github.com/twentyhq/twenty/security/dependabot/600).
2026-03-16 09:49:03 +01:00
Félix MalfaitGitHubClaudeclaude[bot] <41898282+claude[bot]@users.noreply.github.com>github-actionscubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
95a35f8a1d Implement OAuth 2.0 Dynamic Client Registration (RFC 7591) (#18608)
## Summary
This PR implements OAuth 2.0 Dynamic Client Registration (RFC 7591) and
OAuth 2.0 Protected Resource Metadata (RFC 9728) support, enabling
third-party applications to dynamically register as OAuth clients
without manual configuration.

## Key Changes

### OAuth Dynamic Client Registration
- **New Controller**: `OAuthRegistrationController` at `POST
/oauth/register` endpoint
  - Validates client metadata according to RFC 7591 specifications
  - Enforces PKCE-only public client model (no client secrets)
- Supports only `authorization_code` grant type and `code` response type
  - Rate limits registrations to 10 per hour per IP address
  - Returns `client_id` and registration metadata in response

- **Input Validation**: `OAuthRegisterInput` DTO with constraints on:
  - Client name (max 256 chars)
  - Redirect URIs (max 20, validated for security)
  - Grant types, response types, scopes, and auth methods
  - Logo and client URIs (max 2048 chars)

- **Discovery Endpoint Update**: Added `registration_endpoint` to OAuth
discovery metadata

### Stale Registration Cleanup
- **Cleanup Service**: Automatically removes OAuth-only registrations
older than 30 days that have no active installations
- **Cron Job**: Runs daily at 02:30 AM UTC with batch processing (100
records per batch)
- **CLI Command**: `cron:stale-registration-cleanup` to manually trigger
cleanup

### MCP (Model Context Protocol) Authentication
- **New Guard**: `McpAuthGuard` implements RFC 9728 compliance
  - Wraps JWT authentication with proper error responses
- Returns `WWW-Authenticate` header with protected resource metadata URL
on 401
  - Enables OAuth-protected MCP endpoints

### Protected Resource Metadata
- **New Endpoint**: `GET /.well-known/oauth-protected-resource` (RFC
9728)
  - Advertises MCP resource as OAuth-protected
  - Lists supported scopes and bearer token methods
  - Enables OAuth clients to discover authorization requirements

### Application Registration Updates
- **New Source Type**: `OAUTH_ONLY` enum value for OAuth-only
registrations
- **Install Service**: Skips artifact installation for OAuth-only apps
(no code artifacts)

### Frontend Updates
- **Authorization Page**: Support both snake_case (standard OAuth) and
camelCase (legacy) query parameters
  - `client_id` / `clientId`
  - `code_challenge` / `codeChallenge`
  - `redirect_uri` / `redirectUrl`

## Implementation Details

- **Rate Limiting**: Uses token bucket algorithm with 10 registrations
per 3,600,000ms window per IP
- **Scope Validation**: Requested scopes are capped to allowed OAuth
scopes; defaults to all scopes if not specified
- **Redirect URI Validation**: Uses existing `validateRedirectUri`
utility for security
- **Cache Headers**: Registration responses include `Cache-Control:
no-store` and `Pragma: no-cache`
- **Batch Processing**: Cleanup operations process 100 records at a time
to avoid memory issues
- **Grace Period**: 30-day grace period before cleanup to allow time for
client activation

https://claude.ai/code/session_01PxcuWFFRuXMASMaMGTLYk2

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-03-16 09:42:28 +01:00
Abdullah.andGitHub 87c519b72f fix: multer vulnerable to denial of service via uncontrolled recursion (#18659)
Resolves [Dependabot Alert
608](https://github.com/twentyhq/twenty/security/dependabot/608).
2026-03-16 09:08:40 +01:00
Abdullah.andGitHub 1b20bdaf6d fix: @isaacs/brace-expansion has uncontrolled resource consumption (#18660)
Resolves [Dependabot Alert
414](https://github.com/twentyhq/twenty/security/dependabot/414).
2026-03-16 09:08:25 +01:00
Abdullah.andGitHub 67866ff59c fix: expr-eval related dependabot alerts (#18661)
Resolves [Dependabot Alert
593](https://github.com/twentyhq/twenty/security/dependabot/593) and
[Dependabot Alert
594](https://github.com/twentyhq/twenty/security/dependabot/594).

expr-eval was last published six years ago and had changes five years
ago. NPM contains a fork that published the changes from five years ago
under the same name with `-fork` suffix. This PR uses that fork as
suggested by Dependabot.
2026-03-16 09:08:19 +01:00
Abdullah.andGitHub e6f1bdd1c8 fix: yauzl contains an off-by-one error (#18662)
Resolves [Dependabot Alert
633](https://github.com/twentyhq/twenty/security/dependabot/633).
2026-03-16 09:08:12 +01:00
Charles BochetandGitHub ba9aa41bba refactor: metadata store cleanup, SSE unification, mock metadata loading & login redirect fix (#18651)
## Summary
- **SSE unification**: Replaced 11 individual SSE effect components with
a single generic `MetadataStoreSSEEffect`
- **Metadata store cleanup**: Merged `metadataCollectionHashesState`
into `metadataStoreState` (currentCollectionHash / draftCollectionHash
per entity), moved `objectMetadataItemsSelector` to `object-metadata`
domain, converted `navigationMenuItemsState` to a derived selector
- **Naming clarity**: Renamed `isAppMetadataReadyState` →
`isMinimalMetadataReadyState`, `MetadataGater` → `MinimalMetadataGater`,
`useIsLogged` → `useHasAccessTokenPair`,
`patchMetadataStoreFromSSEEvent` now takes named object params
- **Mock metadata loading**: Added `generate-navigation-menu-items.ts`
script, rewrote `useLoadMockedMinimalMetadata` to load full
objects/fields/indexes/views/navItems from generated mock data, enabling
proper sign-in background rendering (table columns, view picker,
navigation)
- **Login/logout transitions**: `MinimalMetadataLoadEffect` manages
mocked↔real metadata transitions based on auth state,
`MainContextStoreProvider` computes context on auth pages for view
picker support
- **Login redirect fix**: `handleLoadWorkspaceAfterAuthentication` now
re-enables `isAppEffectRedirectEnabled` after `loadCurrentUser()`
completes, fixing the blocked post-login navigation
- **Dead code removal**: Deleted `useRefreshPageLayouts`,
`useApplyPageLayouts`, `useStaleMetadataEntities`,
`metadataCollectionHashesState`, and all individual SSE effects

## Test plan
- [x] Login from welcome page redirects to companies page
- [x] Logout transitions cleanly to mocked metadata on welcome page
- [x] Sign-in background shows table columns, view picker, and
navigation items
- [x] SSE events still update metadata store entries correctly
- [x] Navigation menu items persist across page refreshes
- [ ] CI: lint, typecheck, tests pass
2026-03-16 00:38:11 +01:00
Charles BochetandGitHub 06efee1eef feat: hash-based metadata staleness detection (#18649)
## Summary

Replace the single `metadataVersion` integer with per-entity-type
**collection hashes** for granular metadata staleness detection. The
backend already generates a UUID per flat entity map on each cache
recompute (`crypto.randomUUID()` in `WorkspaceCacheService`); we now
expose these via the minimal metadata endpoint and SSE events so the
frontend can compare and know exactly which entity types are stale.

### Key changes

**Backend:**
- `WorkspaceCacheService.getCacheHashes()` — new public method that
reads only `:hash` keys from Redis without fetching full data
- `MinimalMetadataDTO` — added `collectionHashes: Record<string,
string>` (JSON scalar mapping `AllMetadataName` → collection hash),
removed `metadataVersion`
- `MetadataEventDTO` — added optional `updatedCollectionHash` field to
SSE events
- `MetadataEventsToDbListener` — reads the collection hash for the
affected entity type after cache invalidation and attaches it to the SSE
event before publishing
- `MinimalMetadataService` — no longer queries the workspace table; uses
`getCacheHashes()` for all flat entity maps and maps cache keys to
`AllMetadataName` locally

**Frontend:**
- `metadataCollectionHashesState` — new Jotai atom with
`atomWithStorage` + `getOnInit: true` storing
`Partial<Record<MetadataEntityKey, string>>`
- `mapAllMetadataNameToEntityKey()` — explicit mapping from backend
`AllMetadataName` to frontend `MetadataEntityKey` (23 entries)
- `useLoadMinimalMetadata` — stores `collectionHashes` from server,
computes `staleEntityKeys` by comparing local vs server hashes
- `patchMetadataStoreFromSSEEvent()` — accepts optional
`updatedCollectionHash` and updates `metadataCollectionHashesState`
- All 11 SSE effect components — pass
`eventDetail.updatedCollectionHash` through to the patch function
- `useStaleMetadataEntities` — new hook returning entity keys missing
from collection hashes (not yet loaded/synced)
- `resetMetadataStore()` — also clears collection hashes
- Deleted `metadataVersionState` (superseded by collection hashes)

### Design decisions

- **No change to hash generation** — existing `crypto.randomUUID()` is
sufficient. Hashes are persisted in Redis, survive server restarts, and
change only on `invalidateAndRecompute`.
- **"Collection hash" naming** — used consistently to clarify the hash
represents an entire entity collection (e.g., all views), not a single
record.
- **Mapping localized** — backend `WorkspaceCacheKeyName` →
`AllMetadataName` mapping lives in the minimal metadata service.
Frontend `AllMetadataName` → `MetadataEntityKey` mapping lives in a
local utility. Nothing in `twenty-shared`.
- **Backward compatible** — `collectionHashes` is additive;
`updatedCollectionHash` is nullable.
2026-03-14 23:38:37 +01:00
Charles BochetandGitHub 7a3540788a feat: uniformize metadata store with flat types, SSE alignment, presentation endpoint & localStorage (#18647)
## Summary

Uniformizes the metadata store to support **all** backend flat metadata
types, introduces a **minimal metadata endpoint** for fast initial
renders, replaces custom localStorage persistence with **Jotai's
built-in `atomWithStorage`**, and wires up a
**MinimalMetadataLoadEffect** for stale-while-revalidate loading.

### Key changes

- **All flat metadata types**: Added `FlatCommandMenuItem`,
`FlatFrontComponent`, `FlatWebhook`, `FlatRole`, `FlatRoleTarget`,
`FlatAgent`, `FlatSkill`, `FlatRowLevelPermissionPredicate`,
`FlatRowLevelPermissionPredicateGroup` — every entity in the backend
`MetadataEntityTypeMap` now has a corresponding frontend flat type
registered in `ALL_METADATA_ENTITY_KEYS` and `MetadataEntityTypeMap`.

- **Minimal metadata endpoint** (`minimalMetadata` GraphQL query): New
backend module (`MinimalMetadataModule`) returns lightweight object
metadata (names, icons, labels, flags) and basic views (id, type, key,
objectMetadataId) plus a `metadataVersion`. This enables fast first
paint before full metadata loads.

- **Jotai `atomWithStorage` for persistence**: Replaced the custom
`MetadataLocalStorageEffect` with Jotai's built-in `atomWithStorage` on
both `metadataStoreState` (family) and `metadataVersionState`. Added
`localStorageOptions` support to `createAtomFamilyState` for `{
getOnInit: true }` synchronous hydration. Each entity atom auto-persists
under keys like `metadataStoreState__objectMetadataItems`.

- **MinimalMetadataLoadEffect**: New effect mounted before
`MetadataProviderInitialEffects` that checks if the store already has
data (from Jotai localStorage hydration). If empty, it fetches minimal
metadata from the new endpoint. The full metadata load continues in
parallel, eventually enriching the store with complete data.

- **SSE effects alignment**: All metadata entity types now have
corresponding SSE effects that directly patch the metadata store via
`patchMetadataStoreFromSSEEvent`.

- **Existing selectors and joining logic**:
`objectMetadataItemsWithFieldsSelector`, `viewsWithRelationsSelector`,
`pageLayoutsWithRelationsSelector` reconstruct nested data from flat
entities for components that need it.

### Loading flow

```
App mount
  → Jotai atomWithStorage hydrates store from localStorage (sync, getOnInit)
  → MinimalMetadataLoadEffect
      → Store has data? → skip (app renders immediately)
      → Store empty? → fetch minimalMetadata endpoint → populate objects + views
  → MetadataProviderInitialEffects (full metadata load, runs in parallel)
  → LazyMetadataLoadEffect (page layouts, logic functions, nav menu, etc.)
  → IsAppMetadataReadyEffect (sets isAppMetadataReady)
```

## Test plan

- [ ] Verify app loads with empty localStorage (should fetch minimal
metadata, then full)
- [ ] Verify app loads with populated localStorage (should skip minimal
fetch, render immediately)
- [ ] Verify SSE events correctly update metadata store for all entity
types
- [ ] Verify logout clears metadata store (atom reset propagates to
localStorage)
- [ ] Verify all metadata selectors return correct joined data
- [ ] CI: lint, typecheck, tests pass
2026-03-14 20:32:25 +01:00
6711b40922 i18n - docs translations (#18645)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-14 16:49:41 +01:00
c753b2bee1 i18n - docs translations (#18644)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-14 13:57:57 +01:00
70a060b4ee docs: fix contributor docs links and typos (#18637)
## Summary

This PR fixes several small documentation issues in the contributor and
setup guides:

- fixes broken docs links in the root README
- corrects multiple typos and capitalization issues in contributor docs
- fixes malformed Markdown for the Redis command in local setup
- improves wording in the Docker Compose self-hosting guide

## Changes

- updated README installation links to the current docs routes
- changed `Open-source` to `open-source`
- fixed `specially` -> `especially` in the frontend style guide
- normalized `MacOS` -> `macOS`, `powershell` -> `PowerShell`, and
`Postgresql` -> `PostgreSQL`
- replaced the invalid `localhost:5432` Markdown link with inline code
- fixed the malformed fenced code block for `brew services start redis`
- cleaned up Redis naming/capitalization and a few grammar issues in the
setup docs
- improved the warning and environment-variable wording in the Docker
Compose guide

## Testing

- not run; docs-only changes

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-14 12:54:31 +01:00
Charles BochetandGitHub 40ff109179 feat: migrate objectMetadata reads to granular metadata store (#18643)
## Summary

Consolidates `objectMetadataItems` onto the metadata store as the
**single source of truth**, replacing the previous dual-store approach
(separate `objectMetadataItemsState` atom + untyped
`metadataStoreState`).

### Architecture: three-layer design

```
┌─────────────────────────────────────────────────────────┐
│ Store Layer (granular, typed)                           │
│  objectMetadataItems → FlatObjectMetadataItem[]         │
│  fieldMetadataItems  → FlatFieldMetadataItem[]          │
│  indexMetadataItems  → FlatIndexMetadataItem[]           │
└────────────────┬────────────────────────────────────────┘
                 │ .current (never draft)
┌────────────────▼────────────────────────────────────────┐
│ Selectors (typed read-only)                             │
│  objectMetadataItemsSelector                            │
│  fieldMetadataItemsSelector                             │
│  indexMetadataItemsSelector                             │
│  metadataStoreStatusFamilySelector                      │
│  isSystemObjectByNameSingularFamilySelector (narrow)    │
│  activeObjectNameSingularsSelector (narrow)             │
└────────────────┬────────────────────────────────────────┘
                 │ joins objects + fields + indexes + permissions
┌────────────────▼────────────────────────────────────────┐
│ Joining Selector                                        │
│  objectMetadataItemsWithFieldsSelector                  │
│  → produces full ObjectMetadataItem[] with              │
│    readableFields / updatableFields from permissions    │
│  → 12 existing selectors repointed here                 │
└─────────────────────────────────────────────────────────┘
```

### Key changes

- **Granular flat types** (`FlatObjectMetadataItem`,
`FlatFieldMetadataItem`, `FlatIndexMetadataItem`) — objects stored
without embedded fields/indexes, matching backend "Flat" naming
convention
- **Typed write API** — `updateDraft` is now generic via
`MetadataEntityTypeMap`, giving compile-time safety on what data shape
goes to each key
- **Write path refactored** — fetch → split into flat entities via
`splitObjectMetadataItemWithRelated` → write to metadata store directly.
No more dual-write through `objectMetadataItemsState`. Permissions
enrichment moved from write path into the joining selector.
- **SSE effects write directly** — `ObjectMetadataItemSSEEffect` and
`FieldMetadataSSEEffect` now patch the store from the SSE event payload
(create/update/delete) instead of triggering a full re-fetch
- **`objectMetadataItemsState` bridge** — converted from writable
`createAtomState` to read-only `createAtomSelector` that delegates to
the joining selector. All 100+ existing consumers continue to work
without code changes.
- **All selectors use Twenty state API** — `createAtomSelector` /
`createAtomFamilySelector` throughout, no raw `atom()`
- **Narrow selectors** for hot paths —
`isSystemObjectByNameSingularFamilySelector` and
`activeObjectNameSingularsSelector` read from flat objects only,
avoiding re-renders when fields/indexes/permissions change. Placed in
`object-metadata/states/` as higher-level business selectors.
- **Test helper** — `setTestObjectMetadataItemsInMetadataStore` for
tests that need to set up composite object metadata through the store
(clearly named as a testing utility)

### Naming conventions

- `ObjectMetadataItemWithRelated` — type for objects with embedded
fields/indexes (input to split utility)
- `FlatObjectMetadataItem` / `FlatFieldMetadataItem` /
`FlatIndexMetadataItem` — granular store types
- Selector names don't expose "Current" — that's an internal detail of
the metadata store API

### Future work

- Optimistic update API (`updateCurrentOptimistically` with rollback)
- Migrate remaining entities (views, pageLayouts, etc.) to the same
pattern
- Gradually remove `objectMetadataItemsState` bridge once all direct
imports are replaced

## Test plan

- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx lint:diff-with-main twenty-front` passes
- [ ] Verify app loads correctly with metadata from the store
- [ ] Verify SSE updates (object/field changes) propagate correctly
- [ ] Run existing test suites to confirm no regressions
2026-03-14 12:54:19 +01:00
WeikoandGitHub 48172d60fd View field override (#18572)
## Context
This PR introduces overrides for view fields which will be useful for
page layout FIELDS widgets fields position/groups/visibility override +
restore logic.
2026-03-14 12:40:15 +01:00
Thomas des FrancsandGitHub 0b0ffcb8fa Add pitfall reminders to LLMS guidance (#18627)
please chat, no scroll in scroll on dashboards 🙏
2026-03-14 10:53:42 +00:00
6552ec83ec i18n - translations (#18642)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-14 11:03:11 +01:00
Charles BochetandGitHub 602db4ffea feat: enable Rich Text as a creatable field type (#18634)
## Summary

- Removes `RICH_TEXT` from the excluded/hidden field types in the
settings UI so users can create rich text fields on any object (not just
Note/Task)
- Creates a generic `RichTextFieldEditor` component that uses standard
`useUpdateOneRecord` for persistence, decoupled from the
Note/Task-specific `ActivityRichTextEditor`
- Updates the inline `RichTextFieldInput` and side panel to route to the
appropriate editor based on object type (activity editor for Note/Task,
generic editor for everything else)

## Details

### Tier 1 — Settings UI unlock
- Removed `RICH_TEXT` from `excludedFieldTypes` in
`SettingsObjectNewFieldSelect.tsx`
- Removed `RICH_TEXT` from `SettingsExcludedFieldType` type union
- Added `RICH_TEXT` to `previewableTypes` in
`SettingsDataModelFieldSettingsFormCard`

### Tier 2 — Generic inline editing
- New `RichTextFieldEditor` — a generic BlockNote editor that works for
any object using `useUpdateOneRecord` (no activity-specific coupling)
- `RichTextFieldInput` now branches: `ActivityRichTextEditor` for
Note/Task, `RichTextFieldEditor` for all other objects
- Generalized side panel state (`viewableRichTextComponentState`) from
`activityId`/`activityObjectNameSingular` to
`recordId`/`objectNameSingular`/`fieldName`
- `useOpenRichTextInSidePanel` now accepts an optional `fieldName`
parameter

### Tier 3 — Verification
- Search: only `markdown` subfield is indexed (correct behavior)
- Filters: `RichTextFilter` GraphQL input type already exists
- Import/export: `markdown` subfield is already marked `isImportable:
true`
2026-03-14 10:57:27 +01:00
3a9247d9d1 i18n - translations (#18639)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-13 20:02:02 +01:00
6b48f197d4 feat: deprecate WorkspaceFavorite in favor of NavigationMenuItem (#18624)
## Summary

- **Removes the entire `modules/favorites/` directory** (~66 files,
~5000 lines deleted) — components, hooks, states, types, utils, tests,
and the favorite-folder-picker sub-module
- **Eliminates the dual-write pattern** where creating a favorite also
created a NavigationMenuItem — all consumers now use
`useCreateNavigationMenuItem` directly
- **Removes `IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED` feature flag
checks** from ~12 files, always taking the NavigationMenuItem code path
- **Cleans up backend dual-writes** in `object-metadata.service.ts` and
`twenty-standard-application.service.ts` that were creating Favorite
records alongside NavigationMenuItems
- **Updates prefetch system** to only load NavigationMenuItems (removes
favorites prefetch effects and states)
- **Cleans up test infrastructure** — updates Storybook decorators, mock
data, and graphql mocks to remove favorites references

### What was intentionally kept
- **Backend entity definitions** (`FavoriteWorkspaceEntity`,
`FavoriteFolderWorkspaceEntity`) — these define the database schema and
need a proper database migration to remove
- **Cascade deletion listeners** — still needed to clean up existing
Favorite data in workspaces that haven't been fully migrated
- **v1.18 migration commands** — needed for workspaces upgrading from
older versions

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 19:45:40 +01:00
2a8912b17a i18n - docs translations (#18636)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-13 19:40:17 +01:00
55d675bba7 i18n - translations (#18633)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-13 19:23:23 +01:00
Charles BochetandGitHub d9eb317bb5 feat: rename RICH_TEXT_V2 → RICH_TEXT in codebase (keep DB value) (#18628)
## Summary

- Renames the `FieldMetadataType` enum key from `RICH_TEXT_V2` to
`RICH_TEXT` across the entire codebase, while keeping the underlying
string value as `'RICH_TEXT_V2'` to maintain PostgreSQL database
compatibility
- Renames all related types, guards, hooks, components, and files from
`*RichTextV2*` / `*rich-text-v2*` to `*RichText*` / `*rich-text*` (e.g.
`FormRichTextV2FieldInput` → `FormRichTextFieldInput`,
`isFieldRichTextV2` → `isFieldRichText`)
- Updates generated files (GraphQL schema, SDK types) to use the new key
while preserving the `RICH_TEXT_V2` string value for DB/API layer
- Updates i18n locale files, test snapshots, and integration tests to
reflect the rename

## Context

The legacy `RICH_TEXT` (V1) field type was deprecated and migrated to
`TEXT` in a previous PR (#18623). With V1 gone, the `RICH_TEXT_V2`
naming is no longer necessary — `RICH_TEXT` is now the canonical name.
The DB enum value stays `'RICH_TEXT_V2'` to avoid confusion with the
just-deprecated V1 type and to prevent a database migration.

## Test plan

- [x] `twenty-server` typecheck passes
- [x] `twenty-front` typecheck passes (only pre-existing Apollo client
errors remain)
- [x] `twenty-server` lint passes
- [x] `twenty-front` lint passes
- [x] `twenty-shared` build passes
- [ ] CI passes


Made with [Cursor](https://cursor.com)
2026-03-13 19:07:55 +01:00
williamjusticedavisandGitHub 3054679411 fix: add missing React key props to ButtonGroup and FloatingButtonGro… (#18615)
Fix missing React key props on ButtonGroup and FloatingButtonGroup story
children
                  
JSX element arrays defined in Storybook args require explicit key props,
otherwise React emits a "missing key" warning in development. This adds
keys to the children arrays in ButtonGroup.stories.tsx and
FloatingButtonGroup.stories.tsx.
2026-03-13 16:19:30 +00:00
1b1d79b08f i18n - docs translations (#18625)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-13 17:32:09 +01:00
Charles BochetandGitHub 46e515436e Deprecate legacy RICH_TEXT field metadata type (#18623)
## Summary

- Removes the deprecated `RICH_TEXT` (V1) field metadata type from the
codebase entirely
- Adds a 1.20 upgrade command that migrates existing `RICH_TEXT` fields
to `TEXT` in `core.fieldMetadata`
- Cleans up ~70 files across `twenty-shared`, `twenty-server`,
`twenty-front`, `twenty-sdk`, and `twenty-zapier`

## Context

`RICH_TEXT` was a legacy field type that stored rich text as a single
`text` column. It was already **read-only** — writes threw errors
directing users to `RICH_TEXT_V2` instead. `RICH_TEXT_V2` is the current
approach: a composite type with `blocknote` (editor JSON) and `markdown`
subfields. Keeping the deprecated type added maintenance burden without
any value.

Since the underlying database column type for `RICH_TEXT` was already
`text` (same as `TEXT`), the migration only needs to update the metadata
— no data migration or column changes required.

## Changes

### Upgrade command (new)
- `1-20-migrate-rich-text-to-text.command.ts` — runs `UPDATE
core."fieldMetadata" SET "type" = 'TEXT' WHERE "type" = 'RICH_TEXT'` per
workspace, with cache invalidation

### Enum & shared types
- Removed `RICH_TEXT` from `FieldMetadataType` enum
- Removed from `FieldMetadataDefaultValueMapping`,
`isFieldMetadataTextKind`

### Server (~30 files)
- Removed from type mapper (scalar, filter, order-by), data processors,
input transformer, filter operators, zod schemas, column type mapping,
searchable fields, RLS matching, OpenAPI schema, fake value generators
- Removed from field creation flow and field metadata type validator
- Updated dev seeder Pet `bio` field to `TEXT`
- Cleaned up mocks, snapshots, integration tests

### Frontend (~25 files)
- Deleted: `RichTextFieldDisplay`, `isFieldRichText`,
`isFieldRichTextValue`, `useRichTextFieldDisplay`
- Removed from `FieldDisplay`, `usePersistField`, `isFieldValueEmpty`,
`isRecordMatchingFilter`, `generateEmptyFieldValue`,
`isFieldCellSupported`, spreadsheet import, workflow fake values
- Removed from settings types, field type configs, and field creation
exclusion list
- Updated tests, mocks, and stories

### SDK & Zapier
- Removed from generated GraphQL schema and TypeScript types
- Removed from Zapier `computeInputFields`
2026-03-13 17:25:40 +01:00
49bdcd6bd5 i18n - docs translations (#18621)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-13 17:16:43 +01:00
3f01249967 i18n - translations (#18620)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-13 17:16:32 +01:00
4b6c8d52e5 Improve type safety and remove unnecessary store operations (#18622)
## Summary
This PR improves type safety across the codebase by replacing generic
`any` types with proper TypeScript types, removes unnecessary record
store operations, and adds TODO comments for future refactoring of
useEffect hooks.

## Key Changes

### Type Safety Improvements
- **SettingsAgentTurnDetail.tsx**: Replaced `any` type annotations with
proper `AgentMessage` type from generated GraphQL types
- **useCreateManyRecords.ts**: Added `RecordGqlNode` type for better
type safety when handling mutation responses
- **useLazyFindOneRecord.ts**: Replaced generic `Record<string, any>`
with `Record<string, RecordGqlNode>` for improved type checking

### Removed Unnecessary Operations
- **EventCardCalendarEvent.tsx**: Removed unused
`useUpsertRecordsInStore` hook and its associated useEffect that was
upserting calendar event records to the store
- **EventCardMessage.tsx**: Removed unused `useUpsertRecordsInStore`
hook and its associated useEffect that was upserting message records to
the store

### Conditional Query Execution
- **useLoadCurrentUser.ts**: Made the `FindAllCoreViewsDocument` query
conditional - only executes when `isOnAWorkspace` is true, preventing
unnecessary queries for users not on a workspace

### Documentation
- Added TODO comments in multiple files (`useAgentChatData.ts`,
`useWorkspaceFromInviteHash.ts`, `useGetPublicWorkspaceDataByDomain.ts`,
`useFindManyRecords.ts`, `useSingleRecordPickerPerformSearch.ts`)
referencing PR #18584 for future refactoring of useEffect hooks to avoid
unnecessary re-renders

## Implementation Details
- The removal of store upsert operations suggests these records are
already being managed elsewhere or the operations were redundant
- Type improvements maintain backward compatibility while providing
better IDE support and compile-time checking
- Conditional query execution reduces unnecessary network requests and
improves performance for non-workspace users

https://claude.ai/code/session_01YQErkoHotMvM6VL3JkWAqV

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 17:14:56 +01:00
b470cb21a1 Upgrade Apollo Client to v4 and refactor error handling (#18584)
## Summary
This PR upgrades Apollo Client from v3.10.0 to v4 and refactors error
handling patterns across the codebase to use a new centralized
`useSnackBarOnQueryError` hook.

## Key Changes

- **Dependency Update**: Upgraded `@apollo/client` from `^3.10.0` to
`^3.11.0` in root package.json
- **New Hook**: Added `useSnackBarOnQueryError` hook for centralized
Apollo query error handling with snack bar notifications
- **Error Handling Refactor**: Updated 100+ files to use the new error
handling pattern:
  - Removed direct `ApolloError` imports where no longer needed
- Replaced manual error handling logic with `useSnackBarOnQueryError`
hook
- Simplified error handling in hooks and components across multiple
modules
- **GraphQL Codegen**: Updated codegen configuration files to work with
Apollo Client v3.11.0
- **Type Definitions**: Added TypeScript declaration file for
`apollo-upload-client` module
- **Test Updates**: Updated test files to reflect new error handling
patterns

## Notable Implementation Details

- The new `useSnackBarOnQueryError` hook provides a consistent way to
handle Apollo query errors with automatic snack bar notifications
- Changes span across multiple feature areas: auth, object records,
settings, workflows, billing, and more
- All changes maintain backward compatibility while improving code
maintainability and reducing duplication
- Jest configuration updated to work with the new Apollo Client version

https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 14:59:46 +01:00
172bbd01bc Add Gemini 3.1 Flash Lite model to AI registry (#18597)
## Summary
- Adds `gemini-3.1-flash-lite-preview` to the Google AI models registry
- Ultra-low-cost Gemini model ($0.25/M input, $1.50/M output) — half the
price of Gemini 3 Flash
- 1M context window, 64K max output, supports dynamic thinking
- No service code changes needed — the existing `AiModelRegistryService`
auto-discovers models from constants

## Changes
- `ai-models-types.const.ts`: Added `gemini-3.1-flash-lite-preview` to
the `ModelId` type union
- `google-models.const.ts`: Added model configuration with pricing,
context window, and capabilities

## Test plan
- [ ] `npx nx typecheck twenty-server` passes
- [ ] `npx nx lint twenty-server` passes
- [ ] With `GOOGLE_API_KEY` set, model appears in available models list
- [ ] Existing Gemini models unaffected

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 13:22:08 +00:00
58f534939c i18n - docs translations (#18617)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-13 13:36:07 +01:00
Charles BochetandGitHub 0379aea0b1 fix: split tsvector migration, add configurable DB timeout, reorder 1.19 commands (#18614)
## Summary

- **Split tsvector migration into individual per-field transactions**:
each tsvector field now runs in its own
`workspaceMigrationRunnerService.run()` call (its own DB transaction).
Since STORED generated columns trigger full table rewrites, a timeout on
one large table (e.g. `timelineActivity`) no longer rolls back the
others. Each field has its own idempotency check, so the migration is
fully resumable.
- **Add configurable `DATABASE_STATEMENT_TIMEOUT_MS` env var** (default
15000ms): controls the `query_timeout` on the core datasource globally,
allowing operators to raise it for long-running upgrade commands without
code changes.
- **Reorder 1.19 upgrade commands**: move
`fixRoleAndAgentUniversalIdentifiersCommand` first so that subsequent
commands see corrected universal identifiers.
2026-03-13 12:59:31 +01:00
martmullGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
f3e0c12ce6 Fix app install file upload (#18593)
remove wrong file path based file selection

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-03-13 11:06:14 +00:00
Baptiste DevessierandGitHub 0641e07ca6 Bring back relations notes tasks targets (#18600)
## Demo when view isn't defined (front-end mock)


https://github.com/user-attachments/assets/2414076b-a96e-49ef-af02-c72a8e0e80de

## Demo when view is defined


https://github.com/user-attachments/assets/a94487a3-68ec-4d5f-8b33-d6b7242455d4
2026-03-13 10:57:33 +00:00
Thomas TrompetteandGitHub dfd28f5b4a Separate create draft cases op (#18613)
Bug: When creating a draft from an activated workflow version, the draft
row was inserted into the database without steps and trigger, then
updated with them in a separate operation. The SSE create-one event
fired on the INSERT, causing the frontend to refetch the draft before
the UPDATE — resulting in steps: null and trigger: null, which crashed
the step editor.

Fix: Reorder the operations so steps are duplicated first, then either
insert a new draft or update an existing one with steps and trigger
already populated. The row never exists in the database without complete
data.
2026-03-13 10:43:20 +00:00
Raphaël BosiandGitHub 349bfc8462 Backfill existing workspaces with standard command menu items (#18596)
Create a command to backfill command menu items.
2026-03-13 09:03:15 +00:00
Baptiste DevessierandGitHub 262f9f5fe1 Re-fetch conditional display property in the frontend (#18601) 2026-03-13 08:32:53 +00:00
Félix MalfaitGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>claude[bot] <41898282+claude[bot]@users.noreply.github.com>Claude Opus 4.6
5f558e5539 fix: accept production enterprise keys in development environment (#18611)
## Problem
When `NODE_ENV` is development, the server was only using the dev public
key to verify enterprise JWTs. Production keys are signed with the
production private key, so they failed verification with the dev public
key, resulting in "Invalid enterprise key" errors.

## Solution
Try both production and dev public keys when in development, so
production keys work when testing locally. In production, only the
production key is used (unchanged behavior).

## Changes
- `enterprise-plan.service.ts`: Replaced `getPublicKey()` with
`getPublicKeysToTry()` that returns both keys in development; updated
`verifyJwt()` to try each key until one succeeds
- `enterprise-plan.service.spec.ts`: Added test for production key
acceptance when `NODE_ENV` is development

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:20:14 +01:00
WeikoandGitHub 1cb4c98cb3 Add dataloader and read from cache for view entities (#18594)
## Context
Improve view resolution using cache and dataloader

## Performance Comparison

|Run|Main (no DataLoaders/cache)|Feature Branch (DataLoaders +
cache)|Speedup|
|---|---|---|---|
|1 (cold)|418ms|95ms|~4.4x faster|
|2|42ms|19ms|~2.2x faster|
|3|37ms|19ms|~1.9x faster|
|4|39ms|12ms|~3.2x faster|
|5|33ms|13ms|~2.5x faster|

The biggest improvement is to use dataloaders for the multiple relations
associated with views. Cache is a bit less significant since there are
other cache mechanism such as PostgreSQL buffer cache but it will
probably be more meaningful with bigger workspaces
2026-03-12 18:05:30 +00:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Devessier
a3c392ce8b Reset selected widget when exiting record page layout edit mode (#18603)
## Before


https://github.com/user-attachments/assets/b9720898-3433-488b-b784-1fa78e4e68f7

## After


https://github.com/user-attachments/assets/8f3fdde5-773d-44c4-a0f5-cca683736782

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Devessier <29370468+Devessier@users.noreply.github.com>
2026-03-12 17:55:01 +00:00
Charles BochetandGitHub ab13020e2b Fix wrong uuid error on field metadata (#18598)
## Summary
- Same fix as #18590 but applied to `FieldMetadataDTO`
- Changed `universalIdentifier` from `UUID` to `String` type since field
metadata universal identifiers are not necessarily valid UUIDs
- Removed `universalIdentifier` from `FieldFilter` (was using
`UUIDFilterComparison`)
- Updated generated SDK and frontend types accordingly
2026-03-12 18:51:42 +01:00
WeikoandGitHub 3f420c84d7 Fix Flow tab missing for workflow run (#18602)
## Context
Conditional tab rendering was recently introduced for system objects
that now have record page layouts. However Workflow run is a system
object and has a specific "Flow" tab that was not displayed anymore

## Before
<img width="1191" height="640" alt="Screenshot 2026-03-12 at 18 10 03"
src="https://github.com/user-attachments/assets/6f2c6319-6ddf-4906-a83c-0db8a27a8267"
/>

## After
<img width="1299" height="802" alt="Screenshot 2026-03-12 at 18 09 35"
src="https://github.com/user-attachments/assets/35e1e356-e995-43a2-9207-adc0f67cc426"
/>
2026-03-12 17:25:12 +00:00
0ef4741473 i18n - translations (#18595)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-12 18:17:42 +01:00
WeikoandGitHub b5db955ac8 Fix sdk metadata client codegen (#18599)
## Context
Previous token was tied to a non-existing token and codegen was failing
locally due to the server throwing.
This is due to a regression introduced here
https://github.com/twentyhq/twenty/pull/18590/changes#diff-848fff5d5b6f9858c8e2391212dfa9da5151cd3b1325d410df8a82250a229558L26
where a token is hardcoded instead of using the one from the ENV
2026-03-12 18:13:37 +01:00
Baptiste DevessierandGitHub 2a6fcfcfb3 Side Panel Sub Page Framework® (#18579)
Replace hard-coded implementations for sub pages in the side panel with
a proper framework
2026-03-12 15:17:29 +00:00
5bfa4c5c39 Fix wrong uuid error (#18590)
as title

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-12 16:12:30 +01:00
1685d066be i18n - translations (#18591)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-12 15:55:51 +01:00
Thomas TrompetteandGitHub 6a3281a18d Bug fix batches (#18588)
- clear sse state on logout
- fix no record not selectable through keyboard
- fix book a call design
- fix error notif design
2026-03-12 15:55:30 +01:00
Raphaël BosiandGitHub 741e9a8f81 Update yarn lock (#18589)
https://github.com/twentyhq/twenty/pull/18075
2026-03-12 15:42:01 +01:00
Charles BochetandGitHub 0897575fd0 Fix flaky return-to-path e2e tests (#18580)
## Summary

Fixes flaky `return-to-path` e2e tests that were failing intermittently
in CI merge queue runs.

**Root cause:** In the multi-workspace environment used by CI
(`IS_MULTIWORKSPACE_ENABLED=true`), navigating to
`localhost:3001/settings/accounts` triggers a full page redirect to
`app.localhost:3001/welcome` via `useRedirectToDefaultDomain`. This
redirect is a hard navigation (not a React Router transition), which
clears all in-memory Jotai state — including the `returnToPathState`
atom that stores the path the user should be redirected to after login.
After the redirect, the app has no memory of the intended destination
and falls back to `/objects/companies`.

**Fix:** Before performing the cross-domain redirect in
`useRedirectToDefaultDomain`, read the `returnToPath` from the Jotai
store and pass it as a URL search parameter. On the new page load,
`useInitializeQueryParamState` picks it up from the URL and re-hydrates
the Jotai atom, preserving the return-to-path across the full page
reload.

## Test plan

- [x] Verified locally against production build (`serve -s build`) with
`IS_MULTIWORKSPACE_ENABLED=true` — 33/33 consecutive passes of
`return-to-path.spec.ts`
- [x] Lint passes (`npx nx lint:diff-with-main twenty-front`)
2026-03-12 15:29:35 +01:00
501fcc737f i18n - translations (#18586)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-12 15:18:16 +01:00
Raphaël BosiandGitHub c9deab4373 [COMMAND MENU ITEMS] Remove standard front components (#18581)
All standard command menu items will link to an engine component instead
of standard front components.
2026-03-12 15:18:00 +01:00
MarieandGitHub c1da7be6d7 Billing for self-hosts (#18075)
## Summary

Implements enterprise licensing and per-seat billing for self-hosted
environments, with Stripe as the single source of truth for subscription
data.

### Components

- **twenty-website** hosts the private key to sign `ENTERPRISE_KEY` and
`ENTERPRISE_VALIDITY_TOKEN`. It communicates with Stripe to emit the
daily `ENTERPRISE_VALIDITY_TOKEN` if the subscription is active, based
on the user's Stripe subscription ID stored in `ENTERPRISE_KEY`.
- **Stripe** is the single source of truth for subscription data
(status, seats, billing).
- **The client** (twenty-server + DB + workers) saves `ENTERPRISE_KEY`
in the `keyValuePair` table (or `.env` if
`IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false) and the daily-renewed
`ENTERPRISE_VALIDITY_TOKEN` in the `appToken` table.
`ENTERPRISE_VALIDITY_TOKEN` is verified client-side using a public key
to grant access to enterprise features (RLS, SSO, audit logs, etc.).

### Flow

1. When requesting an upgrade to an enterprise plan (from **Enterprise**
in settings), the user is shown a modal to choose monthly/yearly
billing, then redirected to Stripe to enter payment details. After
checkout, they land on twenty-website where they are exposed to their
`ENTERPRISE_KEY`, which they paste in the UI. It is saved in the
`keyValuePair` table. On activation, a first `ENTERPRISE_VALIDITY_TOKEN`
with 30-day validity is stored in the `appToken` table.

2. **Every day**, a cron job runs and does two things:
- **Refreshes the validity token**: communicates with twenty-website to
get a new `ENTERPRISE_VALIDITY_TOKEN` with 30-day validity if the Stripe
subscription is still active. If the subscription is in cancellation,
the emitted token has a validity equal to the cancellation date. If it's
no longer valid, the token is not replaced. The cron only needs to run
every 30 days in practice, but runs daily so it's resilient to
occasional failures.
- **Reports seat count**: counts active (non-soft-deleted)
`UserWorkspace` entries and sends the count to twenty-website, which
updates the Stripe subscription quantity with proration. Seats are also
reported on first activation. If the subscription is canceled or
scheduled for cancellation, the seat update is skipped.

3. `ENTERPRISE_VALIDITY_TOKEN` is verified server-side via a public key
to grant access to enterprise features.

### Key concepts

Three distinct checks are exposed as GraphQL fields on `Workspace`:

| Field | Meaning |
|---|---|
| `hasValidEnterpriseKey` | Has any valid enterprise key (signed JWT
**or** legacy plain string) |
| `hasValidSignedEnterpriseKey` | `ENTERPRISE_KEY` is a properly signed
JWT (billing portal makes sense) |
| `hasValidEnterpriseValidityToken` | `ENTERPRISE_VALIDITY_TOKEN` is
present and not expired (expiration depends on signed token payload, not
on "expiresAt" on appToken table which is only indicative) |

Feature access is gated by `isValid()` =
`hasValidEnterpriseValidityToken || hasValidEnterpriseKey` (to support
both new and legacy keys during transition). After transition isValid()
= hasValidEnterpriseValidityToken

### Frontend states

The Enterprise settings page handles multiple states:
- **No key**: show "Get Enterprise" with checkout modal
- **Orphaned validity token** (token valid but no signed key): prompt
user to set a valid enterprise key
- **Active/trialing but no validity token**: show subscription status
with a "Reload validity token" action
- **Active/trialing**: show full subscription info, billing portal
access, cancel option
- **Cancellation scheduled**: show cancellation date, billing portal
- **Canceled**: show billing history link and option to start a new
subscription
- **Past due / Incomplete**: prompt to update payment or restart

### Temporary retro-compatibility: legacy plain-text keys

Previously, enterprise features were gated by a simple check: any
non-empty string in `ENTERPRISE_KEY` granted access. With this PR, we
transition to a controlled system relying on signed JWTs.

To avoid breaking existing self-hosted users:
- **Legacy plain-text keys still grant access** to enterprise features.
`hasValidEnterpriseKey` returns `true` for both signed JWTs and plain
strings, and `isValid()` checks `hasValidEnterpriseKey` as a fallback
when no validity token is present.
- **A deprecation banner** is shown at the top of the app when
`hasValidEnterpriseKey` is `true` but `hasValidSignedEnterpriseKey` is
`false`, informing the user that their key format is deprecated and they
should activate a new signed key.
- **No billing portal or subscription management** is available for
legacy keys since there is no Stripe subscription to manage.

This retro-compatibility will be removed in a future version. At that
point, `isValid()` will only check `hasValidEnterpriseValidityToken`.

### Edge cases

- **Air-gapped / production environments**: for self-hosted clients that
block external traffic (or for our own production), provide a long-lived
`ENTERPRISE_VALIDITY_TOKEN` (e.g. 99 years) directly in the `appToken`
table, with no `ENTERPRISE_KEY`. The daily cron will skip the refresh
(no enterprise key to authenticate with), but the pre-seeded validity
token will be used to grant feature access. No billing or seat reporting
occurs in this mode.
- **`IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false**: if the user tries to
activate an enterprise key but DB config writes are disabled, the
backend returns a clear error asking them to add `ENTERPRISE_KEY` to
their `.env` file manually.
- **Canceled subscriptions**: the `/seats` endpoint skips Stripe updates
for canceled or cancellation-scheduled subscriptions to avoid Stripe API
errors.

### How to test
- launch twenty-website on a different url (eg localhost:1002)
- add ENTERPRISE_API_URL=http://localhost:3002/api/enterprise (or else)
in your server .env
- ask me for twenty-website's .env file content (STRIPE_SECRET_KEY;
STRIPE_ENTERPRISE_MONTHLY_PRICE_ID;STRIPE_ENTERPRISE_YEARLY_PRICE_ID;
ENTERPRISE_JWT_PRIVATE_KEY; ENTERPRISE_JWT_PUBLIC_KEY;
NEXT_PUBLIC_WEBSITE_URL)
- visit Admin panel / enterprise
2026-03-12 15:07:53 +01:00
WeikoandGitHub c59f420d21 Hide tabs for system objects (#18583)
<img width="1286" height="793" alt="Screenshot 2026-03-12 at 13 57 16"
src="https://github.com/user-attachments/assets/bebfd23f-3172-424a-95ee-ba95358a6196"
/>
2026-03-12 14:46:15 +01:00
WeikoandGitHub 06d4d62e90 Move 1.19 backfill pagelayout and views to 1.20 (#18582) 2026-03-12 13:46:07 +01:00
WeikoandGitHub eb4665bc98 Create missing standard table and fields widget views (#18543) 2026-03-12 13:28:05 +01:00
f19fcd0010 i18n - translations (#18578)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-12 13:23:54 +01:00
Lucas BordeauandGitHub cb3e32df86 Fix AI demo workspace skill (#18575)
This PR fixes what allows to have a working demo workspace skill.

- Skill updated many times into something that works
- Fixed infinite loop in AI chat by memoizing ai-sdk output
- Finished navigateToView implementation
- Increased MAX_STEPS to 300 so the chat don't quit in the middle of a
long running skill
- Added CreateManyRelationFields
2026-03-12 13:19:01 +01:00
Hamza FaidiandGitHub db5b4d9c6c fix: replace unsafe JSON.parse casts with parseJson in filter dropdowns (#18513)
## Problem

Four filter dropdown components were calling `JSON.parse(filter.value)
as string[]` to parse stored filter state. This throws a `SyntaxError`
if the value is malformed (truncated URL, stale localStorage, migration
artifact), crashing the entire dropdown with no recovery.

## Solution

Replace with the existing `parseJson<string[]>` utility from
`twenty-shared`, which wraps `JSON.parse` in a try/catch and returns
`null` on failure. The `?? []` fallback gracefully degrades to an empty
selection instead of crashing.

All four files had an explicit `// TODO: replace by a safe parse`
marking this as a known issue.

## Testing


No new tests — `parseJson` is already tested in `twenty-shared`. No new
logic introduced.

## issue link 
#18514
2026-03-12 13:15:22 +01:00
Charles BochetandGitHub 660536d6bb Fix onboarding flow: workspace creation modal and invite team skip (#18577)
## Summary

- **Fix create-profile modal not showing after workspace creation**:
After activating a workspace, `CreateWorkspace.onSubmit` called
`refreshObjectMetadataItems()` which only updated the
`objectMetadataItemsState` atom but never marked the metadata store as
ready (`metadataStoreState` stayed at `'empty'`). Since `MetadataGater`
excludes `CreateWorkspace` but not `CreateProfile` from its loading
check, navigating to `/create/profile` triggered the skeleton loader
instead of the modal. The fix adds the full metadata pipeline after
refresh — `updateDraft('objectMetadataItems')` + `applyChanges()` for
objects, and `fetchAndLoadIndexViews()` for views — so
`isAppMetadataReady` is `true` before navigation.

- **Fix invite-team "Skip" not persisting to server**: Clicking "Skip"
on the invite-team page called `setNextOnboardingStatus()` which only
updated the local Jotai atom. The early return for empty emails bypassed
`sendInvitation`, so the server never cleared the
`ONBOARDING_INVITE_TEAM_PENDING` user var. On page refresh,
`GetCurrentUser` returned `INVITE_TEAM` and the user was stuck. The fix
removes the early return so `sendInvitation({ emails: [] })` always runs
— the server handles empty arrays fine and clears the pending flag.
2026-03-12 13:15:05 +01:00
e8f8189167 [COMMAND MENU ITEMS] Add engine component key (#18554)
## PR Description

In the process of migrating all the existing commands to the backend, we
stumbled across a couple of problems that made us reconsider the full
migration. This PR introduces a way for command menu items to bypass
front components and to directly reference a frontend component from
twenty front.

It:
- Introduces a `engineFrontComponentKey` field on `CommandMenuItem` as
an alternative to `frontComponentId` and `workflowVersionId`, allowing
command menu items to reference frontend components by key directly
rather than requiring a FrontComponent entity
- Updates the DB constraint to allow exactly one of `workflowVersionId`,
`frontComponentId`, or `engineFrontComponentKey`

### All standard command menu items from the frontend which use
`standardFrontComponentKey`

These are all commands that execute a GraphQL query or a mutation.
Two mains concerned have been raised that made us go with this
(temporary) architecture instead:
- If those commands are part of the standard application, they can only
alter objects from that application and not custom objects.
- We would need to implement a way to trigger optimistic rendering from
the front components, which might take some time to implement.

List:
- Create new record
- Delete (single record)
- Delete records (multiple)
- Restore record
- Restore records (multiple)
- Permanently destroy record
- Permanently destroy records (multiple)
- Add to favorites
- Remove from favorites
- Merge records
- Duplicate Dashboard
- Save Dashboard
- Save Page Layout
- Activate Workflow
- Deactivate Workflow
- Discard Draft (workflow)
- Test Workflow
- Tidy up workflow
- Duplicate Workflow
- Stop (workflow run)
- Use as draft (workflow version)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-12 13:14:45 +01:00
martmullandGitHub 78473a606a Fix app dev flickering (#18562)
- fix ticker issue
- fix too many rendering
2026-03-12 11:58:44 +01:00
neo773andGitHub b21fb4aa6f Fix PDF Upload edge case (#18533)
we were using an older version of `file-type` which has limited support
for PDF as it's a complex spec
Updated to latest version which includes support for plugins and added
`@file-type/pdf` which has extensive spec compliant detection approach

fixes TWENTY-SERVER-FAN
2026-03-12 10:34:24 +00:00
38664249cf i18n - translations (#18576)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-12 11:24:07 +01:00
Baptiste DevessierandGitHub 69542898a1 Display a single Add a Section button (#18563)
- Display a single Add a Section button at the end of the list
- Move other buttons to the section's dropdown menu


https://github.com/user-attachments/assets/b51d8846-635a-477a-9205-bf3266cfcff4
2026-03-12 10:01:48 +00:00
09beddb63d i18n - docs translations (#18566)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-12 10:55:44 +01:00
Hamza FaidiandGitHub 2eac82c207 fix(front): stabilize downloadFile unit test and return promise chain (#18484)
# Description

## What this PR fixes
This PR fixes a flaky/skipped unit test for
[downloadFile](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
and aligns the test with the actual implementation.

## Changes made
Updated
[downloadFile](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
test to validate file-saver behavior instead of DOM anchor creation.
Mocked
[saveAs](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
from file-saver and asserted it is called with the fetched blob and
filename.
Added proper async assertions for:
successful file download
failed fetch path ([status !==
200](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html))
rejecting with Failed downloading file
Updated
[downloadFile](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
implementation to return the fetch promise chain so callers/tests can
await it reliably.

## Related Issue
Closes #18485
2026-03-12 09:00:50 +01:00
f262437da6 Refactor dev environment setup with auto-detection and Docker support (#18564)
## Summary
Completely rewrites the development environment setup script to be more
robust, idempotent, and flexible. The new implementation auto-detects
available services (local PostgreSQL/Redis vs Docker), provides multiple
operational modes, and includes comprehensive health checks and error
handling.

## Key Changes

- **Enhanced setup script** (`packages/twenty-utils/setup-dev-env.sh`):
- Added auto-detection logic to prefer local services (PostgreSQL 16,
Redis) over Docker
  - Implemented service health checks with retry logic (30s timeout)
- Added command-line flags: `--docker` (force Docker), `--down` (stop
services), `--reset` (wipe data)
- Improved error handling with `set -euo pipefail` and descriptive
failure messages
- Added helper functions for service detection, startup, and status
checking
  - Fallback to manual `.env` file copying if Nx is unavailable
  - Enhanced output with clear status messages and usage instructions

- **New Docker Compose file**
(`packages/twenty-docker/docker-compose.dev.yml`):
  - Dedicated development infrastructure file (PostgreSQL 16 + Redis 7)
  - Includes health checks for both services
  - Configured with appropriate restart policies and volume management
  - Separate from production compose configuration

- **Updated documentation** (`CLAUDE.md`):
- Clarified that all environments (CI, local, Claude Code, Cursor) use
the same setup script
  - Documented new command-line flags and their purposes
- Noted that CI workflows manage services independently via GitHub
Actions

- **Updated Cursor environment config** (`.cursor/environment.json`):
- Simplified to use the new unified setup script instead of complex
inline commands

## Implementation Details

The script now follows a clear three-phase approach:
1. **Service startup** — Auto-detects and starts PostgreSQL and Redis
(local or Docker)
2. **Database creation** — Creates 'default' and 'test' databases
3. **Environment configuration** — Sets up `.env` files via Nx or direct
file copy

The auto-detection logic prioritizes local services for better
performance while gracefully falling back to Docker if local services
aren't available. All operations are idempotent and safe to run multiple
times.

https://claude.ai/code/session_01UDxa2Kp1ub9tTL3pnpBVFs

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-12 08:43:58 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
15d0970f72 Bump @swc/core from 1.15.11 to 1.15.18 (#18570)
Bumps
[@swc/core](https://github.com/swc-project/swc/tree/HEAD/packages/core)
from 1.15.11 to 1.15.18.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/swc-project/swc/blob/main/CHANGELOG.md"><code>@​swc/core</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>[1.15.18] - 2026-03-01</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>(html/wasm)</strong> Publish <code>@​swc/html-wasm</code>
for nodejs (<a
href="https://redirect.github.com/swc-project/swc/issues/11601">#11601</a>)
(<a
href="https://github.com/swc-project/swc/commit/bd443f582c553e9d898a1d5e7395abaad60b26d2">bd443f5</a>)</li>
</ul>
<h3>Documentation</h3>
<ul>
<li>
<p>Add AGENTS note about next-gen ast (<a
href="https://redirect.github.com/swc-project/swc/issues/11592">#11592</a>)
(<a
href="https://github.com/swc-project/swc/commit/80b4be872d85dc82cbb6e84c91fe102d807a2780">80b4be8</a>)</p>
</li>
<li>
<p>Add typescript-eslint AST compatibility note (<a
href="https://redirect.github.com/swc-project/swc/issues/11598">#11598</a>)
(<a
href="https://github.com/swc-project/swc/commit/c7bfebec4fb691e6e49f3c3b7b257be178e7f238">c7bfebe</a>)</p>
</li>
</ul>
<h3>Features</h3>
<ul>
<li>
<p><strong>(es/ast)</strong> Add runtime arena crate and bootstrap
swc_es_ast (<a
href="https://redirect.github.com/swc-project/swc/issues/11588">#11588</a>)
(<a
href="https://github.com/swc-project/swc/commit/7a06d967e43fe2f84078fc241bc655b41450d2c1">7a06d96</a>)</p>
</li>
<li>
<p><strong>(es/parser)</strong> Add <code>swc_es_parser</code> (<a
href="https://redirect.github.com/swc-project/swc/issues/11593">#11593</a>)
(<a
href="https://github.com/swc-project/swc/commit/f11fd705ee84909f6b0f984b1b5fc35abf73ec05">f11fd70</a>)</p>
</li>
</ul>
<h3>Ci</h3>
<ul>
<li>Triage main CI breakage (<a
href="https://redirect.github.com/swc-project/swc/issues/11589">#11589</a>)
(<a
href="https://github.com/swc-project/swc/commit/075af578c46c0bfdb74c450c157d0e1753024a36">075af57</a>)</li>
</ul>
<h2>[1.15.17] - 2026-02-26</h2>
<h3>Documentation</h3>
<ul>
<li>Add submodule update step before test runs (<a
href="https://redirect.github.com/swc-project/swc/issues/11576">#11576</a>)
(<a
href="https://github.com/swc-project/swc/commit/81b22c31d1acb447caae1a2d2bd530b2e6a40c26">81b22c3</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>
<p><strong>(bindings)</strong> Add html wasm binding and publish wiring
(<a
href="https://redirect.github.com/swc-project/swc/issues/11587">#11587</a>)
(<a
href="https://github.com/swc-project/swc/commit/b3869c3ae2a592d4539f4cbfbabeaf615e55d69e">b3869c3</a>)</p>
</li>
<li>
<p><strong>(sourcemap)</strong> Support safe scopes round-trip metadata
(<a
href="https://redirect.github.com/swc-project/swc/issues/11581">#11581</a>)
(<a
href="https://github.com/swc-project/swc/commit/de2a348daed80e47c75dabaf2f0ce945d850210a">de2a348</a>)</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/swc-project/swc/commit/7cb1be24a7857a94abd7f3cfe9709d22ac314379"><code>7cb1be2</code></a>
chore: Publish <code>1.15.18</code> with <code>swc_core</code>
<code>v58.0.1</code></li>
<li><a
href="https://github.com/swc-project/swc/commit/2821ed060a6f59a168ab8c60cde08ddc3e5cf0d5"><code>2821ed0</code></a>
chore: Publish <code>1.15.18-nightly-20260301.1</code> with
<code>swc_core</code> <code>v58.0.1</code></li>
<li><a
href="https://github.com/swc-project/swc/commit/08806dfffa4361414f1aad647bc1a9206ac29dbc"><code>08806df</code></a>
chore: Publish <code>1.15.17</code> with <code>swc_core</code>
<code>v58.0.1</code></li>
<li><a
href="https://github.com/swc-project/swc/commit/2fdd5ee6b17733583ba7cf5102534826d0d853bf"><code>2fdd5ee</code></a>
chore: Publish <code>1.15.17-nightly-20260226.1</code> with
<code>swc_core</code> <code>v58.0.1</code></li>
<li><a
href="https://github.com/swc-project/swc/commit/316e5035014020d6430262b4fc5e1b7cf4be9980"><code>316e503</code></a>
chore: Publish <code>1.15.16-nightly-20260226.1</code> with
<code>swc_core</code> <code>v58.0.1</code></li>
<li><a
href="https://github.com/swc-project/swc/commit/fb0ce2af0c6a88cd74bb3e55434f3081e7a5aa75"><code>fb0ce2a</code></a>
chore: Publish <code>1.15.15-nightly-20260226.1</code> with
<code>swc_core</code> <code>v58.0.1</code></li>
<li><a
href="https://github.com/swc-project/swc/commit/5f8fc7bac5be0e25a674455c581cea476aa2f6c7"><code>5f8fc7b</code></a>
chore: Publish <code>1.15.14-nightly-20260225.1</code> with
<code>swc_core</code> <code>v58.0.1</code></li>
<li><a
href="https://github.com/swc-project/swc/commit/2e0ea183f4a92735243a02829d8e02237aa94de3"><code>2e0ea18</code></a>
chore: Publish <code>1.15.13</code> with <code>swc_core</code>
<code>v57.0.1</code></li>
<li><a
href="https://github.com/swc-project/swc/commit/4e7a14c3a28ff667bb1aaac6e4aab83af626b173"><code>4e7a14c</code></a>
chore: Publish <code>1.15.13-nightly-20260223.1</code> with
<code>swc_core</code> <code>v57.0.1</code></li>
<li><a
href="https://github.com/swc-project/swc/commit/efccf48e991f21211377239ece3d7c1475eaae84"><code>efccf48</code></a>
chore: Publish <code>1.15.12-nightly-20260222.1</code> with
<code>swc_core</code> <code>v57.0.1</code></li>
<li>See full diff in <a
href="https://github.com/swc-project/swc/commits/v1.15.18/packages/core">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-12 07:16:45 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1adc325887 Bump path-to-regexp from 8.2.0 to 8.3.0 (#18571)
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from
8.2.0 to 8.3.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pillarjs/path-to-regexp/releases">path-to-regexp's
releases</a>.</em></p>
<blockquote>
<h2>8.3.0</h2>
<p><strong>Changed</strong></p>
<ul>
<li>Add custom error class (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/398">#398</a>)
2a7f2a4</li>
<li>Allow plain objects for <code>TokenData</code> (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/391">#391</a>)
687a9bb</li>
<li>Escape text should escape backslash (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/390">#390</a>)
a4a8552</li>
<li>Improved error messages and stack size (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/363">#363</a>)
a6bdf40</li>
</ul>
<p><strong>Other</strong></p>
<ul>
<li>Minifying the parser
<ul>
<li>PR (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/401">#401</a>)
9df2448</li>
<li>PR (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/395">#395</a>)
4a91505</li>
<li>Shaving some bytes  d63f44b</li>
<li>Remove optional operator  973d15c</li>
</ul>
</li>
</ul>
<p><a
href="https://github.com/pillarjs/path-to-regexp/compare/v8.2.0...v8.3.0">https://github.com/pillarjs/path-to-regexp/compare/v8.2.0...v8.3.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pillarjs/path-to-regexp/commit/c4f5b3fc10782a5de2bee55c3e40e5af890c9cad"><code>c4f5b3f</code></a>
8.3.0</li>
<li><a
href="https://github.com/pillarjs/path-to-regexp/commit/6587c812746cba94855867612f3a719bb25f794e"><code>6587c81</code></a>
Move parameter name errors up in docs (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/402">#402</a>)</li>
<li><a
href="https://github.com/pillarjs/path-to-regexp/commit/9df2448fdfca9d2957cf47a1777b5deda9be18cf"><code>9df2448</code></a>
Remove more bytes from parser (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/401">#401</a>)</li>
<li><a
href="https://github.com/pillarjs/path-to-regexp/commit/012a54a83c9e7fe77d1ee436c67048bca0512aca"><code>012a54a</code></a>
Bump actions/checkout from 4 to 5 (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/403">#403</a>)</li>
<li><a
href="https://github.com/pillarjs/path-to-regexp/commit/9385f7df7406b4607c3d18dfb276d5371f885418"><code>9385f7d</code></a>
Remove engines from package (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/399">#399</a>)</li>
<li><a
href="https://github.com/pillarjs/path-to-regexp/commit/2a7f2a4e9ba42eee41aa9d7a1a69eddb43b79a61"><code>2a7f2a4</code></a>
Add custom error class (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/398">#398</a>)</li>
<li><a
href="https://github.com/pillarjs/path-to-regexp/commit/265a2a7a26916a18fc6d1c5936c878a32a0fedb7"><code>265a2a7</code></a>
100% test coverage (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/396">#396</a>)</li>
<li><a
href="https://github.com/pillarjs/path-to-regexp/commit/4a915059a843dfdd122a0c4936837c7fdda2d4ee"><code>4a91505</code></a>
Reduce bytes in parse function (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/395">#395</a>)</li>
<li><a
href="https://github.com/pillarjs/path-to-regexp/commit/687a9bbc735245b2688c17db7e9fe86013ea0c77"><code>687a9bb</code></a>
Allow plain objects for <code>TokenData</code> (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/391">#391</a>)</li>
<li><a
href="https://github.com/pillarjs/path-to-regexp/commit/a4a8552c9fb4449c470fb9ead458df1c89cadb72"><code>a4a8552</code></a>
Escape text should escape backslash (<a
href="https://redirect.github.com/pillarjs/path-to-regexp/issues/390">#390</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/pillarjs/path-to-regexp/compare/v8.2.0...v8.3.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=path-to-regexp&package-manager=npm_and_yarn&previous-version=8.2.0&new-version=8.3.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-03-12 06:50:07 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5726bd6e17 Bump oxlint from 1.51.0 to 1.53.0 (#18569)
Bumps [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint)
from 1.51.0 to 1.53.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/oxc-project/oxc/releases">oxlint's
releases</a>.</em></p>
<blockquote>
<h2>oxlint v1.27.0 &amp;&amp; oxfmt v0.12.0</h2>
<h1>Oxlint v1.27.0</h1>
<h3>🚀 Features</h3>
<ul>
<li>222a8f0 linter/plugins: Implement
<code>SourceCode#isSpaceBetween</code> (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15498">#15498</a>)
(overlookmotel)</li>
<li>2f9735d linter/plugins: Implement
<code>context.languageOptions</code> (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15486">#15486</a>)
(overlookmotel)</li>
<li>bc731ff linter/plugins: Stub out all <code>Context</code> APIs (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15479">#15479</a>)
(overlookmotel)</li>
<li>5822cb4 linter/plugins: Add <code>extend</code> method to
<code>FILE_CONTEXT</code> (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15477">#15477</a>)
(overlookmotel)</li>
<li>7b1e6f3 apps: Add pure rust binaries and release to github (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15469">#15469</a>)
(Boshen)</li>
<li>2a89b43 linter: Introduce debug assertions after fixes to assert
validity (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15389">#15389</a>)
(camc314)</li>
<li>ad3c45a editor: Add <code>oxc.path.node</code> option (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15040">#15040</a>)
(Sysix)</li>
</ul>
<h3>🐛 Bug Fixes</h3>
<ul>
<li>6f3cd77 linter/no-var: Incorrect warning for blocks (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15504">#15504</a>)
(Hamir Mahal)</li>
<li>6957fb9 linter/plugins: Do not allow access to
<code>Context#id</code> in <code>createOnce</code> (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15489">#15489</a>)
(overlookmotel)</li>
<li>7409630 linter/plugins: Allow access to <code>cwd</code> in
<code>createOnce</code> in ESLint interop mode (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15488">#15488</a>)
(overlookmotel)</li>
<li>732205e parser: Reject <code>using</code> / <code>await using</code>
in a switch <code>case</code> / <code>default</code> clause (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15225">#15225</a>)
(sapphi-red)</li>
<li>a17ca32 linter/plugins: Replace <code>Context</code> class (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15448">#15448</a>)
(overlookmotel)</li>
<li>ecf2f7b language_server: Fail gracefully when tsgolint executable
not found (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15436">#15436</a>)
(camc314)</li>
<li>3c8d3a7 lang-server: Improve logging in failure case for tsgolint
(<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15299">#15299</a>)
(camc314)</li>
<li>ef71410 linter: Use jsx if source type is JS in fix debug assertion
(<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15434">#15434</a>)
(camc314)</li>
<li>e32bbf6 linter/no-var: Handle TypeScript declare keyword in fixer
(<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15426">#15426</a>)
(camc314)</li>
<li>6565dbe linter/switch-case-braces: Skip comments when searching for
<code>:</code> token (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15425">#15425</a>)
(camc314)</li>
<li>85bd19a linter/prefer-class-fields: Insert value after type
annotation in fixer (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15423">#15423</a>)
(camc314)</li>
<li>fde753e linter/plugins: Block access to
<code>context.settings</code> in <code>createOnce</code> (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15394">#15394</a>)
(overlookmotel)</li>
<li>ddd9f9f linter/forward-ref-uses-ref: Dont suggest removing wrapper
in invalid positions (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15388">#15388</a>)
(camc314)</li>
<li>dac2a9c linter/no-template-curly-in-string: Remove fixer (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15387">#15387</a>)
(camc314)</li>
<li>989b8e3 linter/no-var: Only fix to <code>const</code> if the var has
an initializer (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15385">#15385</a>)
(camc314)</li>
<li>cc403f5 linter/plugins: Return empty object for unimplemented
parserServices (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15364">#15364</a>)
(magic-akari)</li>
</ul>
<h3> Performance</h3>
<ul>
<li>25d577e language_server: Start tools in parallel (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15500">#15500</a>)
(Sysix)</li>
<li>3c57291 linter/plugins: Optimize loops (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15449">#15449</a>)
(overlookmotel)</li>
<li>3166233 linter/plugins: Remove <code>Arc</code>s (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15431">#15431</a>)
(overlookmotel)</li>
<li>9de1322 linter/plugins: Lazily deserialize settings JSON (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15395">#15395</a>)
(overlookmotel)</li>
<li>3049ec2 linter/plugins: Optimize <code>deepFreezeSettings</code> (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15392">#15392</a>)
(overlookmotel)</li>
<li>444ebfd linter/plugins: Use single object for
<code>parserServices</code> (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15378">#15378</a>)
(overlookmotel)</li>
</ul>
<h3>📚 Documentation</h3>
<ul>
<li>97d2104 linter: Update comment in lint.rs about default value for
tsconfig path (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15530">#15530</a>)
(Connor Shea)</li>
<li>2c6bd9e linter: Always refer as &quot;ES2015&quot; instead of
&quot;ES6&quot; (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15411">#15411</a>)
(sapphi-red)</li>
<li>a0c5203 linter/import/named: Update &quot;ES7&quot; comment in
examples (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15410">#15410</a>)
(sapphi-red)</li>
<li>3dc24b5 linter,minifier: Always refer as &quot;ES Modules&quot;
instead of &quot;ES6 Modules&quot; (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15409">#15409</a>)
(sapphi-red)</li>
<li>2ad77fb linter/no-this-before-super: Correct &quot;Why is this
bad?&quot; section (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15408">#15408</a>)
(sapphi-red)</li>
<li>57f0ce1 linter: Add backquotes where appropriate (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/15407">#15407</a>)
(sapphi-red)</li>
</ul>
<h1>Oxfmt v0.12.0</h1>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md">oxlint's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<p>All notable changes to this package will be documented in this
file.</p>
<p>The format is based on <a
href="https://keepachangelog.com/en/1.0.0">Keep a Changelog</a>.</p>
<h2>[1.52.0] - 2026-03-09</h2>
<h3>🚀 Features</h3>
<ul>
<li>61bf388 linter: Add
<code>options.reportUnusedDisableDirectives</code> to config file (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/19799">#19799</a>)
(Peter Wagenet)</li>
<li>2919313 linter: Introduce denyWarnings config options (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/19926">#19926</a>)
(camc314)</li>
<li>a607119 linter: Introduce maxWarnings config option (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/19777">#19777</a>)
(camc314)</li>
</ul>
<h3>📚 Documentation</h3>
<ul>
<li>6c0e0b5 linter: Add oxlint.config.ts to the config docs. (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/19941">#19941</a>)
(connorshea)</li>
<li>160e423 linter: Add a note that the typeAware and typeCheck options
require oxlint-tsgolint (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/19940">#19940</a>)
(connorshea)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/oxc-project/oxc/commit/856781f99c7eb521b6221fab0047cfd09343df50"><code>856781f</code></a>
release(apps): oxlint v1.53.0 &amp;&amp; oxfmt v0.38.0 (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/20218">#20218</a>)</li>
<li><a
href="https://github.com/oxc-project/oxc/commit/9870467e0025f0cca44b24cda5ccaa9414b51a56"><code>9870467</code></a>
release(apps): oxlint v1.52.0 &amp;&amp; oxfmt v0.37.0 (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/20143">#20143</a>)</li>
<li><a
href="https://github.com/oxc-project/oxc/commit/61bf3883fd8435e061a055e528db6f664e737132"><code>61bf388</code></a>
feat(linter): add <code>options.reportUnusedDisableDirectives</code> to
config file (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/19">#19</a>...</li>
<li><a
href="https://github.com/oxc-project/oxc/commit/6c0e0b5721cd791f82e36bc7376f7417518c0548"><code>6c0e0b5</code></a>
docs(linter): Add oxlint.config.ts to the config docs. (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/19941">#19941</a>)</li>
<li><a
href="https://github.com/oxc-project/oxc/commit/160e423dce9900f7f7c6bce7f6845229d5732f8b"><code>160e423</code></a>
docs(linter): Add a note that the typeAware and typeCheck options
require oxl...</li>
<li><a
href="https://github.com/oxc-project/oxc/commit/2919313574b988ae9c711c9808fee57bea4d326f"><code>2919313</code></a>
feat(linter): introduce denyWarnings config options (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/19926">#19926</a>)</li>
<li><a
href="https://github.com/oxc-project/oxc/commit/a60711957ea4244885134972bbaac8810f42451c"><code>a607119</code></a>
feat(linter): introduce maxWarnings config option (<a
href="https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint/issues/19777">#19777</a>)</li>
<li>See full diff in <a
href="https://github.com/oxc-project/oxc/commits/oxlint_v1.53.0/npm/oxlint">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=oxlint&package-manager=npm_and_yarn&previous-version=1.51.0&new-version=1.53.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-03-12 06:38:30 +00:00
Abdul RahmanandGitHub 102b49f919 Fix new navbar item position after reordering existing items (#18516)
Closes [#2296](https://github.com/twentyhq/core-team-issues/issues/2296)
2026-03-11 20:35:15 +00:00
Thomas TrompetteandGitHub 2af3121c51 Fix dashboard creation + role permission page design (#18565)
1. **Creating a new dashboard crashes with "Tab not found"** and widgets
can't be added after the crash is prevented.

**Root cause:** `initializePageLayout` wrapped both the persisted and
draft state updates behind an `isDeeplyEqual` guard. After navigation,
`resetPageLayoutEditMode` resets the draft atom to its default but
leaves the persisted atom untouched. On re-initialization,
`isDeeplyEqual` returns true (persisted unchanged), so the draft is
never repopulated. But edit mode is still activated.

**Fix**: Move the draft store.set outside the isDeeplyEqual guard so
it's always set on initialization. Also add a defensive check in
`PageLayoutRendererContent` to prevent the crash when activeTabId
doesn't match available tabs.


https://github.com/user-attachments/assets/bcd69866-63eb-4e5e-a1bb-655e71ba6dc5

2. **Permission role page design broken**
Before
<img width="573" height="1130" alt="role-page-broken"
src="https://github.com/user-attachments/assets/09f60fd2-ef08-4133-bb28-034b15579481"
/>

After
<img width="573" height="266" alt="Capture d’écran 2026-03-11 à 14 25
55"
src="https://github.com/user-attachments/assets/c34f9993-51e1-4108-a7e6-f434f558edfd"
/>
2026-03-11 17:57:54 +00:00
Thomas TrompetteandGitHub a024a04e01 Fix breadcrumb infinite loop (#18561)
`RecordTableNoRecordGroupScrollToPreviousRecordEffect` uses
`useAtomState(lastShowPageRecordIdState)` to read the atom value and
check whether to trigger an effect. Inside `run()`, it calls
`setLastShowPageRecordId(null)` to reset the atom, then`
triggerInitialRecordTableDataLoad()` which fires many `store.set()`
calls on other atoms.

These high-frequency store updates cause the component to re-render
before Jotai's internal useReducer dispatch (propagating the null value)
is processed by React. The result: useAtomState returns a stale non-null
value on every subsequent render, even though the Jotai store already
holds null. The effect re-runs, sees the stale non-null value, calls
`run()` again, creating an infinite loop.

This is a Jotai v2 edge case where useAtom's rendered value desyncs from
the actual store value under high-frequency concurrent updates.

### The fix

Read lastShowPageRecordId directly from the Jotai store via
`store.get()` inside the effect instead of relying on the rendered value
from useAtomState. This guarantees the effect always sees the true store
value and correctly skips when the atom is null.
2026-03-11 18:26:02 +01:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
f0c83434a7 feat: default code interpreter and logic function to Disabled in production (#18559)
## Summary

For security reasons, the code interpreter and logic function drivers
now default based on `NODE_ENV`:

- **Production** (`NODE_ENV=production` or unset): Default to
**Disabled**
- **Development** (`NODE_ENV=development`): Default to **LOCAL** for
convenience

This ensures self-hosted production deployments don't accidentally run
user-provided code without explicit configuration.

## Changes

### Config (`config-variables.ts`)
- `CODE_INTERPRETER_TYPE`: Disabled in prod, LOCAL in dev
- `LOGIC_FUNCTION_TYPE`: Disabled in prod, LOCAL in dev

### Documentation (`setup.mdx`)
- Added **Security Defaults** section explaining NODE_ENV-based behavior
- Fixed variable names: `SERVERLESS_TYPE` → `LOGIC_FUNCTION_TYPE`,
`SERVERLESS_LAMBDA_*` → `LOGIC_FUNCTION_LAMBDA_*`
- Added **Code Interpreter** section with available drivers (Disabled,
Local, E2B)

### Environment files
- `.env.example`: Updated to `LOGIC_FUNCTION_TYPE` with comments
- `.env.test`: Added `LOGIC_FUNCTION_TYPE=LOCAL` for logic function
integration tests

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-03-11 17:54:55 +01:00
Paul RastoinandGitHub b699619756 Create twenty app e2e test ci (#18497)
# Introduction
Verifies whole following flow:
- Create and sdk app build and publication
- Global create-twenty-app installation
- Creating an app
- installing app dependencies
- auth:login
- app:build
- function:execute
- Running successfully auto-generated integration tests

## Create twenty app options refactor
Allow having a flow that do not require any prompt
2026-03-11 16:30:28 +01:00
Raphaël BosiandGitHub b2f053490d Add standard front component ci (#18560)
Checks if the build has been generated correctly before merging
2026-03-11 16:06:13 +01:00
21de221420 i18n - translations (#18557)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-11 15:57:19 +01:00
martmullandGitHub d9b3507866 Run vulnerable operation in isolated environment (#18523)
When driver = LAMBDA:
- run esbuild ts transpilation on dedicated lambda
- run yarn install on app dependencies on a dedicated lambda
2026-03-11 14:08:47 +00:00
Thomas TrompetteandGitHub b346f4fb59 Add common loader (#18556)
To avoid white screens on reload, building a shared skeleton.

Before

https://github.com/user-attachments/assets/42bd0667-141d-4df4-9072-4077192cc71d

After

https://github.com/user-attachments/assets/e6031a72-2e25-47e3-a873-b89aaddfbd3a
2026-03-11 14:56:49 +01:00
nitinandGitHub 2c5af2654d Separate code pathways for IS_COMMAND_MENU_ITEM_ENABLED flag (#18542) 2026-03-11 13:30:22 +00:00
WeikoandGitHub 6cbc7725b7 fix standard app for server build (#18558) 2026-03-11 14:37:18 +01:00
744ef3aa9d i18n - translations (#18555)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-11 14:21:20 +01:00
WeikoandGitHub ef92d2d321 Backfill standard views command (#18540) 2026-03-11 13:48:59 +01:00
WeikoandGitHub 00c3cd1051 Add system view fallback (#18536)
## Context
The goal is to add a "See records" button in all objects that would
redirect to that view (this will be done in a later PR). See screenshot
below.
<img width="665" height="312" alt="Screenshot 2026-03-10 at 15 54 36"
src="https://github.com/user-attachments/assets/6e23a75b-cff0-4d93-bce8-b5481b05c6f6"
/>

## Implementation
- If a view does not exist on an object, there is a **temporary**
fallback where the frontend creates the missing view as a custom view
when going over the object index page
- System objects are now surfaced but we don't want their records to be
editable, they will be readonly (mostly, all fields will be non-editable
except for their custom fields).
- We can't create a new record of a system object, some actions are also
hidden.
- The backend now rejects if you are trying to delete the last view of
an object
2026-03-11 13:48:02 +01:00
99f885306e i18n - translations (#18553)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-11 13:44:13 +01:00
413d1124bb Fix navigation drag drop indicator position (#18515)
Closes [#2295](https://github.com/twentyhq/core-team-issues/issues/2295)

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
2026-03-11 13:35:09 +01:00
ab5fb1f658 Replace newFieldDefaultConfiguration with newFieldDefaultVisibility (#18539)
https://github.com/user-attachments/assets/365092cb-0fe1-44f7-9ae6-c6fc5edb98b2

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-03-11 12:14:30 +00:00
Abdul RahmanandGitHub e4e7137660 Navigate to page when clicking nav item in edit mode (#18526)
Closes [#2298](https://github.com/twentyhq/core-team-issues/issues/2298)
2026-03-11 10:59:22 +00:00
Félix MalfaitandGitHub 7fb8cc1c39 Improve apps settings UI and remove unused tarball upload code (#18549)
## Summary
- Improves the Developer Tab in Settings > Applications: adds
source-type badges (Dev / Npm / Internal), better empty state with `yarn
twenty app:dev` CLI command, renames sections to "Create & Develop" and
"Your Apps"
- Simplifies the Distribution Tab: only shows marketplace section for
npm-sourced apps, removes the manual `isListed` toggle (now managed
automatically by the catalog sync cron)
- Removes dead frontend code: `installApplication` mutation (unused —
installs go through `installMarketplaceApp`), `uploadAppTarball`
mutation/hook, and `SettingsUploadTarballModal`

## Test plan
- [ ] Navigate to Settings > Applications > Developer tab: verify badges
show correctly, empty state shows CLI command
- [ ] Create/view a LOCAL app registration: verify Distribution tab does
NOT show marketplace section
- [ ] Create/view an NPM app registration: verify Distribution tab shows
marketplace section with Featured toggle
- [ ] Verify no references to upload tarball or install application
remain in the UI


Made with [Cursor](https://cursor.com)
2026-03-11 11:40:00 +01:00
2916 changed files with 86773 additions and 68908 deletions
-18
View File
@@ -1,18 +0,0 @@
{
"install": "curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs && node --version && yarn install && echo 'Setting up Docker Compose environment...' && cd packages/twenty-docker && cp -n docker-compose.yml docker-compose.dev.yml || true && echo 'Dependencies installed and docker-compose prepared'",
"start": "sudo service docker start && echo 'Docker service started' && cd packages/twenty-docker && echo 'Installing yq for YAML processing...' && sudo apt-get update -qq && sudo apt-get install -y wget && wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq && echo 'Patching docker-compose for local development...' && yq eval 'del(.services.server.image)' -i docker-compose.dev.yml && yq eval '.services.server.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.server.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && yq eval 'del(.services.worker.image)' -i docker-compose.dev.yml && yq eval '.services.worker.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.worker.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && echo 'Setting up .env file with database configuration...' && echo 'SERVER_URL=http://localhost:3000' > .env && echo 'APP_SECRET='$(openssl rand -base64 32) >> .env && echo 'PG_DATABASE_PASSWORD='$(openssl rand -hex 16) >> .env && echo 'PG_DATABASE_URL=postgres://postgres:password@localhost:5432/postgres' >> .env && echo 'SIGN_IN_PREFILLED=true' >> .env && echo 'Building and starting services...' && docker-compose -f docker-compose.dev.yml up -d --build && echo 'Waiting for services to initialize...' && sleep 30 && echo 'Checking service health...' && docker-compose -f docker-compose.dev.yml ps && echo 'Environment setup complete!'",
"terminals": [
{
"name": "Database Setup & Seed",
"command": "sleep 40 && cd packages/twenty-docker && echo 'Waiting for PostgreSQL to be ready...' && until docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres; do echo 'Waiting for PostgreSQL...'; sleep 5; done && echo 'PostgreSQL is ready!' && echo 'Waiting for Twenty server to be healthy...' && until docker-compose -f docker-compose.dev.yml exec -T server curl --fail http://localhost:3000/healthz 2>/dev/null; do echo 'Waiting for server...'; sleep 5; done && echo 'Server is healthy!' && echo 'Running database setup and seeding...' && docker-compose -f docker-compose.dev.yml exec -T server npx nx database:reset twenty-server && echo 'Database seeded successfully!' && bash"
},
{
"name": "Application Logs",
"command": "sleep 35 && cd packages/twenty-docker && echo 'Following application logs...' && docker-compose -f docker-compose.dev.yml logs -f server worker"
},
{
"name": "Service Monitor",
"command": "sleep 15 && cd packages/twenty-docker && echo '=== Service Status Monitor ===' && while true; do clear; echo '=== Service Status at $(date) ===' && docker-compose -f docker-compose.dev.yml ps && echo '\\n=== Health Status ===' && (docker-compose -f docker-compose.dev.yml exec -T server curl -s http://localhost:3000/healthz 2>/dev/null && echo '✅ Twenty Server: Healthy') || echo '❌ Twenty Server: Not Ready' && (docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres 2>/dev/null && echo '✅ PostgreSQL: Ready') || echo '❌ PostgreSQL: Not Ready' && echo '\\n=== Database Connection Test ===' && docker-compose -f docker-compose.dev.yml exec -T server node -e \"const { Client } = require('pg'); const client = new Client({connectionString: process.env.PG_DATABASE_URL}); client.connect().then(() => {console.log('✅ Database Connection: OK'); client.end();}).catch(e => console.log('❌ Database Connection: Failed -', e.message));\" || echo 'Connection test failed' && sleep 45; done"
}
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"install": "yarn install",
"start": "sudo service docker start && sleep 2 && (docker start twenty_pg 2>/dev/null || make -C packages/twenty-docker postgres-on-docker) && (docker start twenty_redis 2>/dev/null || make -C packages/twenty-docker redis-on-docker) && until docker exec twenty_pg pg_isready -U postgres -h localhost 2>/dev/null; do sleep 1; done && echo 'PostgreSQL ready' && until docker exec twenty_redis redis-cli ping 2>/dev/null | grep -q PONG; do sleep 1; done && echo 'Redis ready' && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
"start": "(sudo service docker start || service docker start || true) && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
"terminals": [
{
"name": "Development Server",
+19
View File
@@ -0,0 +1,19 @@
storage: /tmp/verdaccio-storage
auth:
htpasswd:
file: /tmp/verdaccio-htpasswd
max_users: 100
uplinks:
npmjs:
url: https://registry.npmjs.org/
packages:
'twenty-sdk':
access: $all
publish: $all
'create-twenty-app':
access: $all
publish: $all
'**':
access: $all
proxy: npmjs
log: { type: stdout, format: pretty, level: warn }
+184
View File
@@ -0,0 +1,184 @@
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-shared/**
packages/twenty-server/**
!packages/create-twenty-app/package.json
!packages/twenty-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
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 twenty-sdk create-twenty-app --releaseVersion=$CI_VERSION
- name: Build packages
run: |
npx nx build twenty-sdk
npx nx build create-twenty-app
- 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: |
npm set //localhost:4873/:_authToken "ci-auth-token"
for pkg in twenty-sdk create-twenty-app; do
cd packages/$pkg
npm publish --registry http://localhost:4873 --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: Build scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty build
test -d .twenty/output
- 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: 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
+3 -3
View File
@@ -40,7 +40,8 @@ jobs:
uses: actions/checkout@v4
with:
token: ${{ github.token }}
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Install dependencies
uses: ./.github/actions/yarn-install
@@ -111,7 +112,7 @@ jobs:
run: yarn docs:generate-paths
- name: Commit artifacts to pull request branch
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
run: |
git add packages/twenty-docs/docs.json packages/twenty-docs/navigation/navigation.template.json packages/twenty-shared/src/constants/DocumentationPaths.ts
if git diff --staged --quiet --exit-code; then
@@ -149,4 +150,3 @@ jobs:
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+13 -4
View File
@@ -188,13 +188,22 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
- Descriptive test names: "should [behavior] when [condition]"
- Clear mocks between tests with `jest.clearAllMocks()`
## CI Environment (GitHub Actions)
## Dev Environment Setup
When running in CI, the dev environment is **not** pre-configured. Dependencies are installed but builds, env files, and databases are not set up.
All dev environments (Claude Code web, Cursor, local) use one script:
- **Before running tests, builds, lint, type checks, or DB operations**, run: `bash packages/twenty-utils/setup-dev-env.sh`
```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.
- The script is idempotent and safe to run multiple times.
**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
+3 -3
View File
@@ -25,8 +25,8 @@
# Installation
See:
🚀 [Self-hosting](https://docs.twenty.com/developers/self-hosting/docker-compose)
🖥️ [Local Setup](https://docs.twenty.com/developers/local-setup)
🚀 [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
@@ -36,7 +36,7 @@ We built Twenty for three reasons:
**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.
**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.
<br />
+8
View File
@@ -136,6 +136,14 @@
"cache": true,
"dependsOn": ["^build"]
},
"set-local-version": {
"executor": "nx:run-commands",
"cache": false,
"options": {
"cwd": "{projectRoot}",
"command": "npm pkg set version={args.releaseVersion}"
}
},
"storybook:build": {
"executor": "nx:run-commands",
"cache": true,
+2 -2
View File
@@ -1,7 +1,7 @@
{
"private": true,
"dependencies": {
"@apollo/client": "^3.7.17",
"@apollo/client": "^4.0.0",
"@floating-ui/react": "^0.24.3",
"@linaria/core": "^6.2.0",
"@linaria/react": "^6.2.1",
@@ -164,6 +164,7 @@
"tsc-alias": "^1.8.16",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0",
"verdaccio": "^6.3.1",
"vite": "^7.0.0",
"vitest": "^4.0.18"
},
@@ -206,7 +207,6 @@
"packages/twenty-e2e-testing",
"packages/twenty-shared",
"packages/twenty-sdk",
"packages/twenty-standard-application",
"packages/twenty-apps",
"packages/twenty-cli",
"packages/create-twenty-app",
+43 -26
View File
@@ -36,30 +36,36 @@ cd my-twenty-app
# Get help and list all available commands
yarn twenty help
# Authenticate using your API key (you'll be prompted)
yarn twenty auth:login
# Authenticate with your Twenty server
yarn twenty remote add --local
# Add a new entity to your application (guided)
yarn twenty entity:add
yarn twenty add
# Start dev mode: watches, builds, and syncs local changes to your workspace
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built with the SDK — both available via `twenty-sdk/clients`)
yarn twenty app:dev
yarn twenty dev
# Watch your application's function logs
yarn twenty function:logs
yarn twenty logs
# Execute a function with a JSON payload
yarn twenty function:execute -n my-function -p '{"key": "value"}'
yarn twenty exec -n my-function -p '{"key": "value"}'
# Execute the pre-install function
yarn twenty function:execute --preInstall
yarn twenty exec --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
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 app:uninstall
yarn twenty uninstall
```
## Scaffolding modes
@@ -104,40 +110,51 @@ npx create-twenty-app@latest my-app -m
## Next steps
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty app:dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace.
- 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` (for workspace data via `/graphql`) 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, MetadataApiClient } from 'twenty-sdk/clients'`.
## Publish your application
## Build and publish your application
Applications are currently stored in `twenty/packages/twenty-apps`.
Once your app is ready, build and publish it using the CLI:
You can share your application with all Twenty users:
```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
# pull the Twenty project
git clone https://github.com/twentyhq/twenty.git
cd twenty
# create a new branch
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
```bash
git commit -m "Add new application"
git push
```
Our team reviews contributions for quality, security, and reusability before merging.
## Troubleshooting
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
- Types not generated: ensure `yarn twenty app:dev` is running — it autogenerates the typed client.
- Auth prompts not appearing: run `yarn twenty remote add --local` again and verify the API key permissions.
- Types not generated: ensure `yarn twenty dev` is running — it autogenerates the typed client.
## Contributing
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.7.0-canary.0",
"version": "0.8.0-canary.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
+1
View File
@@ -24,6 +24,7 @@
"command": "node dist/cli.cjs"
}
},
"set-local-version": {},
"typecheck": {},
"lint": {},
"test": {
+30 -1
View File
@@ -18,6 +18,19 @@ const program = new Command(packageJson.name)
'-m, --minimal',
'Create only core entities (application-config and default-role)',
)
.option('-n, --name <name>', 'Application name (skips prompt)')
.option(
'-d, --display-name <displayName>',
'Application display name (skips prompt)',
)
.option(
'--description <description>',
'Application description (skips prompt)',
)
.option(
'--skip-local-instance',
'Skip the local Twenty instance setup prompt',
)
.helpOption('-h, --help', 'Display this help message.')
.action(
async (
@@ -25,6 +38,10 @@ const program = new Command(packageJson.name)
options?: {
exhaustive?: boolean;
minimal?: boolean;
name?: string;
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
},
) => {
const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean);
@@ -47,9 +64,21 @@ const program = new Command(packageJson.name)
process.exit(1);
}
if (options?.name !== undefined && options.name.trim().length === 0) {
console.error(chalk.red('Error: --name cannot be empty.'));
process.exit(1);
}
const mode: ScaffoldingMode = options?.minimal ? 'minimal' : 'exhaustive';
await new CreateAppCommand().execute(directory, mode);
await new CreateAppCommand().execute({
directory,
mode,
name: options?.name,
displayName: options?.displayName,
description: options?.description,
skipLocalInstance: options?.skipLocalInstance,
});
},
);
@@ -11,3 +11,4 @@
- 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.
@@ -2,16 +2,10 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
## Getting Started
First, authenticate to your workspace:
Start development mode — it auto-connects to your local Twenty server at localhost:3000:
```bash
yarn twenty auth:login
```
Then, start development mode to sync your app and watch for changes:
```bash
yarn twenty app:dev
yarn twenty dev
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
@@ -21,19 +15,23 @@ Open your Twenty instance and go to `/settings/applications` section to see the
Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Authentication
yarn twenty auth:login # Authenticate with Twenty
yarn twenty auth:logout # Remove credentials
yarn twenty auth:status # Check auth status
yarn twenty auth:switch # Switch default workspace
yarn twenty auth:list # List all configured workspaces
# Remotes & authentication
yarn twenty remote add --local # Connect to local Twenty server
yarn twenty remote add <url> # Connect to a remote server (OAuth)
yarn twenty remote list # List all configured remotes
yarn twenty remote switch # Switch default remote
yarn twenty remote status # Check auth status
yarn twenty remote remove <name> # Remove a remote
# Application
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty function:logs # Stream function logs
yarn twenty function:execute # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
# Development
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty build # Build the application
yarn twenty deploy # Deploy to a Twenty server
yarn twenty publish # Publish to npm
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty exec # Execute a function with JSON payload
yarn twenty logs # Stream function logs
yarn twenty uninstall # Uninstall app from server
```
## Integration Tests
@@ -1,12 +1,18 @@
import { copyBaseApplicationProject } from '@/utils/app-template';
import { convertToLabel } from '@/utils/convert-to-label';
import { install } from '@/utils/install';
import {
type LocalInstanceResult,
setupLocalInstance,
} from '@/utils/setup-local-instance';
import { tryGitInit } from '@/utils/try-git-init';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import kebabCase from 'lodash.kebabcase';
import { execSync } from 'node:child_process';
import * as path from 'path';
import { isDefined } from 'twenty-shared/utils';
import {
type ExampleOptions,
@@ -15,16 +21,24 @@ import {
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
type CreateAppOptions = {
directory?: string;
mode?: ScaffoldingMode;
name?: string;
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
};
export class CreateAppCommand {
async execute(
directory?: string,
mode: ScaffoldingMode = 'exhaustive',
): Promise<void> {
async execute(options: CreateAppOptions = {}): Promise<void> {
try {
const { appName, appDisplayName, appDirectory, appDescription } =
await this.getAppInfos(directory);
await this.getAppInfos(options);
const exampleOptions = this.resolveExampleOptions(mode);
const exampleOptions = this.resolveExampleOptions(
options.mode ?? 'exhaustive',
);
await this.validateDirectory(appDirectory);
@@ -44,7 +58,29 @@ export class CreateAppCommand {
await tryGitInit(appDirectory);
this.logSuccess(appDirectory);
let localResult: LocalInstanceResult = { running: false };
if (!options.skipLocalInstance) {
const { needsLocalInstance } = await inquirer.prompt([
{
type: 'confirm',
name: 'needsLocalInstance',
message:
'Do you need a local instance of Twenty? Recommended if you not have one already.',
default: true,
},
]);
if (needsLocalInstance) {
localResult = await setupLocalInstance();
}
if (isDefined(localResult.apiKey)) {
this.runAuthLogin(appDirectory, localResult.apiKey);
}
}
this.logSuccess(appDirectory, localResult);
} catch (error) {
console.error(
chalk.red('Initialization failed:'),
@@ -54,19 +90,25 @@ export class CreateAppCommand {
}
}
private async getAppInfos(directory?: string): Promise<{
private async getAppInfos(options: CreateAppOptions): Promise<{
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
}> {
const { directory } = options;
const hasName = isDefined(options.name) || isDefined(directory);
const hasDisplayName = isDefined(options.displayName);
const hasDescription = isDefined(options.description);
const { name, displayName, description } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Application name:',
when: () => !directory,
default: 'my-awesome-app',
when: () => !hasName,
default: 'my-twenty-app',
validate: (input) => {
if (input.length === 0) return 'Application name is required';
return true;
@@ -76,25 +118,33 @@ export class CreateAppCommand {
type: 'input',
name: 'displayName',
message: 'Application display name:',
default: (answers: any) => {
return convertToLabel(answers?.name ?? directory);
when: () => !hasDisplayName,
default: (answers: { name?: string }) => {
return convertToLabel(
answers?.name ?? options.name ?? directory ?? '',
);
},
},
{
type: 'input',
name: 'description',
message: 'Application description (optional):',
when: () => !hasDescription,
default: '',
},
]);
const computedName = name ?? directory;
const appName = (
options.name ??
name ??
directory ??
'my-twenty-app'
).trim();
const appName = computedName.trim();
const appDisplayName =
(options.displayName ?? displayName)?.trim() || convertToLabel(appName);
const appDisplayName = displayName.trim();
const appDescription = description.trim();
const appDescription = (options.description ?? description ?? '').trim();
const appDirectory = directory
? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
@@ -157,16 +207,49 @@ export class CreateAppCommand {
console.log('');
}
private logSuccess(appDirectory: string): void {
private runAuthLogin(appDirectory: string, apiKey: string): void {
try {
execSync(
`yarn twenty auth:login --api-key "${apiKey}" --api-url http://localhost:3000`,
{ cwd: appDirectory, stdio: 'inherit' },
);
console.log(chalk.green('✅ Authenticated with local Twenty instance.'));
} catch {
console.log(
chalk.yellow(
'⚠️ Auto auth:login failed. Run `yarn twenty auth:login` manually.',
),
);
}
}
private logSuccess(
appDirectory: string,
localResult: LocalInstanceResult,
): void {
const dirName = appDirectory.split('/').reverse()[0] ?? '';
console.log(chalk.green('✅ Application created!'));
console.log('');
console.log(chalk.blue('Next steps:'));
console.log(chalk.gray(` cd ${dirName}`));
console.log(
chalk.gray(' yarn twenty auth:login # Authenticate with Twenty'),
);
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
if (localResult.apiKey) {
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
} else if (localResult.running) {
console.log(
chalk.gray(
' yarn twenty remote add --local # Authenticate with Twenty',
),
);
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
} else {
console.log(
chalk.gray(
' yarn twenty remote add --local # Authenticate with Twenty',
),
);
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
}
}
}
@@ -103,7 +103,6 @@ describe('scaffoldIntegrationTest', () => {
expect(content).toContain('TWENTY_API_KEY');
expect(content).not.toContain('TWENTY_TEST_API_KEY');
expect(content).toContain('TWENTY_API_URL');
expect(content).toContain('setup-test.ts');
expect(content).toContain('tsconfig.spec.json');
expect(content).toContain('integration-test.ts');
@@ -30,12 +30,12 @@ export const copyBaseApplicationProject = async ({
includeExampleIntegrationTest: exampleOptions.includeExampleIntegrationTest,
});
await createYarnLock(appDirectory);
await createGitignore(appDirectory);
await createPublicAssetDirectory(appDirectory);
await createYarnLock(appDirectory);
const sourceFolderPath = join(appDirectory, SRC_FOLDER);
await fs.ensureDir(sourceFolderPath);
@@ -142,13 +142,6 @@ const createPublicAssetDirectory = async (appDirectory: string) => {
await fs.ensureDir(join(appDirectory, ASSETS_DIR));
};
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 createGitignore = async (appDirectory: string) => {
const gitignoreContent = `# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
@@ -481,7 +474,7 @@ const createExampleNavigationMenuItem = async ({
const universalIdentifier = v4();
const content = `import { defineNavigationMenuItem } from 'twenty-sdk';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '${universalIdentifier}',
@@ -489,6 +482,7 @@ export default defineNavigationMenuItem({
icon: 'IconList',
color: 'blue',
position: 0,
type: 'VIEW',
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
`;
@@ -590,6 +584,14 @@ export default defineApplication({
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 ({
appName,
appDirectory,
@@ -608,8 +610,9 @@ const createPackageJson = async ({
const devDependencies: Record<string, string> = {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^18.2.0',
react: '^18.2.0',
'@types/react': '^19.0.0',
react: '^19.0.0',
'react-dom': '^19.0.0',
oxlint: '^0.16.0',
'twenty-sdk': createTwentyAppPackageJson.version,
};
@@ -0,0 +1,194 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { execSync } from 'node:child_process';
import { isDefined } from 'twenty-shared/utils';
const INSTALL_SCRIPT_URL =
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh';
const SERVER_CONTAINER = 'twenty-server-1';
const DB_CONTAINER = 'twenty-db-1';
const isDockerAvailable = (): boolean => {
try {
execSync('docker compose version', { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
const isDockerRunning = (): boolean => {
try {
execSync('docker info', { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
const isTwentyServerRunning = async (): Promise<boolean> => {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const response = await fetch('http://localhost:3000/healthz', {
signal: controller.signal,
});
clearTimeout(timeoutId);
const body = await response.json();
return body.status === 'ok';
} catch {
return false;
}
};
const getActiveWorkspaceId = (): string | null => {
try {
const result = execSync(
`docker exec ${DB_CONTAINER} psql -U postgres -d default -t -c "SELECT id FROM core.workspace WHERE \\"activationStatus\\" = 'ACTIVE' LIMIT 1"`,
{ encoding: 'utf-8' },
).trim();
return result || null;
} catch {
return null;
}
};
const generateApiKeyToken = (workspaceId: string): string | null => {
try {
const output = execSync(
`docker exec -e NODE_ENV=development ${SERVER_CONTAINER} yarn command:prod workspace:generate-api-key -w ${workspaceId}`,
{ encoding: 'utf-8' },
);
const TOKEN_PREFIX = 'TOKEN:';
const tokenLine = output
.trim()
.split('\n')
.find((line) => line.includes(TOKEN_PREFIX));
if (!tokenLine) {
return null;
}
const tokenStartIndex =
tokenLine.indexOf(TOKEN_PREFIX) + TOKEN_PREFIX.length;
return tokenLine.slice(tokenStartIndex).trim();
} catch {
return null;
}
};
export type LocalInstanceResult = {
running: boolean;
apiKey?: string;
};
export const setupLocalInstance = async (): Promise<LocalInstanceResult> => {
console.log('');
console.log(chalk.blue('🐳 Setting up local Twenty instance...'));
if (await isTwentyServerRunning()) {
console.log(
chalk.green('✅ Twenty server is already running on localhost:3000.'),
);
} else {
if (!isDockerAvailable()) {
console.log(
chalk.yellow(
'⚠️ Docker Compose is not installed. Please install Docker first.',
),
);
console.log(chalk.gray(' See https://docs.docker.com/get-docker/'));
return { running: false };
}
if (!isDockerRunning()) {
console.log(
chalk.yellow(
'⚠️ Docker is not running. Please start Docker and try again.',
),
);
return { running: false };
}
try {
execSync(`bash <(curl -sL ${INSTALL_SCRIPT_URL})`, {
stdio: 'inherit',
shell: '/bin/bash',
});
} catch {
console.log(
chalk.yellow('⚠️ Local instance setup did not complete successfully.'),
);
return { running: false };
}
}
console.log('');
console.log(
chalk.blue(
'👉 Please create your workspace in the browser before continuing.',
),
);
const { workspaceCreated } = await inquirer.prompt([
{
type: 'confirm',
name: 'workspaceCreated',
message: 'Have you finished creating your workspace?',
default: true,
},
]);
if (!workspaceCreated) {
console.log(
chalk.yellow(
'⚠️ Skipping API key generation. Run `yarn twenty remote add --local` manually after creating your workspace.',
),
);
return { running: true };
}
console.log(chalk.blue('🔑 Generating API key for your workspace...'));
const workspaceId = getActiveWorkspaceId();
if (!isDefined(workspaceId)) {
console.log(
chalk.yellow(
'⚠️ No active workspace found. Make sure you completed the signup flow, then run `yarn twenty auth:login` manually.',
),
);
return { running: true };
}
const apiKey = generateApiKeyToken(workspaceId);
if (!isDefined(apiKey)) {
console.log(
chalk.yellow(
'⚠️ Could not generate API key. Run `yarn twenty auth:login` manually.',
),
);
return { running: true };
}
console.log(chalk.green('✅ API key generated for your workspace.'));
return { running: true, apiKey };
};
@@ -45,7 +45,6 @@ export default defineConfig({
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_URL: 'http://localhost:3000',
TWENTY_API_KEY:
'${SEED_API_KEY}',
},
@@ -120,12 +119,13 @@ beforeAll(async () => {
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
const configFile = {
profiles: {
default: {
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
};
fs.writeFileSync(
@@ -30,7 +30,7 @@ type AnalysisResult = {
commitments: Commitment[];
};
type RichTextV2Data = {
type RichTextData = {
markdown: string;
blocknote: null;
};
@@ -123,7 +123,7 @@ const createNoteInTwenty = async (
bodyV2: {
markdown: noteBodyMarkdown,
blocknote: null,
} satisfies RichTextV2Data,
} satisfies RichTextData,
};
try {
@@ -159,7 +159,7 @@ const createTaskInTwenty = async (
const taskData: {
title: string;
bodyV2: RichTextV2Data;
bodyV2: RichTextData;
dueAt?: string;
} = {
title: actionItem.title,
@@ -10,3 +10,4 @@
- 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.
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty auth:login
yarn twenty remote add --local
```
Then, start development mode to sync your app and watch for changes:
```bash
yarn twenty app:dev
yarn twenty dev
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Authentication
yarn twenty auth:login # Authenticate with Twenty
yarn twenty auth:logout # Remove credentials
yarn twenty auth:status # Check auth status
yarn twenty auth:switch # Switch default workspace
yarn twenty auth:list # List all configured workspaces
# Remotes & Authentication
yarn twenty remote add --local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
yarn twenty remote remove <name> # Remove a remote
# Application
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty function:logs # Stream function logs
yarn twenty function:execute # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty logs # Stream function logs
yarn twenty exec # Execute a function with JSON payload
yarn twenty uninstall # Uninstall app from workspace
```
## LLMs instructions
@@ -32,7 +32,7 @@ type AnalysisResult = {
commitments: Commitment[];
};
type RichTextV2Data = {
type RichTextData = {
markdown: string;
blocknote: null;
};
@@ -362,7 +362,7 @@ const createNoteInTwenty = async (
bodyV2: {
markdown: noteBodyMarkdown,
blocknote: null,
} satisfies RichTextV2Data,
} satisfies RichTextData,
};
try {
@@ -451,7 +451,7 @@ const createTaskInTwenty = async (
const taskData: {
title: string;
bodyV2: RichTextV2Data;
bodyV2: RichTextData;
dueAt?: string;
assigneeId?: string;
} = {
@@ -9,9 +9,9 @@
},
"packageManager": "yarn@4.9.2",
"scripts": {
"app:dev": "twenty app:dev",
"function:execute": "twenty function:execute",
"app:uninstall": "twenty app:uninstall",
"dev": "twenty dev",
"exec": "twenty exec",
"uninstall": "twenty uninstall",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
@@ -9,16 +9,16 @@
},
"packageManager": "yarn@4.9.2",
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"app:uninstall": "twenty app:uninstall",
"remote:add": "twenty remote add --local",
"remote:status": "twenty remote status",
"remote:switch": "twenty remote switch",
"remote:list": "twenty remote list",
"remote:remove": "twenty remote remove",
"dev": "twenty dev",
"add": "twenty add",
"logs": "twenty logs",
"exec": "twenty exec",
"uninstall": "twenty uninstall",
"help": "twenty help",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
@@ -9,16 +9,16 @@
},
"packageManager": "yarn@4.9.2",
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"app:uninstall": "twenty app:uninstall",
"remote:add": "twenty remote add --local",
"remote:status": "twenty remote status",
"remote:switch": "twenty remote switch",
"remote:list": "twenty remote list",
"remote:remove": "twenty remote remove",
"dev": "twenty dev",
"add": "twenty add",
"logs": "twenty logs",
"exec": "twenty exec",
"uninstall": "twenty uninstall",
"help": "twenty help",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
@@ -9,16 +9,16 @@
},
"packageManager": "yarn@4.9.2",
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"app:uninstall": "twenty app:uninstall",
"remote:add": "twenty remote add --local",
"remote:status": "twenty remote status",
"remote:switch": "twenty remote switch",
"remote:list": "twenty remote list",
"remote:remove": "twenty remote remove",
"dev": "twenty dev",
"add": "twenty add",
"logs": "twenty logs",
"exec": "twenty exec",
"uninstall": "twenty uninstall",
"help": "twenty help",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
@@ -1,8 +1,10 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { ALL_POST_CARD_RECIPIENTS_VIEW_ID } from '../views/all-post-card-recipients.view';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
export default defineNavigationMenuItem({
universalIdentifier: 'c1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d',
position: 2,
viewUniversalIdentifier: ALL_POST_CARD_RECIPIENTS_VIEW_ID,
type: NavigationMenuItemType.OBJECT,
targetObjectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
});
@@ -1,8 +1,10 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { ALL_POST_CARDS_VIEW_ID } from '../views/all-post-cards.view';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineNavigationMenuItem({
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
position: 0,
viewUniversalIdentifier: ALL_POST_CARDS_VIEW_ID,
type: NavigationMenuItemType.OBJECT,
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
});
@@ -1,8 +1,10 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { ALL_RECIPIENTS_VIEW_ID } from '../views/all-recipients.view';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/recipient.object';
export default defineNavigationMenuItem({
universalIdentifier: 'c1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d',
position: 1,
viewUniversalIdentifier: ALL_RECIPIENTS_VIEW_ID,
type: NavigationMenuItemType.OBJECT,
targetObjectUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
});
+1
View File
@@ -10,3 +10,4 @@
- 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.
+13 -13
View File
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty auth:login
yarn twenty remote add --local
```
Then, start development mode to sync your app and watch for changes:
```bash
yarn twenty app:dev
yarn twenty dev
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Authentication
yarn twenty auth:login # Authenticate with Twenty
yarn twenty auth:logout # Remove credentials
yarn twenty auth:status # Check auth status
yarn twenty auth:switch # Switch default workspace
yarn twenty auth:list # List all configured workspaces
# Remotes & Authentication
yarn twenty remote add --local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
yarn twenty remote remove <name> # Remove a remote
# Application
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty function:logs # Stream function logs
yarn twenty function:execute # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty logs # Stream function logs
yarn twenty exec # Execute a function with JSON payload
yarn twenty uninstall # Uninstall app from workspace
```
## Integration Tests
@@ -29,12 +29,13 @@ beforeAll(async () => {
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
const configFile = {
profiles: {
default: {
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
};
fs.writeFileSync(
@@ -1,5 +1,6 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '10f90627-e9c2-44b7-9742-bed77e3d1b17',
@@ -7,5 +8,6 @@ export default defineNavigationMenuItem({
icon: 'IconList',
color: 'blue',
position: 0,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
@@ -7,3 +7,4 @@
- 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.
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty auth:login
yarn twenty remote add --local
```
Then, start development mode to sync your app and watch for changes:
```bash
yarn twenty app:dev
yarn twenty dev
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Authentication
yarn twenty auth:login # Authenticate with Twenty
yarn twenty auth:logout # Remove credentials
yarn twenty auth:status # Check auth status
yarn twenty auth:switch # Switch default workspace
yarn twenty auth:list # List all configured workspaces
# Remotes & Authentication
yarn twenty remote add --local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
yarn twenty remote remove <name> # Remove a remote
# Application
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty function:logs # Stream function logs
yarn twenty function:execute # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty logs # Stream function logs
yarn twenty exec # Execute a function with JSON payload
yarn twenty uninstall # Uninstall app from workspace
```
## LLMs instructions
@@ -236,7 +236,7 @@ const handler = async (event: any) => {
},
});
// TODO: remove `as any` after running `yarn twenty app:dev` to regenerate the typed client
// TODO: remove `as any` after running `yarn twenty dev` to regenerate the typed client
const updateSummary = async (markdown: string) => {
await client.mutation({
updateCallRecording: {
@@ -1,10 +1,12 @@
import { CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/call-recording-view';
import { defineNavigationMenuItem } from 'twenty-sdk';
import { NavigationMenuItemType } from 'twenty-shared/types';
export default defineNavigationMenuItem({
universalIdentifier: '5248a62d-7d2e-43a7-ba45-6e8f61876a71',
name: 'Call recordings',
icon: 'IconPhone',
position: 0,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER,
});
@@ -81,7 +81,7 @@ export default defineObject({
},
{
universalIdentifier: TRANSCRIPT_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.RICH_TEXT_V2,
type: FieldType.RICH_TEXT,
name: 'transcript',
label: 'Transcript',
description: 'Human-readable transcript of the call',
@@ -114,7 +114,7 @@ export default defineObject({
},
{
universalIdentifier: SUMMARY_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.RICH_TEXT_V2,
type: FieldType.RICH_TEXT,
name: 'summary',
label: 'Summary',
description: 'AI-generated summary of the call',
@@ -16,7 +16,7 @@ Use this skill when a user asks you to summarize, analyze, or extract insights f
## How to Access the Data
1. Use \`find_one_callRecording\` to fetch the call recording by its ID.
2. Read the \`transcript\` field (RICH_TEXT_V2, markdown format) which contains the full conversation.
2. Read the \`transcript\` field (RICH_TEXT, markdown format) which contains the full conversation.
3. The transcript uses the format: **Speaker Name:** spoken text
## What to Produce
@@ -1,4 +1,5 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
export default defineNavigationMenuItem({
@@ -6,6 +7,7 @@ export default defineNavigationMenuItem({
name: 'Self host user',
icon: 'IconList',
position: 1,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
});
@@ -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/*
!.twenty/output/
# 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
@@ -0,0 +1 @@
24.5.0
@@ -0,0 +1 @@
nodeLinker: node-modules
@@ -0,0 +1,9 @@
## 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/cli/__tests__/apps/rich-app
## 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.
@@ -0,0 +1,51 @@
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
## Getting Started
First, authenticate to your workspace:
```bash
yarn twenty auth:login
```
Then, start development mode to sync your app and watch for changes:
```bash
yarn twenty app:dev
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
## Available Commands
Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Authentication
yarn twenty auth:login # Authenticate with Twenty
yarn twenty auth:logout # Remove credentials
yarn twenty auth:status # Check auth status
yarn twenty auth:switch # Switch default workspace
yarn twenty auth:list # List all configured workspaces
# Application
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty function:logs # Stream function logs
yarn twenty function:execute # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
```
## LLMs instructions
Main docs and pitfalls are available in LLMS.md file.
## Learn More
To learn more about Twenty applications, take a look at the following resources:
- [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) - learn about `twenty-sdk` tool.
- [Twenty doc](https://docs.twenty.com/) - Twenty's documentation.
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
You can check out [the Twenty GitHub repository](https://github.com/twentyhq/twenty) - your feedback and contributions are welcome!
@@ -0,0 +1,29 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default [
// Base JS recommended rules
js.configs.recommended,
// TypeScript recommended rules
...tseslint.configs.recommended,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// Common TypeScript-friendly tweaks
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'off',
'no-unused-vars': 'off', // handled by TS rule
},
},
];
@@ -0,0 +1,28 @@
{
"name": "twentyfortwenty",
"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": {
"twenty": "twenty",
"lint": "eslint",
"lint:fix": "eslint --fix"
},
"dependencies": {
"twenty-sdk": "latest",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^18.2.0",
"eslint": "^9.32.0",
"react": "^18.2.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.0"
}
}
@@ -0,0 +1,32 @@
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { defineApplication } from 'twenty-sdk';
export default defineApplication({
universalIdentifier: '0fac8de4-9d11-492b-9e6a-577e11e1c442',
displayName: 'Twenty for Twenty',
description: '',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
applicationVariables: {
CLICKHOUSE_URL: {
universalIdentifier: 'b204304f-d3d3-4ae2-aafa-24b21c159181',
description:
'The URL of the ClickHouse server, including the protocol and port (e.g., http://localhost:8123)',
isSecret: true,
},
CLICKHOUSE_USERNAME: {
universalIdentifier: '711e8ea8-8b19-4cf0-82ab-ab44417312bd',
description: 'The username for authenticating with the ClickHouse server',
isSecret: true,
},
CLICKHOUSE_PASSWORD: {
universalIdentifier: 'faba7ed7-9f94-4202-944b-c25c683e9504',
description: 'The password for authenticating with the ClickHouse server',
isSecret: true,
},
CLICKHOUSE_DATABASE: {
universalIdentifier: '3e6698fa-0c00-49e5-9c4d-34d5b177bff3',
description: 'The name of the ClickHouse database to connect to',
isSecret: true,
},
},
});
@@ -0,0 +1,37 @@
// Field metadata identifiers for cloudUser2 object
// These constants are used by both the object definition and views
export const CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID =
'56e3d6ce-d57e-4ded-830f-958eb18d4d36';
export const CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID =
'1f7062c1-0e96-4599-a59c-6d7427794bb1';
export const CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID =
'e134ce6b-5ab0-4e9c-ba32-36d0851aca21';
export const CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID =
'f305d341-7b12-4939-9299-e5e1b6d1591e';
export const CLOUD_USER_2_EMAIL_FIELD_ID =
'ee110a77-34d7-4c8b-bc10-8560b9e2333a';
export const CLOUD_USER_2_FULL_NAME_FIELD_ID =
'3c9540be-6e42-40f2-8598-973117bbe105';
export const CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID =
'efaa6e54-8019-454c-a556-2e340f0b156d';
export const CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID =
'86a07ca4-420e-4c82-82f8-8f8dfbfc2dc4';
export const CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID =
'87bf8c09-1863-4c65-811f-6fb54a87238e';
export const CLOUD_USER_2_IS_TWENTY_FIELD_ID =
'f6dcd9f3-3f7e-4aea-84a5-41282a68910d';
export const CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID =
'643ab4a2-b6f2-4d11-9173-6859884b8781';
export const CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID =
'294e95cb-13b3-4163-95f8-e4f31837cb47';
export const CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID =
'8dacb4f1-b4ef-42d6-a917-17c78d6273a7';
export const CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID =
'c7741332-d9b6-42ed-84e2-8afa184b58f3';
export const CLOUD_USER_2_UPDATED_BY_FIELD_ID =
'639c9a30-2926-44c3-ab44-100dbda91c64';
export const CLOUD_USER_2_USER_TENURE_FIELD_ID =
'0cc9ca63-c06f-4422-9325-5e99f98d05ed';
export const CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID =
'5af6ae62-b741-471b-90bb-7fb6678fc8c9';
@@ -0,0 +1,30 @@
import {
defineField,
FieldType,
OnDeleteAction,
RelationType,
} from 'twenty-sdk';
import {
CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID,
CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID,
} from 'src/fields/cloud-user-workspaces-on-cloud-user.field';
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
export default defineField({
universalIdentifier: CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID,
objectUniversalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'cloudUser2',
label: 'Cloud User 2',
relationTargetObjectMetadataUniversalIdentifier:
CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'cloudUser2Id',
},
});
@@ -0,0 +1,11 @@
// Field metadata identifiers for cloudUserWorkspace2 object
// These constants are used by both the object definition and views
export const CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID =
'ad149693-0df5-4346-90ff-39e6d945b90f';
export const CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID =
'c5d02b59-ebaf-4604-809c-ce3f6e50dedc';
export const CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID =
'6a0f8018-be3c-49b0-bb91-8d3e6b44adec';
export const CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID =
'29d47f11-1e45-4c67-b495-fcfa45bb67e2';
@@ -0,0 +1,24 @@
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
export const CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID =
'2290d722-d4b1-47ab-a895-4e2c3163a541';
export const CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID =
'49b08cf9-15e1-4583-826f-943fe3c6b0e8';
export default defineField({
universalIdentifier: CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID,
objectUniversalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'cloudUserWorkspaces2',
label: 'Cloud User Workspaces 2',
relationTargetObjectMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
@@ -0,0 +1,24 @@
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
import { CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-workspace-2';
export const CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID =
'90d9dfc7-7058-4d1c-aac2-1505bd7cb827';
export const CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID =
'7bcd2dda-d1f8-4aa7-a83f-6cf240be80b2';
export default defineField({
universalIdentifier: CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID,
objectUniversalIdentifier: CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'cloudUserWorkspaces2',
label: 'Cloud User Workspaces 2',
relationTargetObjectMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
@@ -0,0 +1,27 @@
import {
defineField,
FieldType,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
export const CLOUD_USERS_2_ON_PERSON_ID =
'f238e91c-11d8-4a27-a39b-c9fa3515d4a0';
export const PERSON_ON_CLOUD_USER_2_ID = '08d3aff0-7548-4a99-a097-20a0ad2c9ee7';
export default defineField({
universalIdentifier: CLOUD_USERS_2_ON_PERSON_ID,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.RELATION,
name: 'cloudUsers2',
label: 'Cloud Users 2',
relationTargetObjectMetadataUniversalIdentifier:
CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: PERSON_ON_CLOUD_USER_2_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
@@ -0,0 +1,83 @@
// Field metadata identifiers for cloudWorkspace2 object
// These constants are used by both the object definition and views
export const CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID =
'c30799d5-adc0-49be-82a1-6d41b5aa9e82';
export const CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID =
'bfa2f91d-1c6c-48de-bb11-ac2a1e2545a9';
export const CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID =
'f9f2ef3a-8235-4d21-9a45-d29cc2124456';
export const CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID =
'fbeaab89-77e0-4e13-97ed-caeb757a38c7';
export const CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID =
'7d3d4ba3-5af2-4248-afc7-7f4ce7c82805';
export const CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID =
'56e99339-dcc4-4950-ae64-1b6ae326af2c';
export const CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID =
'e9c703e6-9ffc-497d-9755-018724ac8d66';
export const CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID =
'81d8f622-2366-481f-b0e8-b14fb7efa4d5';
export const CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID =
'3e53d606-1afa-4d49-be62-52f4bb1cb49e';
export const CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID =
'3fdd272c-73dd-432e-99c7-782f237a7cd8';
export const CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID =
'1931f3cd-a984-41e3-b8f5-510f67b508e6';
export const CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID =
'1f6a0ef6-3faf-4157-88a7-f624b7655c6b';
export const CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID =
'ab0f46aa-9fef-40c6-a229-6ed4e05867d4';
export const CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID =
'38095859-2efb-4f40-86a1-aa883fd88e3d';
export const CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID =
'4ebd8329-a18c-4606-9ee0-f32fb1c4aa6f';
export const CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID =
'754f137f-b759-47da-8b57-7de71ad34d37';
export const CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID =
'1627e6b8-e87a-4048-b954-8d2fe09b67c8';
export const CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID =
'1d8bb210-b50d-4b38-a611-d78ba865cbae';
export const CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID =
'42879662-d4fc-4806-bcfb-fdb586827491';
export const CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID =
'44b2c12f-aedd-459a-848a-ade9fc4620b7';
export const CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID =
'cac2d3e0-c2ee-4e22-bd96-4fa051a36f73';
export const CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID =
'45cbddfa-54cc-4d94-88d2-db7a7a550414';
export const CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID =
'a6686308-dfcf-4abb-be0f-e8f48cc579d9';
export const CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID =
'0432af7e-7575-486e-a022-f4a625a8ac14';
export const CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID =
'40854f41-4175-4b14-9567-c6bfb3f5d441';
export const CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID =
'44aba339-be2e-4364-a894-cf730081c8a6';
export const CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID =
'f11ddfc2-3356-453a-a6e1-494b6c6b12f6';
export const CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID =
'8fb18f52-44e9-4288-87c1-9650222efb05';
export const CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID =
'8f5dfc54-213b-43b2-a502-631f0f3240d1';
export const CLOUD_WORKSPACE_2_MRR_FIELD_ID =
'67125b6c-ff41-459f-8153-a338ed11c4f0';
export const CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID =
'0c38b297-0e9d-47cc-8f8f-a0e5402413ce';
export const CLOUD_WORKSPACE_2_ARR_FIELD_ID =
'f932f94b-c48b-4ef3-9eea-0c8dad370d89';
export const CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID =
'ae5da0c9-e9ac-401b-ad48-c37a31c6ecd1';
export const CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID =
'ef44daa9-d272-4680-a19c-caf2bac32b70';
export const CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID =
'a88e8cab-333f-44a3-980e-f2cc22871aee';
export const CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID =
'fccc73da-2b6e-434b-921b-e3bcbab1c56c';
export const CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID =
'488b38b4-971e-4c33-b60d-7e7934911579';
export const CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID =
'3815b992-a919-4c52-8629-b4b5f89a6060';
export const CLOUD_WORKSPACE_2_TAGS_FIELD_ID =
'0f41485f-4868-4bd6-8d76-709a6b7a54e3';
export const CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID =
'252cdff1-facb-4f66-8608-6138a585d099';
@@ -0,0 +1,25 @@
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk';
import {
CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID,
CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID,
} from 'src/fields/cloud-user-workspaces-on-cloud-workspace.field';
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
import { CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-workspace-2';
export default defineField({
universalIdentifier: CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID,
objectUniversalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'cloudWorkspace2',
label: 'Cloud Workspace 2',
relationTargetObjectMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'cloudWorkspace2Id',
},
});
@@ -0,0 +1,29 @@
import {
defineField,
FieldType,
OnDeleteAction,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import {
CLOUD_USERS_2_ON_PERSON_ID,
PERSON_ON_CLOUD_USER_2_ID,
} from 'src/fields/cloud-users-2-on-person.field';
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
export default defineField({
universalIdentifier: PERSON_ON_CLOUD_USER_2_ID,
objectUniversalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: CLOUD_USERS_2_ON_PERSON_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
@@ -0,0 +1,49 @@
import { defineLogicFunction } from 'twenty-sdk';
import { syncCloudUserWorkspaces } from 'src/logic-functions/sync-product-data/sync-cloud-user-workspaces';
import { syncCloudUsers } from 'src/logic-functions/sync-product-data/sync-cloud-users';
import { syncCloudWorkspaces } from 'src/logic-functions/sync-product-data/sync-cloud-workspaces';
const handler = async (): Promise<{ message: string }> => {
try {
console.log('Starting product data sync');
const cloudUsersResult = await syncCloudUsers();
console.log(
`Cloud users sync complete: ${cloudUsersResult.syncedCount} users`,
);
const cloudWorkspacesResult = await syncCloudWorkspaces();
console.log(
`Cloud workspaces sync complete: ${cloudWorkspacesResult.syncedCount} workspaces`,
);
const cloudUserWorkspacesResult = await syncCloudUserWorkspaces();
console.log(
`Cloud user workspaces sync complete: ${cloudUserWorkspacesResult.syncedCount} user workspaces`,
);
return {
message: `Product data sync complete — ${cloudUsersResult.syncedCount} users, ${cloudWorkspacesResult.syncedCount} workspaces, ${cloudUserWorkspacesResult.syncedCount} user workspaces`,
};
} catch (err) {
console.log(err);
throw err;
}
};
export default defineLogicFunction({
universalIdentifier: '3897e059-715e-4a4b-b165-c44f17d2e30a',
name: 'sync-product-data',
description:
'Syncs cloud users, cloud workspaces, and cloud user workspaces from ClickHouse',
timeoutSeconds: 120,
handler,
cronTriggerSettings: {
pattern: '*/10 * * * *',
},
});
@@ -0,0 +1,93 @@
import { z } from 'zod';
import { getApplicationConfig } from 'src/shared/application-config';
import { fetchFromClickHouse } from 'src/shared/clickhouse-client';
import { twentyClient } from 'src/shared/twenty-client';
const clickHouseUserWorkspaceSchema = z.object({
id: z.string(),
workspaceId: z.string(),
userId: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
deletedAt: z.string(),
is_active_membership: z.coerce.boolean(),
});
type ClickHouseUserWorkspace = z.infer<typeof clickHouseUserWorkspaceSchema>;
const fetchUserWorkspacesFromClickHouse = async (): Promise<
ClickHouseUserWorkspace[]
> => {
const { clickHouseDatabase } = getApplicationConfig();
// const nowDate = 'now()'
const nowDate = "'2026-02-04 14:28:52.000'";
const query = `
SELECT
*
FROM (
SELECT
*,
row_number() OVER (PARTITION BY id ORDER BY updatedAt DESC) AS rn
FROM
${clickHouseDatabase}.user_workspace
WHERE
updatedAt >= ${nowDate} - INTERVAL 500 MINUTE
AND
updatedAt <= ${nowDate}
)
WHERE
rn = 1
FORMAT
JSONEachRow;
`;
return fetchFromClickHouse(query, clickHouseUserWorkspaceSchema);
};
const buildCloudUserWorkspaceInput = (
userWorkspace: ClickHouseUserWorkspace,
) => ({
id: userWorkspace.id,
twentyUserIdentifier: userWorkspace.userId,
twentyWorkspaceIdentifier: userWorkspace.workspaceId,
idOfTheUserWorkspace: userWorkspace.id,
cloudUser2Id: userWorkspace.userId,
cloudWorkspace2Id: userWorkspace.workspaceId,
});
export const syncCloudUserWorkspaces = async (): Promise<{
syncedCount: number;
}> => {
const userWorkspaces = await fetchUserWorkspacesFromClickHouse();
console.log(
`Fetched ${userWorkspaces.length} user workspaces from ClickHouse`,
);
if (userWorkspaces.length === 0) {
return { syncedCount: 0 };
}
const cloudUserWorkspaceInputs = userWorkspaces.map(
buildCloudUserWorkspaceInput,
);
console.log(
`Batch-upserting ${cloudUserWorkspaceInputs.length} cloud user workspaces`,
);
await twentyClient.mutation({
createCloudUserWorkspaces2: {
__args: {
data: cloudUserWorkspaceInputs,
upsert: true,
},
__scalar: true,
},
});
return { syncedCount: userWorkspaces.length };
};
@@ -0,0 +1,268 @@
import { enumCloudUser2ActivityStatusEnum } from 'twenty-sdk/generated';
import { z } from 'zod';
import { getApplicationConfig } from 'src/shared/application-config';
import { fetchFromClickHouse } from 'src/shared/clickhouse-client';
import { clickHouseDateToIso } from 'src/shared/clickhouse-date-to-iso';
import { twentyClient } from 'src/shared/twenty-client';
const clickHouseUserSchema = z.object({
userId: z.uuid(),
firstName: z.string(),
lastName: z.string(),
email: z.email(),
fullName: z.string(),
isEmailVerified: z.boolean(),
disabled: z.boolean(),
canImpersonate: z.boolean(),
canAccessFullAdminPanel: z.boolean(),
createdAt: z.string(),
updatedAt: z.string(),
deletedAt: z.string(),
locale: z.string(),
createdDate: z.string(),
workspaceCount: z.coerce.number(),
workspaceIds: z.string(),
workspaceDomains: z.string(),
firstActivityDate: z.string(),
lastActivityDate: z.string(),
lastWorkspaceId: z.string(),
totalPageviews: z.coerce.number(),
pageviewsLast30d: z.coerce.number(),
pageviewsLast7d: z.coerce.number(),
pageviewsLast24h: z.coerce.number(),
userAgeDays: z.coerce.number(),
daysSinceLastActivity: z.coerce.number(),
isActiveLast30d: z.boolean(),
isActiveLast7d: z.boolean(),
isActiveLast24h: z.boolean(),
activityStatus: z
.string()
.transform((val) => val.toUpperCase())
.pipe(z.enum(enumCloudUser2ActivityStatusEnum)),
avgDailyPageviewsLast30d: z.coerce.number(),
isTwenty: z.coerce.boolean(),
maxWorkspaceMembers: z.coerce.number(),
inTrial: z.boolean(),
});
type ClickHouseUser = z.infer<typeof clickHouseUserSchema>;
const fetchUsersFromClickHouse = async (): Promise<ClickHouseUser[]> => {
const { clickHouseDatabase } = getApplicationConfig();
// const nowDate = 'now()'
const nowDate = "'2026-02-04 14:28:52.000'";
const query = `
SELECT
*
FROM
${clickHouseDatabase}.user
WHERE
lastActivityDate >= ${nowDate} - INTERVAL 500 MINUTE
AND
lastActivityDate <= ${nowDate}
FORMAT
JSONEachRow;
`;
return fetchFromClickHouse(query, clickHouseUserSchema);
};
const fetchAllPeopleFromTwentyByEmail = async (emails: string[]) => {
if (emails.length === 0) {
return [];
}
const allPeople = await twentyClient.query({
people: {
edges: {
node: {
id: true,
emails: {
primaryEmail: true,
},
},
},
__args: {
filter: {
emails: {
primaryEmail: {
in: emails,
},
},
},
},
},
});
return allPeople.people?.edges.map((edge) => edge.node) ?? [];
};
const buildCloudUserInput = ({
user,
personId,
}: {
user: ClickHouseUser;
personId: string;
}) => ({
id: user.userId,
name: user.fullName,
email: {
primaryEmail: user.email,
},
personId,
fullName: {
lastName: user.lastName,
firstName: user.firstName,
},
isTwenty: user.isTwenty,
userTenure: user.userAgeDays,
isActiveL7d: user.isActiveLast7d,
isActiveL24h: user.isActiveLast24h,
isActiveL30d: user.isActiveLast30d,
pageViewsL7d: user.pageviewsLast7d,
pageViewsL24h: user.pageviewsLast24h,
pageViewsL30d: user.pageviewsLast30d,
activityStatus: user.activityStatus,
workspaceCount: user.workspaceCount,
lastActivityDate: clickHouseDateToIso(user.lastActivityDate),
dataLastUpdatedAt: new Date().toISOString(),
avgDailyPageviewsLast30d: user.avgDailyPageviewsLast30d,
daysSinceLastActivity: user.daysSinceLastActivity,
});
export const syncCloudUsers = async (): Promise<{
syncedCount: number;
}> => {
const users = await fetchUsersFromClickHouse();
console.log('Fetched users from ClickHouse', users);
const emails = users.map((user) => user.email);
console.log('Fetching people from Twenty with emails', emails);
const people = await fetchAllPeopleFromTwentyByEmail(emails);
console.log('Fetched people from Twenty', people);
// Build a map of email -> personId from existing people
const emailToPersonId = new Map<string, string>();
for (const person of people) {
if (person.emails?.primaryEmail !== undefined) {
emailToPersonId.set(person.emails.primaryEmail.toLowerCase(), person.id);
}
}
// Partition users into those with/without an existing person
const usersWithoutPerson = users.filter(
(user) => !emailToPersonId.has(user.email.toLowerCase()),
);
// Step 1: Batch-create missing people
const newlyCreatedPersonIds: string[] = [];
if (usersWithoutPerson.length > 0) {
console.log(`Batch-creating ${usersWithoutPerson.length} people records`);
const createPeopleResult = await twentyClient.mutation({
createPeople: {
__args: {
data: usersWithoutPerson.map((user) => ({
name: {
firstName: user.firstName,
lastName: user.lastName,
},
emails: {
primaryEmail: user.email,
},
})),
},
id: true,
emails: {
primaryEmail: true,
},
},
});
const createdPeople = createPeopleResult.createPeople ?? [];
for (const person of createdPeople) {
if (
person.emails?.primaryEmail === undefined ||
person.id === undefined
) {
continue;
}
newlyCreatedPersonIds.push(person.id);
emailToPersonId.set(person.emails.primaryEmail.toLowerCase(), person.id);
}
}
// Step 2: Batch-upsert all cloud users, with rollback on failure
const cloudUserInputs = users.map((user) => {
const personId = emailToPersonId.get(user.email.toLowerCase());
if (personId === undefined) {
throw new Error(
`No personId found for user ${user.email} — this should not happen`,
);
}
return buildCloudUserInput({ user, personId });
});
try {
console.log(`Batch-upserting ${cloudUserInputs.length} cloud users`);
await twentyClient.mutation({
createCloudUsers2: {
__args: {
data: cloudUserInputs,
upsert: true,
},
__scalar: true,
},
});
} catch (cloudUserError) {
console.log(
'Cloud user upsert failed, rolling back newly created people',
cloudUserError,
);
// Rollback: hard-delete people that were created in step 1
if (newlyCreatedPersonIds.length > 0) {
try {
await twentyClient.mutation({
destroyPeople: {
__args: {
filter: {
id: {
in: newlyCreatedPersonIds,
},
},
},
id: true,
},
});
console.log(
`Rolled back ${newlyCreatedPersonIds.length} newly created people`,
);
} catch (rollbackError) {
console.log(
'Rollback of newly created people also failed',
rollbackError,
);
}
}
throw cloudUserError;
}
return { syncedCount: users.length };
};
@@ -0,0 +1,201 @@
import {
enumCloudWorkspace2ActivationStatusEnum,
enumCloudWorkspace2PaymentFrequencyEnum,
enumCloudWorkspace2SubscriptionStatusEnum,
} from 'twenty-sdk/generated';
import { z } from 'zod';
import { getApplicationConfig } from 'src/shared/application-config';
import { fetchFromClickHouse } from 'src/shared/clickhouse-client';
import { clickHouseDateToIso } from 'src/shared/clickhouse-date-to-iso';
import { twentyClient } from 'src/shared/twenty-client';
const clickHouseWorkspaceSchema = z.object({
workspaceId: z.string(),
workspaceName: z.string(),
subdomain: z.string(),
customDomain: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
deletedAt: z.string(),
activationStatus: z
.string()
.transform((val) => {
const upper = val.toUpperCase();
if (upper === '') return 'EMPTY';
const valid: string[] = Object.values(
enumCloudWorkspace2ActivationStatusEnum,
);
return valid.includes(upper) ? upper : 'EMPTY';
})
.pipe(z.enum(enumCloudWorkspace2ActivationStatusEnum)),
createdDate: z.string(),
lastPageviewDate: z.string(),
pageviewsLast30d: z.coerce.number(),
pageviewsLast7d: z.coerce.number(),
pageviewsLast24h: z.coerce.number(),
totalEverActiveUsers: z.coerce.number(),
totalWorkspaceUsers: z.coerce.number(),
activeUsersLast30d: z.coerce.number(),
activeUsersLast7d: z.coerce.number(),
activeUsersLast24h: z.coerce.number(),
isActiveLast30d: z.coerce.boolean(),
isActiveLast7d: z.coerce.boolean(),
isActiveLast24h: z.coerce.boolean(),
activeUserRatioLast30d: z.coerce.number(),
activeUserRatioLast7d: z.coerce.number(),
workspaceAgeDays: z.coerce.number(),
totalEvents: z.coerce.number(),
eventsLast30d: z.coerce.number(),
eventsPerUser: z.coerce.number(),
subscription_status: z
.string()
.transform((val) => {
const upper = val.toUpperCase();
if (upper === '') return 'EMPTY';
const valid: string[] = Object.values(
enumCloudWorkspace2SubscriptionStatusEnum,
);
return valid.includes(upper) ? upper : 'OTHER';
})
.pipe(z.enum(enumCloudWorkspace2SubscriptionStatusEnum)),
payment_frequency: z
.string()
.transform((val) => {
const upper = val.toUpperCase();
if (upper === '') return 'EMPTY';
const valid: string[] = Object.values(
enumCloudWorkspace2PaymentFrequencyEnum,
);
return valid.includes(upper) ? upper : 'OTHER';
})
.pipe(z.enum(enumCloudWorkspace2PaymentFrequencyEnum)),
trial_status: z.string(),
mrr: z.coerce.number(),
potential_arr: z.coerce.number(),
arr: z.coerce.number().nullable(),
next_renewal_date: z.string(),
workspace_domain: z.string(),
domain_source: z.string(),
creatorUserId: z.string(),
creatorEmail: z.string(),
creator_domain_type: z.string(),
primary_business_domain: z.string(),
business_domain_user_count: z.coerce.number(),
isTwenty: z.coerce.boolean(),
});
type ClickHouseWorkspace = z.infer<typeof clickHouseWorkspaceSchema>;
const fetchWorkspacesFromClickHouse =
async (): Promise<ClickHouseWorkspace[]> => {
const { clickHouseDatabase } = getApplicationConfig();
// const nowDate = 'now()'
const nowDate = "'2026-02-04 14:28:52.000'";
const query = `
SELECT
*
FROM (
SELECT
*,
row_number() OVER (PARTITION BY workspaceId ORDER BY updatedAt DESC) AS rn
FROM
${clickHouseDatabase}.workspace
WHERE
updatedAt >= ${nowDate} - INTERVAL 500 MINUTE
AND
updatedAt <= ${nowDate}
)
WHERE
rn = 1
FORMAT
JSONEachRow;
`;
return fetchFromClickHouse(query, clickHouseWorkspaceSchema);
};
// Convert a dollar amount to micros (1 USD = 1_000_000 micros).
const dollarsToAmountMicros = (dollars: number) =>
Math.round(dollars * 1_000_000);
const buildCloudWorkspaceInput = (workspace: ClickHouseWorkspace) => ({
id: workspace.workspaceId,
name: workspace.workspaceName,
subDomain: workspace.subdomain,
customDomain: {
primaryLinkUrl: workspace.customDomain,
primaryLinkLabel: '',
},
activationStatus: workspace.activationStatus,
subscriptionStatus: workspace.subscription_status,
paymentFrequency: workspace.payment_frequency,
lastPageViewDate: workspace.lastPageviewDate,
pageViewsL30D: workspace.pageviewsLast30d,
pageViewsL7D: workspace.pageviewsLast7d,
pageViewsL24H: workspace.pageviewsLast24h,
totalEverActiveWorkspaceUsers: workspace.totalEverActiveUsers,
totalWorkspaceUsers: workspace.totalWorkspaceUsers,
activeUsersL30D: workspace.activeUsersLast30d,
activeUsersL7D: workspace.activeUsersLast7d,
activeUsersL24H: workspace.activeUsersLast24h,
isActiveL30D: workspace.isActiveLast30d,
isActiveL7D: workspace.isActiveLast7d,
isActiveL24H: workspace.isActiveLast24h,
workspaceTenure: workspace.workspaceAgeDays,
numberOfEventsTotal: workspace.totalEvents,
numberOfEventsL30D: workspace.eventsLast30d,
mrr: {
amountMicros: dollarsToAmountMicros(workspace.mrr),
currencyCode: 'USD',
},
potentialArr: {
amountMicros: dollarsToAmountMicros(workspace.potential_arr),
currencyCode: 'USD',
},
arr: {
amountMicros: dollarsToAmountMicros(workspace.arr ?? 0),
currencyCode: 'USD',
},
nextRenewalDate: clickHouseDateToIso(workspace.next_renewal_date),
creatorEmail: {
primaryEmail: workspace.creatorEmail,
},
workspaceBusinessDomain: {
primaryLinkUrl: workspace.primary_business_domain,
primaryLinkLabel: '',
},
dataLastUpdatedAt: new Date().toISOString(),
});
export const syncCloudWorkspaces = async (): Promise<{
syncedCount: number;
}> => {
const workspaces = await fetchWorkspacesFromClickHouse();
console.log(`Fetched ${workspaces.length} workspaces from ClickHouse`);
if (workspaces.length === 0) {
return { syncedCount: 0 };
}
const cloudWorkspaceInputs = workspaces.map(buildCloudWorkspaceInput);
console.log(
`Batch-upserting ${cloudWorkspaceInputs.length} cloud workspaces`,
);
await twentyClient.mutation({
createCloudWorkspaces2: {
__args: {
data: cloudWorkspaceInputs,
upsert: true,
},
__scalar: true,
},
});
return { syncedCount: workspaces.length };
};
@@ -0,0 +1,13 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { ALL_CLOUD_USER_2_VIEW_ID } from 'src/views/all-cloud-user-2';
export default defineNavigationMenuItem({
universalIdentifier: 'ac6df084-0f0f-404a-b3d1-b084cb624d76',
type: NavigationMenuItemType.VIEW,
name: 'cloud-user-2',
icon: 'IconList',
position: 0,
viewUniversalIdentifier: ALL_CLOUD_USER_2_VIEW_ID,
});
@@ -0,0 +1,13 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID } from 'src/views/all-cloud-user-workspace-2';
export default defineNavigationMenuItem({
universalIdentifier: 'e29251f0-78ad-493f-8f99-ec54ab0b5378',
type: NavigationMenuItemType.VIEW,
name: 'cloud-user-workspace-2',
icon: 'IconList',
position: 0,
viewUniversalIdentifier: ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID,
});
@@ -0,0 +1,13 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { ALL_CLOUD_WORKSPACE_2_VIEW_ID } from 'src/views/all-cloud-workspace-2';
export default defineNavigationMenuItem({
universalIdentifier: '0ba6315e-0d79-47a5-8c05-29dec621a647',
type: NavigationMenuItemType.VIEW,
name: 'cloud-workspace-2',
icon: 'IconList',
position: 0,
viewUniversalIdentifier: ALL_CLOUD_WORKSPACE_2_VIEW_ID,
});
@@ -0,0 +1,171 @@
import { defineObject, FieldType } from 'twenty-sdk';
import {
CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
CLOUD_USER_2_EMAIL_FIELD_ID,
CLOUD_USER_2_FULL_NAME_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
CLOUD_USER_2_IS_TWENTY_FIELD_ID,
CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
CLOUD_USER_2_UPDATED_BY_FIELD_ID,
CLOUD_USER_2_USER_TENURE_FIELD_ID,
CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
} from 'src/fields/cloud-user-2-field-ids';
export const CLOUD_USER_2_UNIVERSAL_IDENTIFIER =
'da264c72-df22-49b3-98e3-21cf6013e671';
export default defineObject({
universalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
nameSingular: 'cloudUser2',
namePlural: 'cloudUsers2',
labelSingular: 'Cloud user 2',
labelPlural: 'Cloud users 2',
icon: 'IconBox',
fields: [
{
universalIdentifier: CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
type: FieldType.SELECT,
name: 'activityStatus',
label: 'Activity Status',
options: [
{
id: '141d21f3-71b0-4b37-a3d2-5d91d43d0493',
value: 'ACTIVE',
label: 'Active',
position: 0,
color: 'green',
},
{
id: 'adb1d290-750f-468e-99ee-ed892bcb8974',
value: 'RECENT',
label: 'Recent',
position: 1,
color: 'blue',
},
{
id: '96f24b75-a671-4727-a78f-5a72419370ea',
value: 'DORMANT',
label: 'Dormant',
position: 2,
color: 'orange',
},
{
id: '88ddbef9-16a8-4643-9726-5b271cd477fa',
value: 'INACTIVE',
label: 'Inactive',
position: 3,
color: 'blue',
},
],
},
{
universalIdentifier: CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
type: FieldType.NUMBER,
name: 'avgDailyPageviewsLast30d',
label: 'Avg Daily Page Views',
},
{
universalIdentifier: CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
type: FieldType.DATE_TIME,
name: 'dataLastUpdatedAt',
label: 'Data last updated at',
},
{
universalIdentifier: CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
type: FieldType.NUMBER,
name: 'daysSinceLastActivity',
label: 'Days Since Last Activity',
},
{
universalIdentifier: CLOUD_USER_2_EMAIL_FIELD_ID,
type: FieldType.EMAILS,
name: 'email',
label: 'Email',
},
{
universalIdentifier: CLOUD_USER_2_FULL_NAME_FIELD_ID,
type: FieldType.FULL_NAME,
name: 'fullName',
label: 'Full Name',
},
{
universalIdentifier: CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL24h',
label: 'Is Active L24H',
defaultValue: true,
},
{
universalIdentifier: CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL30d',
label: 'Is Active L30D',
defaultValue: false,
},
{
universalIdentifier: CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL7d',
label: 'Is Active L7D',
defaultValue: true,
},
{
universalIdentifier: CLOUD_USER_2_IS_TWENTY_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isTwenty',
label: 'Is Twenty',
defaultValue: false,
},
{
universalIdentifier: CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
type: FieldType.DATE_TIME,
name: 'lastActivityDate',
label: 'Last Activity Date',
},
{
universalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL24h',
label: 'Page Views L24H',
},
{
universalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL30d',
label: 'Page Views L30D',
},
{
universalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL7d',
label: 'Page Views L7D',
},
{
universalIdentifier: CLOUD_USER_2_UPDATED_BY_FIELD_ID,
type: FieldType.ACTOR,
name: 'updatedBy',
label: 'Updated by',
},
{
universalIdentifier: CLOUD_USER_2_USER_TENURE_FIELD_ID,
type: FieldType.NUMBER,
name: 'userTenure',
label: 'User tenure',
},
{
universalIdentifier: CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
type: FieldType.NUMBER,
name: 'workspaceCount',
label: 'Workspace count',
},
],
});
@@ -0,0 +1,70 @@
import { defineObject, FieldType } from 'twenty-sdk';
import {
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
} from 'src/fields/cloud-user-workspace-2-field-ids';
export const CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER =
'14d6e1f4-c513-4766-9210-bc5dc8294e51';
export default defineObject({
universalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
nameSingular: 'cloudUserWorkspace2',
namePlural: 'cloudUserWorkspaces2',
labelSingular: 'Cloud user workspace 2',
labelPlural: 'Cloud user workspaces 2',
icon: 'IconBox',
fields: [
{
universalIdentifier:
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
type: FieldType.TEXT,
name: 'twentyUserIdentifier',
label: 'Twenty User Identifier',
icon: 'IconMan',
isNullable: true,
defaultValue: null,
universalSettings: {
displayedMaxRows: 0,
},
},
{
universalIdentifier:
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
type: FieldType.TEXT,
name: 'twentyWorkspaceIdentifier',
label: 'Twenty Workspace Identifier',
icon: 'IconScreenShare',
isNullable: true,
defaultValue: null,
universalSettings: {
displayedMaxRows: 0,
},
},
{
universalIdentifier:
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
type: FieldType.TEXT,
name: 'idOfTheUserWorkspace',
label: 'Id of the user workspace',
icon: 'IconTypography',
isNullable: true,
defaultValue: null,
universalSettings: {
displayedMaxRows: 0,
},
},
{
universalIdentifier: CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
type: FieldType.ACTOR,
name: 'updatedBy',
label: 'Updated by',
description: 'The user who last updated the record',
icon: 'IconUserCircle',
isNullable: true,
},
],
});
@@ -0,0 +1,582 @@
import { defineObject, FieldType } from 'twenty-sdk';
import {
CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
CLOUD_WORKSPACE_2_ARR_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
CLOUD_WORKSPACE_2_MRR_FIELD_ID,
CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
} from 'src/fields/cloud-workspace-2-field-ids';
export const CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER =
'77376ed3-19c1-4859-bde5-90f19ad02113';
export default defineObject({
universalIdentifier: CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
nameSingular: 'cloudWorkspace2',
namePlural: 'cloudWorkspaces2',
labelSingular: 'Cloud workspace 2',
labelPlural: 'Cloud workspaces 2',
icon: 'IconBox',
fields: [
{
universalIdentifier: CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
type: FieldType.LINKS,
name: 'customDomain',
label: 'Custom domain',
description: 'Custom domain set up by the customer to use their own.',
icon: 'IconLink',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
type: FieldType.LINKS,
name: 'workspaceBusinessDomain',
label: 'Workspace Business Domain',
icon: 'IconLink',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
type: FieldType.LINKS,
name: 'companyLinkedin',
label: 'Company LinkedIn',
icon: 'IconBrandLinkedin',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
type: FieldType.DATE,
name: 'dataLastUpdatedAt',
label: 'Data last updated at',
description: 'by workflow',
icon: 'IconCalendarEvent',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
type: FieldType.DATE,
name: 'lastPageViewDate',
label: 'Last Page View Date',
icon: 'IconCalendarEvent',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier:
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
type: FieldType.NUMBER,
name: 'totalEverActiveWorkspaceUsers',
label: 'Total ever active workspace users',
icon: 'IconUsers',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
type: FieldType.NUMBER,
name: 'activeUsersL30D',
label: 'Active Users L30D',
icon: 'IconUsersGroup',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
type: FieldType.NUMBER,
name: 'activeUsersL7D',
label: 'Active Users L7D',
icon: 'IconUsersGroup',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
type: FieldType.NUMBER,
name: 'activeUsersL24H',
label: 'Active Users L24H',
icon: 'IconUsersGroup',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
type: FieldType.NUMBER,
name: 'workspaceTenure',
label: 'Workspace tenure',
description: 'in days',
icon: 'IconCalendarHeart',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL30D',
label: 'Page Views L30D',
icon: 'IconClick',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL7D',
label: 'Page Views L7D',
icon: 'IconClick',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL24H',
label: 'Page Views L24H',
icon: 'IconClick',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
type: FieldType.NUMBER,
name: 'totalWorkspaceUsers',
label: 'Total workspace users',
icon: 'IconUsers',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
type: FieldType.NUMBER,
name: 'numberOfEventsL30D',
label: 'Number of events L30D',
icon: 'IconHandClick',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
type: FieldType.NUMBER,
name: 'numberOfEventsTotal',
label: 'Number of events total',
icon: 'IconHandClick',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
type: FieldType.NUMBER,
name: 'alexaRank',
label: 'Alexa Rank',
icon: 'IconNumber9',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
type: FieldType.NUMBER,
name: 'employees',
label: 'Employees',
icon: 'IconUsers',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL7D',
label: 'Is Active L7D',
icon: 'IconActivity',
isNullable: true,
defaultValue: false,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL24H',
label: 'Is Active L24H',
icon: 'IconActivity',
isNullable: true,
defaultValue: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL30D',
label: 'Is Active L30D',
icon: 'IconActivity',
isNullable: true,
defaultValue: false,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isEnriched',
label: 'Is Enriched',
icon: 'IconToggleLeft',
isNullable: true,
defaultValue: false,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
type: FieldType.TEXT,
name: 'industry',
label: 'Industry',
icon: 'IconWorldLongitude',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
type: FieldType.TEXT,
name: 'companyFoundedYear',
label: 'Company Founded Year',
icon: 'IconCalendarPlus',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
type: FieldType.TEXT,
name: 'description',
label: 'Description',
icon: 'IconBaselineDensitySmall',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
type: FieldType.TEXT,
name: 'latestFundingStage',
label: 'Latest Funding Stage',
icon: 'IconBellDollar',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
type: FieldType.TEXT,
name: 'companyName',
label: 'Company Name',
icon: 'IconTypography',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
type: FieldType.TEXT,
name: 'subDomain',
label: 'Sub domain',
description:
'This is the subdomain chosen by customers to access their workspace',
icon: 'IconLink',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
type: FieldType.CURRENCY,
name: 'annualRevenue',
label: 'Annual Revenue',
icon: 'IconCurrencyDollar',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_MRR_FIELD_ID,
type: FieldType.CURRENCY,
name: 'mrr',
label: 'MRR',
icon: 'IconMoneybag',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
type: FieldType.CURRENCY,
name: 'potentialArr',
label: 'Potential ARR',
icon: 'IconMoneybag',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ARR_FIELD_ID,
type: FieldType.CURRENCY,
name: 'arr',
label: 'ARR',
icon: 'IconMoneybag',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
type: FieldType.CURRENCY,
name: 'totalFunding',
label: 'Total Funding',
icon: 'IconMoneybag',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
type: FieldType.SELECT,
name: 'activationStatus',
label: 'Activation Status',
icon: 'IconTag',
isNullable: true,
defaultValue: null,
options: [
{
id: 'c41ee49c-e629-431e-8601-183726f93315',
color: 'yellow',
label: 'Pending creation',
value: 'PENDING_CREATION',
position: 0,
},
{
id: 'cb82ad14-5fcb-4ed0-af58-c92f84b45d2b',
color: 'gray',
label: 'Suspended',
value: 'SUSPENDED',
position: 1,
},
{
id: 'b1084bc8-0efd-44ce-974c-7fb44c310be5',
color: 'green',
label: 'Active',
value: 'ACTIVE',
position: 2,
},
{
id: 'b5b33c8e-eb3f-4758-99c9-d2f39c011b96',
color: 'blue',
label: 'Ongoing creation',
value: 'ONGOING_CREATION',
position: 3,
},
{
id: '2ae41a69-c521-4545-9e0b-f1f941ceee83',
color: 'purple',
label: 'Empty',
value: 'EMPTY',
position: 4,
},
],
},
{
universalIdentifier: CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
type: FieldType.SELECT,
name: 'subscriptionStatus',
label: 'Subscription Status',
icon: 'IconTag',
isNullable: true,
defaultValue: null,
options: [
{
id: 'b5ee9a91-0a15-4cbe-a36e-198f5bae431d',
color: 'green',
label: 'Active',
value: 'ACTIVE',
position: 0,
},
{
id: '78837243-38d9-44d4-8b9c-a6c0ca082ba8',
color: 'blue',
label: 'Trialing',
value: 'TRIALING',
position: 1,
},
{
id: '6b27ee33-3c30-4694-a607-19264765b11c',
color: 'gray',
label: 'Canceled',
value: 'CANCELED',
position: 2,
},
{
id: '05118231-18b6-4589-be23-8f1368c0182f',
color: 'yellow',
label: 'Other',
value: 'OTHER',
position: 3,
},
{
id: 'ea83a7ff-4fe0-4c56-88f8-cf0bdf76cb08',
color: 'gray',
label: 'Empty',
value: 'EMPTY',
position: 4,
},
],
},
{
universalIdentifier: CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
type: FieldType.SELECT,
name: 'paymentFrequency',
label: 'Payment Frequency',
icon: 'IconCurrencyDollar',
isNullable: true,
defaultValue: null,
options: [
{
id: 'fa97d4fb-12bf-4cf4-b252-14b1c657d3b6',
color: 'sky',
label: 'Month',
value: 'MONTH',
position: 0,
},
{
id: 'c09c3f1d-31e7-4b29-969a-a4304f04abe0',
color: 'purple',
label: 'Year',
value: 'YEAR',
position: 1,
},
{
id: 'a91d77ed-90ad-41fc-83ae-e688c981abb2',
color: 'gray',
label: 'Other',
value: 'OTHER',
position: 2,
},
{
id: '5c595203-1b33-430b-abd8-8bc80aeedbd7',
color: 'red',
label: 'Empty',
value: 'EMPTY',
position: 3,
},
],
},
{
universalIdentifier: CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
type: FieldType.DATE_TIME,
name: 'nextRenewalDate',
label: 'Next Renewal Date',
icon: 'IconCalendarClock',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
type: FieldType.EMAILS,
name: 'creatorEmail',
label: 'Creator Email',
icon: 'IconMail',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
type: FieldType.ARRAY,
name: 'tags',
label: 'Tags',
icon: 'IconBracketsContain',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
type: FieldType.ACTOR,
name: 'updatedBy',
label: 'Updated by',
description: 'The user who last updated the record',
icon: 'IconUserCircle',
isNullable: true,
},
],
});
@@ -0,0 +1,14 @@
import { defineRole } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'4bd740cc-f6a1-40c5-b46b-e2a4dbf5af04';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Twenty for Twenty default function role',
description: 'Twenty for Twenty default function role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
});
@@ -0,0 +1,25 @@
import { z } from 'zod';
const applicationConfigSchema = z
.object({
CLICKHOUSE_DATABASE: z.string().nonempty(),
CLICKHOUSE_URL: z.url(),
CLICKHOUSE_USERNAME: z.string().nonempty(),
CLICKHOUSE_PASSWORD: z.string().nonempty(),
});
export const getApplicationConfig = () => {
const env = applicationConfigSchema.parse({
CLICKHOUSE_DATABASE: process.env.CLICKHOUSE_DATABASE,
CLICKHOUSE_URL: process.env.CLICKHOUSE_URL,
CLICKHOUSE_USERNAME: process.env.CLICKHOUSE_USERNAME,
CLICKHOUSE_PASSWORD: process.env.CLICKHOUSE_PASSWORD,
});
return {
clickHouseDatabase: env.CLICKHOUSE_DATABASE,
clickHouseUrl: env.CLICKHOUSE_URL,
clickHouseUsername: env.CLICKHOUSE_USERNAME,
clickHousePassword: env.CLICKHOUSE_PASSWORD,
};
};
@@ -0,0 +1,43 @@
import { z } from 'zod';
import { getApplicationConfig } from 'src/shared/application-config';
// Fetches rows from ClickHouse using a raw SQL query,
// parses the JSONEachRow response, and validates each row against the provided Zod schema.
export const fetchFromClickHouse = async <T>(
query: string,
schema: z.ZodType<T>,
): Promise<T[]> => {
const { clickHouseUrl, clickHouseUsername, clickHousePassword } =
getApplicationConfig();
const response = await fetch(clickHouseUrl, {
method: 'POST',
headers: {
Authorization:
'Basic ' +
Buffer.from(`${clickHouseUsername}:${clickHousePassword}`).toString(
'base64',
),
'Content-Type': 'text/plain',
},
body: query,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`ClickHouse error: ${response.status} - ${errorText}`);
}
// Format is a list of JSON objects, one per line (JSONEachRow format).
const text = await response.text();
const rows = text
.trim()
.split('\n')
.filter((line) => line.trim())
.map((line) => JSON.parse(line));
return z.array(schema).parse(rows);
};
@@ -0,0 +1,16 @@
// ClickHouse returns DateTime64 values in the format 'YYYY-MM-DD HH:mm:ss.SSSSSS'
// (space-separated, up to 6 fractional digits). Twenty's DATE_TIME fields expect
// ISO 8601 format ('YYYY-MM-DDTHH:mm:ss.SSSZ', at most 3 fractional digits).
//
// Additionally, ClickHouse uses '1970-01-01 00:00:00.000000' as a sentinel for
// null/empty dates. We map that to null.
const CLICKHOUSE_EPOCH_SENTINEL = '1970-01-01 00:00:00.000000';
export const clickHouseDateToIso = (value: string): string | null => {
if (!value || value === CLICKHOUSE_EPOCH_SENTINEL) {
return null;
}
return new Date(value.replace(' ', 'T') + 'Z').toISOString();
};
@@ -0,0 +1,3 @@
import Twenty from 'twenty-sdk/generated';
export const twentyClient = new Twenty();
@@ -0,0 +1,159 @@
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
import {
CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
CLOUD_USER_2_EMAIL_FIELD_ID,
CLOUD_USER_2_FULL_NAME_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
CLOUD_USER_2_IS_TWENTY_FIELD_ID,
CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
CLOUD_USER_2_UPDATED_BY_FIELD_ID,
CLOUD_USER_2_USER_TENURE_FIELD_ID,
CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
} from 'src/fields/cloud-user-2-field-ids';
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
export const ALL_CLOUD_USER_2_VIEW_ID = 'd6137e11-dbcd-4824-85d3-42fe1ca48cb6';
export default defineView({
universalIdentifier: ALL_CLOUD_USER_2_VIEW_ID,
name: 'all-cloud-user-2',
objectUniversalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
type: ViewType.TABLE,
fields: [
{
universalIdentifier: '9e82305e-9fd9-4bf7-8cc5-098eb35a748e',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_FULL_NAME_FIELD_ID,
position: 0,
isVisible: true,
size: 180,
},
{
universalIdentifier: 'ea737839-bcb7-4da1-aaf6-79cda65e6263',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_EMAIL_FIELD_ID,
position: 1,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'ccd1eca7-c851-4e97-9f0e-48c7139f3ced',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
position: 2,
isVisible: true,
size: 130,
},
{
universalIdentifier: 'e99c82d3-d2fe-4758-9253-ab2ca2b50ef8',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_TWENTY_FIELD_ID,
position: 3,
isVisible: true,
size: 100,
},
{
universalIdentifier: 'c03da046-30b6-402e-8d4b-10799aa5102d',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
position: 4,
isVisible: true,
size: 130,
},
{
universalIdentifier: 'e3a787f2-77d1-4865-a5c2-c60976809d4d',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_USER_TENURE_FIELD_ID,
position: 5,
isVisible: true,
size: 110,
},
{
universalIdentifier: '352d77f2-726b-4a75-8c9d-3117cfabedc1',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
position: 6,
isVisible: true,
size: 120,
},
{
universalIdentifier: 'cf06d6b0-e425-4daf-8a6d-9b66a058ae80',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
position: 7,
isVisible: true,
size: 120,
},
{
universalIdentifier: '4ab63729-0b48-4b0b-ac9e-e72f5448998c',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
position: 8,
isVisible: true,
size: 120,
},
{
universalIdentifier: '8c6a3315-fa47-4a12-b9fe-1775462c803d',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
position: 9,
isVisible: true,
size: 130,
},
{
universalIdentifier: '89011d63-e670-498e-bc8f-142f7ceafd04',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
position: 10,
isVisible: true,
size: 130,
},
{
universalIdentifier: '88816c57-aa5b-4efe-b53a-b288fad24ab4',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
position: 11,
isVisible: true,
size: 130,
},
{
universalIdentifier: '3b0ff937-1eaf-4589-80ec-23abb4f2e204',
fieldMetadataUniversalIdentifier:
CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
position: 12,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'dee5f679-9ad9-4bc4-94a9-af28d7210699',
fieldMetadataUniversalIdentifier:
CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
position: 13,
isVisible: true,
size: 150,
},
{
universalIdentifier: '25b4e175-be18-4b20-9c51-5db0a596d968',
fieldMetadataUniversalIdentifier:
CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
position: 14,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'f1706c21-d164-414c-9cf5-4cf376584bb9',
fieldMetadataUniversalIdentifier:
CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
position: 15,
isVisible: true,
size: 150,
},
{
universalIdentifier: '88d0f343-42c1-4d36-82df-cfdb74e785eb',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_UPDATED_BY_FIELD_ID,
position: 16,
isVisible: true,
size: 150,
},
],
});
@@ -0,0 +1,56 @@
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
import {
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
} from 'src/fields/cloud-user-workspace-2-field-ids';
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
export const ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID =
'f80e44ef-43fb-409e-a003-decadaa38b3e';
export default defineView({
universalIdentifier: ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID,
name: 'all-cloud-user-workspace-2',
objectUniversalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
type: ViewType.TABLE,
fields: [
{
universalIdentifier: '4a0d9d93-d182-443d-8a70-3a4f25e51c50',
fieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
position: 0,
isVisible: true,
size: 200,
},
{
universalIdentifier: '14b78fc4-a6a5-471b-ba87-6aebc9a3da68',
fieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
position: 1,
isVisible: true,
size: 220,
},
{
universalIdentifier: '8c9c8a1b-d511-4706-afe7-32b22cce2e7e',
fieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
position: 2,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'ee35eded-4222-4a87-b31c-69e415e331db',
fieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
position: 3,
isVisible: true,
size: 150,
},
],
});
@@ -0,0 +1,369 @@
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
import {
CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
CLOUD_WORKSPACE_2_ARR_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
CLOUD_WORKSPACE_2_MRR_FIELD_ID,
CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
} from 'src/fields/cloud-workspace-2-field-ids';
import { CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-workspace-2';
export const ALL_CLOUD_WORKSPACE_2_VIEW_ID =
'3747a3a0-25a0-42f0-99d9-61a4b4a76009';
export default defineView({
universalIdentifier: ALL_CLOUD_WORKSPACE_2_VIEW_ID,
name: 'all-cloud-workspace-2',
objectUniversalIdentifier: CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
type: ViewType.TABLE,
fields: [
{
universalIdentifier: '146e3f00-8efc-46be-aafb-a6bafcefa5b0',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
position: 0,
isVisible: true,
size: 180,
},
{
universalIdentifier: '10ce2149-d25b-4e21-8215-c628677bc4cb',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
position: 1,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'c8735db0-9a89-4204-ad58-b68b71c10182',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
position: 2,
isVisible: true,
size: 250,
},
{
universalIdentifier: 'cf3f182b-1a69-4274-a200-ef191400a9e8',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
position: 3,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'c932adb5-129b-4457-bbfa-75baaf27707f',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
position: 4,
isVisible: true,
size: 110,
},
{
universalIdentifier: '5f0d33f0-60f0-442c-8c2d-19bfe1c9ea49',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
position: 5,
isVisible: true,
size: 150,
},
{
universalIdentifier: '904e4ecd-f997-42b6-933a-1174cf2bebdc',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
position: 6,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'f2d919e7-f782-4edb-ad1a-b99649fbbaf9',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_MRR_FIELD_ID,
position: 7,
isVisible: true,
size: 120,
},
{
universalIdentifier: '92afa154-72f2-4b01-8376-2f5311bb40f9',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_ARR_FIELD_ID,
position: 8,
isVisible: true,
size: 120,
},
{
universalIdentifier: 'c63bcd0a-f8d3-4c86-b9c0-6d638d7e9195',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
position: 9,
isVisible: true,
size: 130,
},
{
universalIdentifier: '8659ca48-23b0-4fb8-b588-0919169cae4d',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
position: 10,
isVisible: true,
size: 140,
},
{
universalIdentifier: '1524079b-0e7e-41a6-80bc-85bcdfcab537',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
position: 11,
isVisible: true,
size: 130,
},
{
universalIdentifier: '36a755ee-903c-4c61-b091-ebc0e687333d',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
position: 12,
isVisible: true,
size: 140,
},
{
universalIdentifier: '5ac28b38-823f-421c-a177-1749b8283381',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
position: 13,
isVisible: true,
size: 140,
},
{
universalIdentifier: '6fa0bfe5-ac55-4165-8830-5c084466c7f5',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
position: 14,
isVisible: true,
size: 140,
},
{
universalIdentifier: 'bee2ed03-23c2-4c99-86db-c0dd4599c669',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
position: 15,
isVisible: true,
size: 130,
},
{
universalIdentifier: 'e8f9775b-c984-4832-800a-3dd26060c0aa',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
position: 16,
isVisible: true,
size: 130,
},
{
universalIdentifier: '95c4fdfc-8fe5-43ce-9885-3f0fc5b4756a',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
position: 17,
isVisible: true,
size: 130,
},
{
universalIdentifier: '6448c0f1-1469-466e-99ed-c7aa015d9e38',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
position: 18,
isVisible: true,
size: 150,
},
{
universalIdentifier: '082ed200-9bba-444a-9b28-3a129b7ac9d8',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
position: 19,
isVisible: true,
size: 180,
},
{
universalIdentifier: 'd61bbf0c-48ad-428a-90a1-87b54a87bacf',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
position: 20,
isVisible: true,
size: 140,
},
{
universalIdentifier: '26058a5b-edde-4caa-a034-32b8edb54ce6',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
position: 21,
isVisible: true,
size: 120,
},
{
universalIdentifier: '36931d2b-f250-4325-a877-3285dd8a797e',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
position: 22,
isVisible: true,
size: 120,
},
{
universalIdentifier: 'c120fc92-4796-4cf4-866f-c1d8c4555321',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
position: 23,
isVisible: true,
size: 120,
},
{
universalIdentifier: 'a6674592-070b-41c1-baab-f360984328cc',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
position: 24,
isVisible: true,
size: 110,
},
{
universalIdentifier: 'd3701295-ef41-46c4-8dd3-b31c27663516',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
position: 25,
isVisible: true,
size: 180,
},
{
universalIdentifier: 'dd22ffe7-95ed-4c67-be33-c2d1592daa32',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
position: 26,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'c7a9bcf8-d41d-4ded-b680-868e470e3228',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
position: 27,
isVisible: true,
size: 180,
},
{
universalIdentifier: '398eb88f-0d12-446d-a486-d1fb262f82bb',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
position: 28,
isVisible: true,
size: 200,
},
{
universalIdentifier: '52d2a1b3-e2b2-4d6b-a5dd-ea8d63c7c369',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
position: 29,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'f74fed2c-94dc-4954-ac27-79acd7e04103',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
position: 30,
isVisible: true,
size: 150,
},
{
universalIdentifier: '666d0cea-e50b-4cc8-9ed5-5f5c0ec7fe7e',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
position: 31,
isVisible: true,
size: 150,
},
{
universalIdentifier: '60508519-5ac4-4f96-a30f-5c75d3ce9a93',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
position: 32,
isVisible: true,
size: 160,
},
{
universalIdentifier: '3b1ca2b6-31a1-4755-a9a4-07ee9a9b8a33',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
position: 33,
isVisible: true,
size: 160,
},
{
universalIdentifier: '47ba21ab-a5c5-42b9-b1e2-7a988ece154d',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
position: 34,
isVisible: true,
size: 110,
},
{
universalIdentifier: 'de5e2e42-2600-488c-81d0-f700e7dd0f67',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
position: 35,
isVisible: true,
size: 160,
},
{
universalIdentifier: 'd6bec718-8b50-41e2-95d7-521151e2df82',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
position: 36,
isVisible: true,
size: 160,
},
{
universalIdentifier: '9aaff045-64fa-4857-a8df-7394cf43a120',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
position: 37,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'f2cb82b9-ebb4-4258-971b-061d709f28e5',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
position: 38,
isVisible: true,
size: 150,
},
{
universalIdentifier: '7e631992-3ae6-478b-9874-67c3b211f05a',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
position: 39,
isVisible: true,
size: 150,
},
],
});
@@ -0,0 +1,31 @@
{
"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"]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
# Development infrastructure services only (Postgres + Redis).
# Use this when developing locally against the source code.
#
# Usage:
# docker compose -f docker-compose.dev.yml up -d
# docker compose -f docker-compose.dev.yml down # stop
# docker compose -f docker-compose.dev.yml down -v # stop + wipe data
name: twenty-dev
services:
db:
image: postgres:16
volumes:
- dev-db-data:/var/lib/postgresql/data
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: default
healthcheck:
test: pg_isready -U postgres -h localhost -d postgres
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
redis:
image: redis:7
ports:
- "6379:6379"
command: ["--maxmemory-policy", "noeviction"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
volumes:
dev-db-data:
@@ -38,7 +38,6 @@ services:
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
@@ -92,7 +91,6 @@ services:
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
@@ -61,7 +61,6 @@
"EMAIL_SMTP_NO_TLS": { "type": "boolean" },
"EMAIL_FROM_ADDRESS": { "type": "string" },
"EMAIL_FROM_NAME": { "type": "string" },
"EMAIL_SYSTEM_ADDRESS": { "type": "string" },
"IS_EMAIL_VERIFICATION_REQUIRED": { "type": "boolean" },
"EMAIL_VERIFICATION_TOKEN_EXPIRES_IN": { "type": "string" },
"PASSWORD_RESET_TOKEN_EXPIRES_IN": { "type": "string" }
+1 -4
View File
@@ -15,7 +15,6 @@ COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
COPY ./packages/twenty-standard-application/package.json /app/packages/twenty-standard-application/
# Install all dependencies
RUN yarn && yarn cache clean && npx nx reset
@@ -29,13 +28,11 @@ COPY ./packages/twenty-emails /app/packages/twenty-emails
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
COPY ./packages/twenty-standard-application /app/packages/twenty-standard-application
COPY ./packages/twenty-server /app/packages/twenty-server
RUN npx nx build twenty-standard-application
RUN npx nx run twenty-server:build
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-standard-application twenty-server
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-server
# Build the front
FROM common-deps AS twenty-front-build
@@ -9,7 +9,7 @@ The goal here is to have a consistent codebase, which is easy to read and easy t
For this, it's better to be a bit more verbose than to be too concise.
Always keep in mind that people read code more often than they write it, specially on an open source project, where anyone can contribute.
Always keep in mind that people read code more often than they write it, especially on an open source project, where anyone can contribute.
There are a lot of rules that are not defined here, but that are automatically checked by linters.
@@ -150,7 +150,7 @@ type MyType = {
### Use string literals instead of enums
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) are the go-to way to handle enum-like values in TypeScript. They are easier to extend with Pick and Omit, and offer a better developer experience, specially with code completion.
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) are the go-to way to handle enum-like values in TypeScript. They are easier to extend with Pick and Omit, and offer a better developer experience, especially with code completion.
You can see why TypeScript recommends avoiding enums [here](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums).
@@ -288,4 +288,3 @@ An Oxlint rule, `typescript/consistent-type-imports`, enforces the no-type impor
Please note that this rule specifically addresses rare edge cases where unintentional type imports occur. TypeScript itself discourages this practice, as mentioned in the [TypeScript 3.8 release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html). In most situations, you should not need to use type-only imports.
To ensure your code complies with this rule, make sure to run Oxlint as part of your development workflow.
@@ -7,7 +7,7 @@ description: "The guide for contributors (or curious developers) who want to run
## Prerequisites
<Tabs>
<Tab title="Linux and MacOS">
<Tab title="Linux and macOS">
Before you can install and use Twenty, make sure you install the following on your computer:
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
@@ -31,7 +31,7 @@ wsl --install
```
You should now see a prompt to restart your computer. If not, restart it manually.
Upon restart, a powershell window will open and install Ubuntu. This may take up some time.
Upon restart, a PowerShell window will open and install Ubuntu. This may take up some time.
You'll see a prompt to create a username and password for your Ubuntu installation.
2. Install and configure git
@@ -104,7 +104,7 @@ You should run all commands in the following steps from the root of the project.
<Tabs>
<Tab title="Linux">
**Option 1 (preferred):** To provision your database locally:
Use the following link to install Postgresql on your Linux machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
Use the following link to install PostgreSQL on your Linux machine: [PostgreSQL Installation](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
@@ -131,7 +131,7 @@ You should run all commands in the following steps from the root of the project.
```
The installer might not create the `postgres` user by default when installing
via Homebrew on MacOS. Instead, it creates a PostgreSQL role that matches your macOS
via Homebrew on macOS. Instead, it creates a PostgreSQL role that matches your macOS
username (e.g., "john").
To check and create the `postgres` user if necessary, follow these steps:
```bash
@@ -174,8 +174,8 @@ You should run all commands in the following steps from the root of the project.
<Tab title="Windows (WSL)">
All the following steps are to be run in the WSL terminal (within your virtual machine)
**Option 1:** To provision your Postgresql locally:
Use the following link to install Postgresql on your Linux virtual machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
**Option 1:** To provision your PostgreSQL locally:
Use the following link to install PostgreSQL on your Linux virtual machine: [PostgreSQL Installation](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
@@ -190,10 +190,12 @@ You should run all commands in the following steps from the root of the project.
</Tab>
</Tabs>
You can now access the database at [localhost:5432](localhost:5432), with user `postgres` and password `postgres` .
You can now access the database at `localhost:5432`.
If you used the Docker option above, the default credentials are user `postgres` and password `postgres`. For native PostgreSQL installations, use the credentials and roles configured on your machine.
## Step 4: Set up a Redis Database (cache)
Twenty requires a redis cache to provide the best performance
Twenty requires a Redis cache to provide the best performance.
<Tabs>
<Tab title="Linux">
@@ -210,8 +212,10 @@ Twenty requires a redis cache to provide the best performance
```bash
brew install redis
```
Start your redis server:
```brew services start redis```
Start your Redis server:
```bash
brew services start redis
```
**Option 2:** If you have docker installed:
```bash
@@ -229,11 +233,11 @@ Twenty requires a redis cache to provide the best performance
</Tab>
</Tabs>
If you need a Client GUI, we recommend [redis insight](https://redis.io/insight/) (free version available)
If you need a client GUI, we recommend [Redis Insight](https://redis.io/insight/) (free version available).
## Step 5: Setup environment variables
## Step 5: Set up environment variables
Use environment variables or `.env` files to configure your project. More info [here](/developers/self-host/capabilities/setup)
Use environment variables or `.env` files to configure your project. More info [here](/developers/self-host/capabilities/setup).
Copy the `.env.example` files in `/front` and `/server`:
```bash
@@ -63,6 +63,12 @@ yarn twenty function:execute --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
# Build the app for distribution
yarn twenty app:build
# Publish the app to npm or a Twenty server
yarn twenty app:publish
# Uninstall the application from the current workspace
yarn twenty app:uninstall
@@ -1224,6 +1230,113 @@ Key points:
Explore a minimal, end-to-end example that demonstrates objects, logic functions, front components, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Building your app
Once you've developed your app with `app:dev`, use `app:build` to compile it into a distributable package.
```bash filename="Terminal"
# Build the app (output goes to .twenty/output/)
yarn twenty app:build
# Build and create a tarball (.tgz) for distribution
yarn twenty app:build --tarball
```
The build process:
1. **Parses and validates the manifest** — reads all `defineX()` entities from your source files and validates the manifest structure.
2. **Compiles logic functions and front components** — bundles TypeScript sources into ESM `.mjs` files using esbuild.
3. **Generates checksums** — computes MD5 hashes for each built file, stored in the manifest as `builtHandlerChecksum` / `builtComponentChecksum`.
4. **Generates the typed API client** — introspects the GraphQL schema and generates typed `CoreApiClient` and `MetadataApiClient` clients.
5. **Runs a TypeScript type check** — runs `tsc --noEmit` to catch type errors before publishing.
6. **Rebuilds with the generated client** — performs a second compilation pass so the generated client types are included.
7. **Optionally creates a tarball** — if `--tarball` is passed, runs `npm pack` to create a `.tgz` file ready for distribution.
The build output in `.twenty/output/` contains:
```text
.twenty/output/
├── manifest.json # Manifest with checksums for all built files
├── package.json # Copied from app root
├── yarn.lock # Copied from app root
├── src/
│ ├── logic-functions/ # Compiled .mjs logic function files
│ └── front-components/ # Compiled .mjs front component files
├── public/ # Static assets (if any)
└── my-app-1.0.0.tgz # Only with --tarball flag
```
| Option | Description |
|--------|-------------|
| `[appPath]` | Path to the app directory (defaults to current directory) |
| `--tarball` | Also pack the output into a `.tgz` tarball |
## Publishing your app
Use `app:publish` to distribute your app — either to the npm registry or directly to a Twenty server.
### Publish to npm (default)
```bash filename="Terminal"
# Publish to npm (requires npm login)
yarn twenty app:publish
# Publish with a dist-tag (e.g. beta, next)
yarn twenty app:publish --tag beta
```
This builds the app and runs `npm publish` from the `.twenty/output/` directory. The published package can then be installed from the Twenty marketplace by any workspace.
### Publish to a Twenty server
```bash filename="Terminal"
# Publish directly to a Twenty server
yarn twenty app:publish --server https://app.twenty.com
```
This builds the app with a tarball, uploads it to the server via the `uploadAppTarball` GraphQL mutation, and triggers installation in one step. This is useful for private deployments or testing against a specific server.
| Option | Description |
|--------|-------------|
| `[appPath]` | Path to the app directory (defaults to current directory) |
| `--server <url>` | Publish to a Twenty server instead of npm |
| `--token <token>` | Authentication token for the target server |
| `--tag <tag>` | npm dist-tag (e.g. `beta`, `next`) — only for npm publish |
## Application registration
Before an app can be installed in a workspace, it must be **registered**. A registration is a metadata record that describes where the app comes from and how to authenticate it. This is handled automatically by the CLI in most cases.
### Source types
Each registration has a **source type** that determines how the app's files are resolved during installation:
| Source type | How files are resolved | Typical use case |
|-------------|----------------------|------------------|
| `LOCAL` | Files are synced in real-time by the CLI watcher — installation is skipped | Development with `app:dev` |
| `NPM` | Fetched from the npm registry via the `sourcePackage` field | Published apps on npm |
| `TARBALL` | Extracted from an uploaded `.tgz` file stored on the server | Private apps published with `--server` |
### How registration happens
- **`app:dev`** — automatically creates a `LOCAL` registration the first time you run dev mode against a workspace.
- **`app:publish --server`** — uploads a tarball and creates (or updates) a `TARBALL` registration, then installs the app.
- **npm marketplace** — `NPM` registrations are created when apps are synced from the npm registry into the Twenty marketplace catalog.
- **GraphQL API** — you can also create registrations programmatically via the `createApplicationRegistration` mutation.
### Registration vs installation
**Registration** and **installation** are separate concepts:
- A **registration** (`ApplicationRegistration`) is a global metadata record describing the app: its name, source type, OAuth credentials, and marketplace listing status. It exists independently of any workspace.
- An **installation** (`Application`) is a per-workspace instance. When a user installs an app, Twenty resolves the package from the registration's source, writes the built files to storage, and synchronizes the manifest (creating objects, fields, logic functions, etc.) in that workspace.
One registration can be installed in many workspaces. Each workspace gets its own copy of the app's files and data model.
### OAuth credentials
Each registration includes OAuth credentials (`oAuthClientId` and `oAuthClientSecret`) generated at creation time. These are used by the app to authenticate API requests on behalf of users. The client secret is returned **once** at creation — store it securely. You can rotate it later via the `rotateApplicationRegistrationClientSecret` mutation.
## Manual setup (without the scaffolder)
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire a single script in your package.json:
@@ -4,7 +4,7 @@ title: 1-Click w/ Docker Compose
<Warning>
Docker containers are for production hosting or self-hosting, for the contribution please check the [Local Setup](/developers/contribute/capabilities/local-setup).
Docker containers are for production hosting or self-hosting. For contributing, please check the [Local Setup](/developers/contribute/capabilities/local-setup).
</Warning>
## Overview
@@ -13,7 +13,7 @@ This guide provides step-by-step instructions to install and configure the Twent
**Important:** Only modify settings explicitly mentioned in this guide. Altering other configurations may lead to issues.
See docs [Setup Environment Variables](/developers/self-host/capabilities/setup) for advanced configuration. All environment variables must be declared in the docker-compose.yml file at the server and / or worker level depending on the variable.
See [Setup Environment Variables](/developers/self-host/capabilities/setup) for advanced configuration. All environment variables must be declared in the `docker-compose.yml` file at the server and/or worker level, depending on the variable.
## System Requirements
@@ -237,4 +237,3 @@ docker compose up -d
If you encounter any problem, check [Troubleshooting](/developers/self-host/capabilities/troubleshooting) for solutions.
@@ -289,43 +289,57 @@ yarn command:prod cron:workflow:automated-cron-trigger
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
</Warning>
## Logic Functions
## Logic Functions & Code Interpreter
Twenty supports logic functions for workflows and custom logic. The execution environment is configured via the `SERVERLESS_TYPE` environment variable.
Twenty supports logic functions for workflows and the code interpreter for AI data analysis. Both run user-provided code and require explicit configuration for security.
### Security Defaults
**In production (NODE_ENV=production):** Both logic functions and code interpreter default to **Disabled**. You must explicitly enable them with `LOGIC_FUNCTION_TYPE` and `CODE_INTERPRETER_TYPE` if you need these features.
**In development (NODE_ENV=development):** Both default to **LOCAL** for convenience when running locally.
<Warning>
**Security Notice:** The local driver (`SERVERLESS_TYPE=LOCAL`) runs code directly on the host in a Node.js process with no sandboxing. It should only be used for trusted code in development. For production deployments handling untrusted code, we highly recommend using `SERVERLESS_TYPE=LAMBDA` or `SERVERLESS_TYPE=DISABLED`.
**Security Notice:** The local driver (`LOGIC_FUNCTION_TYPE=LOCAL` or `CODE_INTERPRETER_TYPE=LOCAL`) runs code directly on the host in a Node.js process with no sandboxing. It should only be used for trusted code in development. For production deployments handling untrusted code, use `LOGIC_FUNCTION_TYPE=LAMBDA` or `CODE_INTERPRETER_TYPE=E2B` (with sandboxing), or keep them disabled.
</Warning>
### Available Drivers
### Logic Functions - Available Drivers
| Driver | Environment Variable | Use Case | Security Level |
|--------|---------------------|----------|----------------|
| Disabled | `SERVERLESS_TYPE=DISABLED` | Disable logic functions entirely | N/A |
| Local | `SERVERLESS_TYPE=LOCAL` | Development and trusted environments | Low (no sandboxing) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Production with untrusted code | High (hardware-level isolation) |
| Disabled | `LOGIC_FUNCTION_TYPE=DISABLED` | Disable logic functions entirely | N/A |
| Local | `LOGIC_FUNCTION_TYPE=LOCAL` | Development and trusted environments | Low (no sandboxing) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Production with untrusted code | High (hardware-level isolation) |
### Recommended Configuration
### Logic Functions - Recommended Configuration
**For development:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**For production (AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**To disable logic functions:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Code Interpreter - Available Drivers
| Driver | Environment Variable | Use Case | Security Level |
|--------|---------------------|----------|----------------|
| Disabled | `CODE_INTERPRETER_TYPE=DISABLED` | Disable AI code execution | N/A |
| Local | `CODE_INTERPRETER_TYPE=LOCAL` | Development only | Low (no sandboxing) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Production with sandboxed execution | High (isolated sandbox) |
<Note>
When using `SERVERLESS_TYPE=DISABLED`, any attempt to execute a logic function will return an error. This is useful if you want to run Twenty without logic function capabilities.
When using `LOGIC_FUNCTION_TYPE=DISABLED` or `CODE_INTERPRETER_TYPE=DISABLED`, any attempt to execute will return an error. This is useful if you want to run Twenty without these capabilities.
</Note>
+4
View File
@@ -6302,6 +6302,10 @@
"source": "/developers/extend/capabilities/apps",
"destination": "/developers/extend/apps/getting-started"
},
{
"source": "/developers/extend/mcp",
"destination": "/user-guide/ai/capabilities/mcp"
},
{
"source": "/developers/local-setup",
"destination": "/developers/contribute/capabilities/local-setup"
@@ -8,7 +8,7 @@ title: دليل الأسلوب
لهذا، من الأفضل أن تكون تفصيلًا أكثر قليلاً بدلاً من أن تكون موجزًا للغاية.
دائمًا ضع في اعتبارك أن الناس يقرؤون التعليمات البرمجية أكثر مما يكتبونها، وخاصة في المشاريع مفتوحة المصدر، حيث يمكن لأي شخص المساهمة.
دائمًا ضع في اعتبارك أن الناس يقرؤون التعليمات البرمجية أكثر مما يكتبونها، وخاصة في مشروع مفتوح المصدر، حيث يمكن لأي شخص المساهمة.
هناك العديد من القواعد التي لم يتم تعريفها هنا، ولكن يتم التحقق منها تلقائيًا بواسطة أدوات الفحص.
@@ -6,7 +6,7 @@ description: الدليل للمساهمين (أو المطورين الفضول
## المتطلبات الأساسية
<Tabs>
<Tab title="Linux و MacOS">
<Tab title="Linux و macOS">
قبل أن تتمكن من تثبيت واستخدام Twenty، تأكد من تثبيت الأمور التالية على جهاز الكمبيوتر الخاص بك:
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
@@ -103,7 +103,7 @@ cd twenty
<Tabs>
<Tab title="Linux">
**الخيار 1 (المفضل):** لتوفير قاعدة بياناتك محليًا:
استخدم الرابط التالي لتثبيت Postgresql على جهاز Linux الخاص بك: [تثبيت Postgresql](https://www.postgresql.org/download/linux/)
استخدم الرابط التالي لتثبيت PostgreSQL على جهاز Linux الخاص بك: [تثبيت PostgreSQL](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
@@ -129,8 +129,8 @@ cd twenty
brew services list
```
المثبت قد لا ينشئ المستخدم `postgres` افتراضيًا عند التثبيت
عبر Homebrew على MacOS. بدلاً من ذلك، فإنه ينشئ دور PostgreSQL يطابق
قد لا يقوم المُثبِّت بإنشاء المستخدم `postgres` افتراضيًا عند التثبيت
عبر Homebrew على macOS. بدلاً من ذلك، فإنه ينشئ دور PostgreSQL يطابق
اسم المستخدم الخاص بك في MacOS (مثل "john").
للتحقق وإنشاء المستخدم `postgres` إذا لزم الأمر، اتبع هذه الخطوات:
```bash
@@ -173,8 +173,8 @@ cd twenty
<Tab title="ويندوز (WSL)">
يجب أن تُنفذ جميع الخطوات التالية في تيرمينال WSL (داخل جهازك الافتراضي)
**الخيار 1:** لتوفير قاعدة بيانات Postgresql الخاصة بك محليًا:
استخدم الرابط التالي لتثبيت Postgresql على جهاز Linux الافتراضي الخاص بك: [تثبيت Postgresql](https://www.postgresql.org/download/linux/)
**الخيار 1:** لتوفير قاعدة بيانات PostgreSQL الخاصة بك محليًا:
استخدم الرابط التالي لتثبيت PostgreSQL على جهاز Linux الافتراضي الخاص بك: [تثبيت PostgreSQL](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
@@ -189,11 +189,13 @@ cd twenty
</Tab>
</Tabs>
يمكنك الآن الوصول إلى قاعدة البيانات على [localhost:5432](localhost:5432)، مع المستخدم `postgres` وكلمة المرور `postgres`.
يمكنك الآن الوصول إلى قاعدة البيانات على `localhost:5432`.
إذا استخدمت خيار Docker أعلاه، فإن بيانات الاعتماد الافتراضية هي اسم المستخدم `postgres` وكلمة المرور `postgres`. بالنسبة لتثبيتات PostgreSQL الأصلية، استخدم بيانات الاعتماد والأدوار المُكوَّنة على جهازك.
## الخطوة 4: إعداد قاعدة بيانات Redis (للتخزين المؤقت)
يتطلب Twenty مخزن بيانات Redis لتقديم أفضل أداء
يتطلب Twenty مخزن بيانات Redis لتقديم أفضل أداء.
<Tabs>
<Tab title="Linux">
@@ -210,8 +212,10 @@ cd twenty
```bash
brew install redis
```
ابدأ خادم redis الخاص بك:
`brew services start redis`
ابدأ تشغيل خادم Redis:
```bash
brew services start redis
```
**الخيار 2:** إذا كنت قد قمت بتثبيت docker:
```bash
@@ -229,11 +233,11 @@ cd twenty
</Tab>
</Tabs>
إذا كنت بحاجة إلى واجهة رسومية للعميل، نوصي بـ [redis insight](https://redis.io/insight/) (يتوفر إصدار مجاني)
إذا كنت بحاجة إلى واجهة رسومية للعميل، نوصي بـ [Redis Insight](https://redis.io/insight/) (يتوفر إصدار مجاني).
## الخطوة 5: إعداد متغيرات البيئة
استخدم متغيرات البيئة أو ملفات `.env` لتكوين مشروعك. المزيد من المعلومات [هنا](/l/ar/developers/self-host/capabilities/setup)
استخدم متغيرات البيئة أو ملفات `.env` لتكوين مشروعك. المزيد من المعلومات [هنا](/l/ar/developers/self-host/capabilities/setup).
انسخ ملفات `.env.example` الموجودة في `/front` و`/server`:
@@ -64,6 +64,12 @@ yarn twenty function:execute --preInstall
# نفّذ دالة ما بعد التثبيت
yarn twenty function:execute --postInstall
# ابنِ التطبيق للتوزيع
yarn twenty app:build
# انشر التطبيق إلى npm أو إلى خادم Twenty
yarn twenty app:publish
# أزل تثبيت التطبيق من مساحة العمل الحالية
yarn twenty app:uninstall
@@ -1240,6 +1246,113 @@ uploadFile(
استكشف مثالًا بسيطًا شاملًا من البداية إلى النهاية يوضح الكائنات والوظائف المنطقية والمكوّنات الأمامية ومشغّلات متعددة [هنا](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## بناء تطبيقك
بمجرد أن تطوّر تطبيقك باستخدام `app:dev`، استخدم `app:build` لإنشاء حزمة قابلة للتوزيع منه.
```bash filename="Terminal"
# ابنِ التطبيق (الإخراج يذهب إلى .twenty/output/)
yarn twenty app:build
# ابنِ وأنشئ ملف tarball (.tgz) للتوزيع
yarn twenty app:build --tarball
```
عملية البناء:
1. **يقوم بتحليل ملف البيان والتحقق من صحته** — يقرأ جميع الكيانات `defineX()` من ملفات المصدر لديك ويُتحقّق من بنية ملف البيان.
2. **يُصرِّف دوال المنطق ومكوّنات الواجهة** — يُجمّع مصادر TypeScript إلى ملفات ESM `.mjs` باستخدام esbuild.
3. **يولّد قيم التحقّق** — يحسب تجزئات MD5 لكل ملف مُبنًى، وتُخزَّن في ملف البيان كـ `builtHandlerChecksum` / `builtComponentChecksum`.
4. **ينشئ عميل API مضبوط الأنواع** — يفحص مخطط GraphQL ويُنشئ عميلَي `CoreApiClient` و`MetadataApiClient` مضبوطي الأنواع.
5. **يشغّل فحص الأنواع لـ TypeScript** — يشغّل `tsc --noEmit` لاكتشاف أخطاء الأنواع قبل النشر.
6. **يعيد البناء باستخدام العميل المُولَّد** — يُجري مرحلة ترجمة ثانية بحيث تُدرَج أنواع العميل المُولَّد.
7. **ينشئ أرشيف tar اختياريًا** — إذا تم تمرير `--tarball`، يشغّل `npm pack` لإنشاء ملف `.tgz` جاهز للتوزيع.
مخرجات البناء في `.twenty/output/` تتضمّن:
```text
.twenty/output/
├── manifest.json # Manifest with checksums for all built files
├── package.json # Copied from app root
├── yarn.lock # Copied from app root
├── src/
│ ├── logic-functions/ # Compiled .mjs logic function files
│ └── front-components/ # Compiled .mjs front component files
├── public/ # Static assets (if any)
└── my-app-1.0.0.tgz # Only with --tarball flag
```
| الخيار | الوصف |
| ----------- | -------------------------------------------------- |
| `[appPath]` | المسار إلى دليل التطبيق (افتراضيًا: الدليل الحالي) |
| `--tarball` | قم أيضًا بحزم المخرجات في أرشيف `.tgz` |
## نشر تطبيقك
استخدم `app:publish` لتوزيع تطبيقك — إما إلى سجل npm أو مباشرةً إلى خادم Twenty.
### النشر إلى npm (الإعداد الافتراضي)
```bash filename="Terminal"
# Publish to npm (requires npm login)
yarn twenty app:publish
# Publish with a dist-tag (e.g. beta, next)
yarn twenty app:publish --tag beta
```
يقوم هذا ببناء التطبيق وتشغيل `npm publish` من دليل `.twenty/output/`. بعد ذلك يمكن تثبيت الحزمة المنشورة من سوق Twenty بواسطة أي مساحة عمل.
### النشر إلى خادم Twenty
```bash filename="Terminal"
# Publish directly to a Twenty server
yarn twenty app:publish --server https://app.twenty.com
```
يقوم هذا ببناء التطبيق مع أرشيف tar، ويرفعه إلى الخادم عبر العملية `uploadAppTarball` في GraphQL، ويبدأ التثبيت في خطوة واحدة. يكون هذا مفيدًا لعمليات النشر الخاصة أو للاختبار مقابل خادم محدّد.
| الخيار | الوصف |
| ----------------- | -------------------------------------------------------- |
| `[appPath]` | المسار إلى دليل التطبيق (افتراضيًا: الدليل الحالي) |
| `--server <url>` | انشر إلى خادم Twenty بدلًا من npm |
| `--token <token>` | رمز المصادقة للخادم المستهدف |
| `--tag <tag>` | علامة توزيع npm (مثل `beta`، `next`) — للنشر عبر npm فقط |
## تسجيل التطبيق
قبل أن يمكن تثبيت تطبيق في مساحة عمل، يجب أن يكون **مسجّلًا**. التسجيل هو سجل بيانات وصفية يوضّح مصدر التطبيق وكيفية مصادقته. يُعالَج هذا تلقائيًا بواسطة CLI في معظم الحالات.
### أنواع المصادر
لكل تسجيل **نوع مصدر** يحدّد كيفية تحديد ملفات التطبيق أثناء التثبيت:
| نوع المصدر | كيفية تحديد الملفات | حالة الاستخدام النموذجية |
| ---------- | ------------------------------------------------------------------------- | --------------------------------------- |
| `LOCAL` | تتم مزامنة الملفات في الوقت الفعلي بواسطة مُراقِب CLI — يتم تخطّي التثبيت | التطوير باستخدام `app:dev` |
| `NPM` | تُجلب من سجل npm عبر الحقل `sourcePackage` | تطبيقات منشورة على npm |
| `TARBALL` | تُستخرَج من ملف `.tgz` مرفوع ومخزَّن على الخادم | تطبيقات خاصة منشورة باستخدام `--server` |
### كيفية إجراء التسجيل
* **`app:dev`** — ينشئ تلقائيًا تسجيلًا من نوع `LOCAL` في المرة الأولى التي تشغّل فيها وضع التطوير لمساحة عمل.
* **`app:publish --server`** — يرفع أرشيف tar وينشئ (أو يحدّث) تسجيلًا من نوع `TARBALL`، ثم يثبّت التطبيق.
* **سوق npm** — يتم إنشاء تسجيلات `NPM` عند مزامنة التطبيقات من سجل npm إلى كتالوج سوق Twenty.
* **واجهة برمجة تطبيقات GraphQL** — يمكنك أيضًا إنشاء التسجيلات برمجيًا عبر العملية `createApplicationRegistration`.
### التسجيل مقابل التثبيت
**التسجيل** و**التثبيت** مفهومان منفصلان:
* **التسجيل** (`ApplicationRegistration`) هو سجل بيانات وصفية عام يصف التطبيق: اسمه، نوع المصدر، بيانات اعتماد OAuth، وحالة إدراجه في السوق. وهو موجود بشكل مستقل عن أي مساحة عمل.
* **التثبيت** (`Application`) هو مثيل لكل مساحة عمل. عند قيام مستخدم بتثبيت تطبيق، تقوم Twenty بحلّ الحزمة من مصدر التسجيل، وتكتب الملفات المُبنَاة إلى التخزين، وتزامن البيان التعريفي (إنشاء الكائنات والحقول ودوال المنطق، إلخ) في مساحة العمل تلك.
يمكن تثبيت تسجيل واحد في العديد من مساحات العمل. تحصل كل مساحة عمل على نسختها الخاصة من ملفات التطبيق ونموذج البيانات.
### بيانات اعتماد OAuth
يتضمن كل تسجيل بيانات اعتماد OAuth (`oAuthClientId` و`oAuthClientSecret`) يتم إنشاؤها وقت الإنشاء. يستخدمها التطبيق لمصادقة طلبات واجهة برمجة التطبيقات بالنيابة عن المستخدمين. يُعرَض سر العميل مرةً **واحدة** عند الإنشاء — خزّنه بأمان. يمكنك تدويره لاحقًا عبر العملية `rotateApplicationRegistrationClientSecret`.
## إعداد يدوي (بدون المهيئ)
بينما نوصي باستخدام `create-twenty-app` للحصول على أفضل تجربة للبدء، يمكنك أيضًا إعداد مشروع يدويًا. لا تثبّت CLI عالميًا. بدل ذلك، أضف `twenty-sdk` كاعتماد محلي واربط سكربتًا واحدًا في ملف package.json لديك:
@@ -3,7 +3,7 @@ title: بنقرة واحدة مع Docker Compose
---
<Warning>
الحاويات الخاصة بدوكر مخصصة للاستضافة الإنتاجية أو الاستضافة الذاتية، للتحقيق يرجى التحقق من [الإعداد المحلي](/l/ar/developers/contribute/capabilities/local-setup).
حاويات Docker مخصصة للاستضافة في بيئة الإنتاج أو للاستضافة الذاتية. للمساهمة، يُرجى الاطلاع على [الإعداد المحلي](/l/ar/developers/contribute/capabilities/local-setup).
</Warning>
## نظرة عامة
@@ -12,7 +12,7 @@ title: بنقرة واحدة مع Docker Compose
**مهم:** عدّل الإعدادات المذكورة صراحة في هذا الدليل فقط. قد يؤدي تعديل التكوينات الأخرى إلى مشاكل.
راجع المستندات الخاصة بـ [إعداد متغيرات البيئة](/l/ar/developers/self-host/capabilities/setup) لإعداد متقدم. يجب إعلان جميع متغيرات البيئة في الملف docker-compose.yml على مستوى الخادم و/أو العامل بناءً على المتغير.
راجع [إعداد متغيرات البيئة](/l/ar/developers/self-host/capabilities/setup) لإعداد متقدم. يجب إعلان جميع متغيرات البيئة في ملف `docker-compose.yml` على مستوى الخادم و/أو العامل، اعتمادًا على المتغير.
## متطلبات النظام
@@ -297,46 +297,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
**وضع بيئي فقط:** إذا كنت قد ضبطت `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`، فأضف هذه المتغيرات إلى ملف `.env` الخاص بك بدلاً من ذلك.
</Warning>
## الوظائف المنطقية
## الوظائف المنطقية ومفسر الشيفرة
تدعم Twenty الوظائف المنطقية لعمليات سير العمل والمنطق المخصص. يتم تكوين بيئة التنفيذ عبر متغير البيئة `SERVERLESS_TYPE`.
تدعم Twenty الوظائف المنطقية لعمليات سير العمل ومفسر الشيفرة لتحليل بيانات الذكاء الاصطناعي. كلاهما يقوم بتشغيل الشيفرة المقدمة من المستخدم ويتطلب تهيئة صريحة لأغراض الأمان.
### الإعدادات الافتراضية للأمان
**في بيئة الإنتاج (NODE_ENV=production):** يكون الإعداد الافتراضي لكل من الوظائف المنطقية ومفسر الشيفرة هو **معطل**. يجب تمكينهما صراحة باستخدام `LOGIC_FUNCTION_TYPE` و`CODE_INTERPRETER_TYPE` إذا كنت تحتاج إلى هذه الميزات.
**في بيئة التطوير (NODE_ENV=development):** يكون الإعداد الافتراضي لكليهما **LOCAL** لتسهيل التشغيل محلياً.
<Warning>
**ملاحظة أمنية:** يقوم برنامج التشغيل المحلي (`SERVERLESS_TYPE=LOCAL`) بتشغيل الشيفرة مباشرةً على المضيف ضمن عملية Node.js من دون عزل. يجب استخدامه فقط للشيفرة الموثوقة أثناء التطوير. بالنسبة لعمليات النشر الإنتاجية التي تتعامل مع شيفرة غير موثوق بها، نوصي بشدة باستخدام `SERVERLESS_TYPE=LAMBDA` أو `SERVERLESS_TYPE=DISABLED`.
**ملاحظة أمنية:** يقوم برنامج التشغيل المحلي (`LOGIC_FUNCTION_TYPE=LOCAL` أو `CODE_INTERPRETER_TYPE=LOCAL`) بتشغيل الشيفرة مباشرة على المضيف ضمن عملية Node.js من دون عزل. يجب استخدامه فقط للشيفرة الموثوقة أثناء التطوير. لعمليات النشر الإنتاجية التي تتعامل مع شيفرة غير موثوقة، استخدم `LOGIC_FUNCTION_TYPE=LAMBDA` أو `CODE_INTERPRETER_TYPE=E2B` (مع وضع الحماية)، أو اتركهما مُعطَّلَيْن.
</Warning>
### برامج التشغيل المتاحة
### الوظائف المنطقية - برامج التشغيل المتاحة
| برنامج التشغيل | متغير البيئة | حالة الاستخدام | مستوى الأمان |
| -------------- | -------------------------- | ------------------------------- | ----------------------------- |
| معطل | `SERVERLESS_TYPE=DISABLED` | تعطيل الوظائف المنطقية بالكامل | غير متاح |
| محلي | `SERVERLESS_TYPE=LOCAL` | بيئات التطوير والبيئات الموثوقة | منخفض (من دون عزل) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | الإنتاج مع شيفرة غير موثوق بها | مرتفع (عزل على مستوى الأجهزة) |
| برنامج التشغيل | متغير البيئة | حالة الاستخدام | مستوى الأمان |
| -------------- | ------------------------------ | ------------------------------- | ----------------------------- |
| معطل | `LOGIC_FUNCTION_TYPE=DISABLED` | تعطيل الوظائف المنطقية بالكامل | غير متاح |
| محلي | `LOGIC_FUNCTION_TYPE=LOCAL` | بيئات التطوير والبيئات الموثوقة | منخفض (من دون عزل) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | الإنتاج مع شيفرة غير موثوق بها | مرتفع (عزل على مستوى الأجهزة) |
### التكوين الموصى به
### الوظائف المنطقية - الإعداد الموصى به
**للتطوير:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**للإنتاج (AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**لتعطيل الوظائف المنطقية:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### مفسر الشيفرة - برامج التشغيل المتاحة
| برنامج التشغيل | متغير البيئة | حالة الاستخدام | مستوى الأمان |
| -------------- | -------------------------------- | ------------------------------------- | ------------------------ |
| معطل | `CODE_INTERPRETER_TYPE=DISABLED` | تعطيل تنفيذ الشيفرة بالذكاء الاصطناعي | غير متاح |
| محلي | `CODE_INTERPRETER_TYPE=LOCAL` | للتطوير فقط | منخفض (من دون عزل) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | الإنتاج مع تنفيذ ضمن صندوق رمل معزول | مرتفعة (صندوق رمل معزول) |
<Note>
عند استخدام `SERVERLESS_TYPE=DISABLED`، ستؤدي أي محاولة لتنفيذ وظيفة منطقية إلى إرجاع خطأ. يكون هذا مفيدًا إذا كنت ترغب في تشغيل Twenty من دون إمكانات الوظائف المنطقية.
عند استخدام `LOGIC_FUNCTION_TYPE=DISABLED` أو `CODE_INTERPRETER_TYPE=DISABLED`، سترجع أي محاولة للتنفيذ خطأً. يكون هذا مفيدًا إذا كنت ترغب في تشغيل Twenty من دون هذه الإمكانات.
</Note>
@@ -0,0 +1,142 @@
---
title: خادم MCP
description: اربط مساعدي الذكاء الاصطناعي بمساحة عمل Twenty الخاصة بك باستخدام بروتوكول سياق النموذج.
---
<Warning>
MCP حاليًا في مرحلة **ألفا** وهو متاح فقط في بعض مساحات العمل. قد لا يكون مفعّلًا لمساحة عملك بعد.
</Warning>
تعرض Twenty خادم [MCP](https://modelcontextprotocol.io/) بحيث تتمكّن مساعدات الذكاء الاصطناعي — Claude Desktop وClaude Code وCursor وChatGPT وغيرها — من قراءة وكتابة بيانات نظام إدارة علاقات العملاء (CRM) لديك باستخدام اللغة الطبيعية.
استخدم **عنوان URL لمساحة العمل** (عنوان URL الذي تستخدمه للوصول إلى Twenty) كنقطة نهاية MCP. على Twenty Cloud، قد يكون عنوان URL لمساحة العمل هو `https://{mycompany}.twenty.com` أو نطاق مخصص. الخادم متاح على:
| البيئة | نقطة نهاية MCP |
| --------------------- | ---------------------------------------------------------------------------------------- |
| **السحابة** | `https://{your-workspace-url}/mcp` (على سبيل المثال: `https://mycompany.twenty.com/mcp`) |
| **الاستضافة الذاتية** | `https://{your-domain}/mcp` |
## طرق المصادقة
لديك طريقتان لمصادقة عميل MCP: **OAuth** (مُوصى بها) أو **مفتاح API**.
### الخيار أ — OAuth (مُوصى به)
باستخدام OAuth، يفتح عميل MCP لديك نافذة متصفح لتسجيل الدخول. لا يتم تخزين أي أسرار في ملفات الإعداد، ويتم تحديث الرموز المميِّزة تلقائيًا.
<Note>
يتطلب OAuth عميل MCP يدعم [مواصفة تفويض MCP](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization). تدعمه Claude Desktop وClaude Code وCursor وChatGPT.
</Note>
أضِف ما يلي إلى تهيئة عميل MCP لديك، واستبدِل `{your-workspace-url}` بمضيف مساحة العمل لديك (على سبيل المثال: `mycompany.twenty.com`):
```json
{
"mcpServers": {
"twenty": {
"type": "streamable-http",
"url": "https://{your-workspace-url}/mcp"
}
}
}
```
هذا كل شيء — لا حاجة إلى مفتاح API. عند اتصال العميل للمرة الأولى، سيفعل ما يلي:
1. اكتشاف بيانات التعريف الخاصة بـ OAuth لدى Twenty عبر `/.well-known/oauth-protected-resource` و`/.well-known/oauth-authorization-server`
2. تسجيل نفسه كعميل OAuth عبر التسجيل الديناميكي للعميل (RFC 7591)
3. فتح متصفحك لتفويض الوصول
4. استلام الرموز المميِّزة والاتصال بخادم MCP
تعيد الاتصالات اللاحقة استخدام الرموز المميِّزة المخزنة وتحدِّثها تلقائيًا.
### الخيار ب — مفتاح API
إذا كان عميل MCP لديك لا يدعم OAuth، أو كنت تفضّل بيانات اعتماد ثابتة، فمرِّر مفتاح API في ترويسة `Authorization`:
```json
{
"mcpServers": {
"twenty": {
"type": "streamable-http",
"url": "https://{your-workspace-url}/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
<Warning>
يمنح مفتاح API الخاص بك حق الوصول إلى بيانات مساحة العمل. أبعِده عن أنظمة التحكم في الإصدارات وملفات dotfiles المشتركة.
</Warning>
لإنشاء مفتاح API، انتقل إلى **Settings > APIs & Webhooks > + Create key**. راجع [واجهات برمجة التطبيقات](/l/ar/developers/extend/api#create-an-api-key) للتفاصيل.
## البدء السريع
### 1. انسخ الإعداد
انتقل إلى **Settings > AI > More > MCP Server** في Twenty. اختر طريقة المصادقة (OAuth أو مفتاح API)، وانسخ مقطع JSON (سيستخدم بالفعل عنوان URL لمساحة العمل لديك)، ثم الصقه في ملف إعدادات عميل MCP لديك.
| العميل | موقع ملف الإعداد |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Claude Desktop** | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) أو `%APPDATA%\Claude\claude_desktop_config.json` (Windows) |
| **Claude Code** | `~/.claude.json` (المستخدم) أو `.mcp.json` (المشروع) |
| **Cursor** | `.cursor/mcp.json` ضمن مشروعك، أو `~/.cursor/mcp.json` عالميًا |
| **ChatGPT** | فعِّل وضع المطوّر في **Settings > Apps & Connectors > Advanced settings**، ثم استخدم **Create** في **Settings > Apps & Connectors** لإضافة خادم MCP |
### 2. الاتصال
أعِد تشغيل عميل MCP لديك (أو أعد تحميل ملف الإعداد). إذا كنت تستخدم OAuth فسيتم توجيهك إلى Twenty لتفويض الوصول. إذا كنت تستخدم مفتاح API فسيكون الاتصال فوريًا.
### 3. ابدأ باستخدامه
اطلب من مساعد الذكاء الاصطناعي التفاعل مع نظام إدارة علاقات العملاء (CRM) لديك:
* *"أرني أحدث 5 شركات تم إنشاؤها"*
* *"أنشئ شخصًا جديدًا باسم Jane Doe في Acme Corp"*
* *"اعثر على جميع الفرص المفتوحة التي تزيد قيمتها عن 10 آلاف دولار"*
## الأدوات المتاحة
بعد الاتصال، يوفّر خادم MCP أدوات تعكس واجهة برمجة تطبيقات Twenty (API). سير العمل الموصى به هو:
1. **`get_tool_catalog`** — اكتشف جميع الأدوات المتاحة
2. **`learn_tools`** — احصل على مخطط الإدخال لأدوات محددة
3. **`execute_tool`** — شغّل أداة
لا تحتاج إلى تذكّر أسماء الأدوات. اسأل مساعد الذكاء الاصطناعي عمّا يمكنه فعله وسيستدعي `get_tool_catalog` تلقائيًا.
## الصلاحيات
ترث اتصالات MCP أذونات المستخدم المُصادَق عليه (OAuth) أو الدور المُعيَّن لمفتاح API. لتقييد ما يمكن لخادم MCP القيام به:
* **OAuth**: ينطبق دور المستخدم في مساحة العمل.
* **API Key**: عيِّن دورًا لمفتاح API ضمن **Settings > Roles**. راجع [الأذونات](/l/ar/user-guide/permissions-access/capabilities/permissions).
## التكوين ذاتي الاستضافة
في حالات الاستضافة الذاتية، استبدِل `{your-workspace-url}` بعنوان URL الخاص بالخادم لديك. تأكّد من أن قيمة `SERVER_URL` في بيئتك تطابق عنوان URL العام لمثيل Twenty لديك — إذ يُستخدَم ذلك لإنشاء بيانات تعريف اكتشاف OAuth.
```bash
SERVER_URL=https://twenty.yourcompany.com
```
تُشتق نقطة نهاية MCP ونقاط نهاية OAuth وبيانات تعريف الاكتشاف جميعها من هذه القيمة.
## استكشاف الأخطاء وإصلاحها
**أخطاء "Unauthorized" أو 401**
* OAuth: أعد التفويض عبر مسح الرموز المميِّزة المخزنة في عميل MCP لديك ثم أعد الاتصال.
* API Key: تحقّق من أن المفتاح صالح ولم تنتهِ صلاحيته. أعِد توليده إذا لزم الأمر.
**عملية OAuth لا تفتح متصفحًا**
* تأكّد من أن عميل MCP لديك يدعم تفويض MCP. ارجع إلى طريقة مفتاح API إذا لم يكن كذلك.
**انتهاء مهلة الاتصال**
* تحقّق من إمكانية الوصول إلى عنوان URL لنقطة نهاية MCP من جهازك. بالنسبة لحالات الاستضافة الذاتية، تحقّق من أن الخادم يعمل وأن `SERVER_URL` مُعيَّن بشكل صحيح.
@@ -6,7 +6,7 @@ description: Der Leitfaden für Mitwirkende (oder neugierige Entwickler), die Tw
## Voraussetzungen
<Tabs>
<Tab title="Linux und MacOS">
<Tab title="Linux und macOS">
Bevor Sie Twenty installieren und verwenden können, stellen Sie sicher, dass Sie Folgendes auf Ihrem Computer installiert haben:
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
@@ -103,7 +103,7 @@ Alle folgenden Befehle innerhalb des Projekts sind vom Stammverzeichnis aus ausz
<Tabs>
<Tab title="Linux">
**Option 1 (bevorzugt):** Um Ihre Datenbank lokal bereitzustellen:
Verwenden Sie den folgenden Link, um PostgreSQL auf Ihrem Linux-Rechner zu installieren: [Postgresql-Installation](https://www.postgresql.org/download/linux/)
Verwenden Sie den folgenden Link, um PostgreSQL auf Ihrem Linux-Rechner zu installieren: [PostgreSQL-Installation](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
@@ -129,8 +129,8 @@ Alle folgenden Befehle innerhalb des Projekts sind vom Stammverzeichnis aus ausz
brew services list
```
Der Installer erstellt möglicherweise nicht standardmäßig den Benutzer `postgres`, wenn er
über Homebrew auf MacOS installiert wird. Stattdessen wird eine PostgreSQL-Rolle erstellt, die Ihrem macOS
Das Installationsprogramm erstellt den Benutzer `postgres` möglicherweise nicht standardmäßig bei der Installation
über Homebrew auf macOS. Stattdessen wird eine PostgreSQL-Rolle erstellt, die Ihrem macOS
Benutzernamen (z. B. "john") entspricht.
Um zu überprüfen und, falls erforderlich, den Benutzer `postgres` zu erstellen, führen Sie folgende Schritte aus:
```bash
@@ -174,7 +174,7 @@ Alle folgenden Befehle innerhalb des Projekts sind vom Stammverzeichnis aus ausz
Alle folgenden Schritte sind im WSL-Terminal auszuführen (innerhalb Ihrer virtuellen Maschine)
**Option 1:** Um Ihr PostgreSQL lokal bereitzustellen:
Verwenden Sie den folgenden Link, um PostgreSQL auf Ihrer Linux-VM zu installieren: [Postgresql-Installation](https://www.postgresql.org/download/linux/)
Verwenden Sie den folgenden Link, um PostgreSQL auf Ihrer Linux-VM zu installieren: [PostgreSQL-Installation](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
@@ -189,11 +189,13 @@ Alle folgenden Befehle innerhalb des Projekts sind vom Stammverzeichnis aus ausz
</Tab>
</Tabs>
Sie können jetzt über [localhost:5432](localhost:5432) auf die Datenbank zugreifen, mit dem Benutzer `postgres` und dem Passwort `postgres`.
Sie können nun über `localhost:5432` auf die Datenbank zugreifen.
Wenn Sie die oben genannte Docker-Option verwendet haben, lauten die Standardanmeldedaten Benutzer `postgres` und Passwort `postgres`. Für native PostgreSQL-Installationen verwenden Sie die auf Ihrem Rechner konfigurierten Anmeldedaten und Rollen.
## Schritt 4: Einrichten einer Redis-Datenbank (Cache)
Twenty benötigt einen Redis-Cache, um die beste Leistung zu bieten
Twenty benötigt einen Redis-Cache, um die beste Leistung zu bieten.
<Tabs>
<Tab title="Linux">
@@ -211,7 +213,9 @@ Twenty benötigt einen Redis-Cache, um die beste Leistung zu bieten
brew install redis
```
Starten Sie Ihren Redis-Server:
`brew services start redis`
```bash
brew services start redis
```
**Option 2:** Wenn Sie Docker installiert haben:
```bash
@@ -229,11 +233,11 @@ Twenty benötigt einen Redis-Cache, um die beste Leistung zu bieten
</Tab>
</Tabs>
Wenn Sie eine Client-GUI benötigen, empfehlen wir [redis insight](https://redis.io/insight/) (kostenlose Version verfügbar)
Wenn Sie eine Client-GUI benötigen, empfehlen wir [Redis Insight](https://redis.io/insight/) (kostenlose Version verfügbar).
## Schritt 5: Einrichten von Umgebungsvariablen
Verwenden Sie Umgebungsvariablen oder `.env`-Dateien, um Ihr Projekt zu konfigurieren. Weitere Informationen [hier](/l/de/developers/self-host/capabilities/setup)
Verwenden Sie Umgebungsvariablen oder `.env`-Dateien, um Ihr Projekt zu konfigurieren. Weitere Informationen [hier](/l/de/developers/self-host/capabilities/setup).
Kopieren Sie die `.env.example`-Dateien in `/front` und `/server`:
@@ -64,11 +64,17 @@ yarn twenty function:execute --preInstall
# Die Post-Installationsfunktion ausführen
yarn twenty function:execute --postInstall
# Die Anwendung für die Verteilung erstellen
yarn twenty app:build
# Die Anwendung auf npm oder einen Twenty-Server veröffentlichen
yarn twenty app:publish
# Die Anwendung aus dem aktuellen Arbeitsbereich deinstallieren
yarn twenty app:uninstall
# Hilfe zu Befehlen anzeigen
yarn twenty help},{
yarn twenty help
```
Siehe auch: die CLI-Referenzseiten für [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) und [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@@ -1240,6 +1246,113 @@ Hauptpunkte:
Ein minimales End-to-End-Beispiel, das Objekte, Logikfunktionen, Frontend-Komponenten und mehrere Trigger demonstriert, finden Sie [hier](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Erstellen Ihrer App
Sobald Sie Ihre App mit `app:dev` entwickelt haben, verwenden Sie `app:build`, um sie in ein verteilbares Paket zu kompilieren.
```bash filename="Terminal"
# Die App erstellen (Ausgabe nach .twenty/output/)
yarn twenty app:build
# Build ausführen und ein Tarball (.tgz) für die Verteilung erstellen
yarn twenty app:build --tarball
```
Der Build-Prozess:
1. **Parst und validiert das Manifest** — liest alle `defineX()`-Entitäten aus Ihren Quelldateien und validiert die Manifeststruktur.
2. **Kompiliert Logikfunktionen und Front-Komponenten** — bündelt TypeScript-Quellcode in ESM `.mjs`-Dateien mit esbuild.
3. **Erzeugt Checksummen** — berechnet MD5-Hashes für jede erstellte Datei, die im Manifest als `builtHandlerChecksum` / `builtComponentChecksum` gespeichert werden.
4. **Generiert den typisierten API-Client** — führt eine Introspektion des GraphQL-Schemas durch und generiert die typisierten Clients `CoreApiClient` und `MetadataApiClient`.
5. **Führt eine TypeScript-Typprüfung aus** — führt `tsc --noEmit` aus, um Typfehler vor der Veröffentlichung zu erkennen.
6. **Baut mit dem generierten Client neu** — führt einen zweiten Kompiliervorgang durch, damit die generierten Client-Typen enthalten sind.
7. **Erstellt optional einen Tarball** — wenn `--tarball` übergeben wird, wird `npm pack` ausgeführt, um eine `.tgz`-Datei zu erstellen, die für die Verteilung bereit ist.
Der Build-Output in `.twenty/output/` enthält:
```text
.twenty/output/
├── manifest.json # Manifest with checksums for all built files
├── package.json # Copied from app root
├── yarn.lock # Copied from app root
├── src/
│ ├── logic-functions/ # Compiled .mjs logic function files
│ └── front-components/ # Compiled .mjs front component files
├── public/ # Static assets (if any)
└── my-app-1.0.0.tgz # Only with --tarball flag
```
| Option | Beschreibung |
| ----------- | -------------------------------------------------------------- |
| `[appPath]` | Pfad zum App-Verzeichnis (standardmäßig aktuelles Verzeichnis) |
| `--tarball` | Den Output zusätzlich in einen `.tgz`-Tarball packen |
## Veröffentlichen Ihrer App
Verwenden Sie `app:publish`, um Ihre App zu verteilen — entweder zur npm-Registry oder direkt zu einem Twenty-Server.
### Bei npm veröffentlichen (Standard)
```bash filename="Terminal"
# Publish to npm (requires npm login)
yarn twenty app:publish
# Publish with a dist-tag (e.g. beta, next)
yarn twenty app:publish --tag beta
```
Dies baut die App und führt `npm publish` aus dem Verzeichnis `.twenty/output/` aus. Das veröffentlichte Paket kann dann von jedem Arbeitsbereich über den Twenty-Marktplatz installiert werden.
### Auf einem Twenty-Server veröffentlichen
```bash filename="Terminal"
# Publish directly to a Twenty server
yarn twenty app:publish --server https://app.twenty.com
```
Dies erstellt beim Build einen Tarball, lädt ihn über die GraphQL-Mutation `uploadAppTarball` auf den Server hoch und stößt die Installation in einem Schritt an. Dies ist nützlich für private Bereitstellungen oder Tests gegen einen bestimmten Server.
| Option | Beschreibung |
| ----------------- | ------------------------------------------------------------------ |
| `[appPath]` | Pfad zum App-Verzeichnis (standardmäßig aktuelles Verzeichnis) |
| `--server <url>` | Auf einen Twenty-Server anstelle von npm veröffentlichen |
| `--token <token>` | Authentifizierungstoken für den Zielserver |
| `--tag <tag>` | npm dist-tag (z. B. `beta`, `next`) — nur für npm-Veröffentlichung |
## Anwendungsregistrierung
Bevor eine App in einem Arbeitsbereich installiert werden kann, muss sie **registriert** werden. Eine Registrierung ist ein Metadatensatz, der beschreibt, woher die App stammt und wie sie authentifiziert wird. Dies wird in den meisten Fällen automatisch durch die CLI erledigt.
### Quelltypen
Jede Registrierung hat einen **Quelltyp**, der bestimmt, wie die Dateien der App während der Installation aufgelöst werden:
| Quelltyp | Wie Dateien aufgelöst werden | Typischer Anwendungsfall |
| --------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `LOCAL` | Dateien werden in Echtzeit vom CLI-Watcher synchronisiert — die Installation wird übersprungen | Entwicklung mit `app:dev` |
| `NPM` | Über das Feld `sourcePackage` aus der npm-Registry abgerufen | Veröffentlichte Apps auf npm |
| `TARBALL` | Aus einer hochgeladenen, auf dem Server gespeicherten `.tgz`-Datei extrahiert | Private Apps, die mit `--server` veröffentlicht wurden |
### Wie die Registrierung erfolgt
* **`app:dev`** — erstellt beim ersten Ausführen des Dev-Modus für einen Arbeitsbereich automatisch eine `LOCAL`-Registrierung.
* **`app:publish --server`** — lädt einen Tarball hoch und erstellt (oder aktualisiert) eine `TARBALL`-Registrierung und installiert anschließend die App.
* **npm-Marktplatz** — `NPM`-Registrierungen werden erstellt, wenn Apps aus der npm-Registry in den Twenty-Marktplatzkatalog synchronisiert werden.
* **GraphQL-API** — Sie können Registrierungen auch programmgesteuert über die Mutation `createApplicationRegistration` erstellen.
### Registrierung vs. Installation
**Registrierung** und **Installation** sind unterschiedliche Konzepte:
* Eine **Registrierung** (`ApplicationRegistration`) ist ein globaler Metadatensatz, der die App beschreibt: ihren Namen, den Quelltyp, die OAuth-Anmeldedaten und den Status der Marktplatzlistung. Sie existiert unabhängig von jedem Arbeitsbereich.
* Eine **Installation** (`Application`) ist eine Instanz pro Arbeitsbereich. Wenn ein Benutzer eine App installiert, ermittelt Twenty das Paket aus der Quelle der Registrierung, schreibt die erstellten Dateien in den Speicher und synchronisiert das Manifest (wobei Objekte, Felder, Logikfunktionen usw. erstellt werden) in diesem Arbeitsbereich.
Eine Registrierung kann in vielen Arbeitsbereichen installiert werden. Jeder Arbeitsbereich erhält seine eigene Kopie der Dateien und des Datenmodells der App.
### OAuth-Anmeldedaten
Jede Registrierung enthält OAuth-Anmeldedaten (`oAuthClientId` und `oAuthClientSecret`), die bei der Erstellung generiert werden. Diese werden von der App verwendet, um API-Anfragen im Namen der Benutzer zu authentifizieren. Das Client-Secret wird bei der Erstellung **einmalig** zurückgegeben — bewahren Sie es sicher auf. Sie können es später über die Mutation `rotateApplicationRegistrationClientSecret` rotieren.
## Manuelle Einrichtung (ohne Scaffolder)
Wir empfehlen zwar `create-twenty-app` für das beste Einstiegserlebnis, Sie können ein Projekt aber auch manuell einrichten. Installieren Sie die CLI nicht global. Fügen Sie stattdessen `twenty-sdk` als lokale Abhängigkeit hinzu und binden Sie ein einzelnes Skript in Ihrer package.json ein:
@@ -3,7 +3,7 @@ title: 1-Klick mit Docker Compose
---
<Warning>
Docker-Container sind für die Produktion oder das Selbsthosten bestimmt. Für Beiträge siehe bitte das [Lokale Setup](/l/de/developers/contribute/capabilities/local-setup).
Docker-Container sind für produktives Hosting oder Selbsthosting vorgesehen. Zum Mitwirken siehe [Lokale Einrichtung](/l/de/developers/contribute/capabilities/local-setup).
</Warning>
## Überblick
@@ -12,7 +12,7 @@ Diese Anleitung enthält Schritt-für-Schritt-Anweisungen, um die Twenty-Anwendu
**Wichtig:** Ändern Sie nur die in dieser Anleitung explizit erwähnten Einstellungen. Andere Konfigurationen zu ändern, kann zu Problemen führen.
Siehe die Dokumentation [Umgebungsvariablen einrichten](/l/de/developers/self-host/capabilities/setup) zur erweiterten Konfiguration. Alle Umgebungsvariablen müssen in der Datei docker-compose.yml auf Server- und/oder Worker-Ebene deklariert werden, je nach Variable.
Siehe die Dokumentation [Umgebungsvariablen einrichten](/l/de/developers/self-host/capabilities/setup) zur erweiterten Konfiguration. Alle Umgebungsvariablen müssen in der Datei `docker-compose.yml` auf Server- und/oder Worker-Ebene deklariert werden, je nach Variable.
## Systemanforderungen
@@ -297,46 +297,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
**Nur-Umgebungsmodus:** Wenn Sie `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` setzen, fügen Sie diese Variablen stattdessen Ihrer `.env`-Datei hinzu.
</Warning>
## Logikfunktionen
## Logikfunktionen & Code-Interpreter
Twenty unterstützt Logikfunktionen für Workflows und benutzerdefinierte Logik. Die Ausführungsumgebung wird über die Umgebungsvariable `SERVERLESS_TYPE` konfiguriert.
Twenty unterstützt Logikfunktionen für Workflows und den Code-Interpreter für KI-Datenanalyse. Beide führen vom Benutzer bereitgestellten Code aus und erfordern aus Sicherheitsgründen eine explizite Konfiguration.
### Sicherheits-Standardeinstellungen
**In Produktion (NODE_ENV=production):** Sowohl Logikfunktionen als auch der Code-Interpreter sind standardmäßig **deaktiviert**. Sie müssen sie, wenn Sie diese Funktionen benötigen, explizit mit `LOGIC_FUNCTION_TYPE` und `CODE_INTERPRETER_TYPE` aktivieren.
**In der Entwicklung (NODE_ENV=development):** Beide sind der Einfachheit halber beim lokalen Betrieb standardmäßig **LOCAL**.
<Warning>
**Sicherheitshinweis:** Der lokale Treiber (`SERVERLESS_TYPE=LOCAL`) führt Code ohne Sandbox direkt auf dem Host in einem Node.js-Prozess aus. Er sollte nur für vertrauenswürdigen Code in der Entwicklung verwendet werden. Für Produktivbereitstellungen, die nicht vertrauenswürdigen Code verarbeiten, empfehlen wir nachdrücklich, `SERVERLESS_TYPE=LAMBDA` oder `SERVERLESS_TYPE=DISABLED` zu verwenden.
**Sicherheitshinweis:** Der lokale Treiber (`LOGIC_FUNCTION_TYPE=LOCAL` oder `CODE_INTERPRETER_TYPE=LOCAL`) führt Code ohne Sandbox direkt auf dem Host in einem Node.js-Prozess aus. Er sollte nur für vertrauenswürdigen Code in der Entwicklung verwendet werden. Für Produktionsbereitstellungen, die nicht vertrauenswürdigen Code verarbeiten, verwenden Sie `LOGIC_FUNCTION_TYPE=LAMBDA` oder `CODE_INTERPRETER_TYPE=E2B` (mit Sandbox-Isolierung), oder lassen Sie sie deaktiviert.
</Warning>
### Verfügbare Treiber
### Logikfunktionen - Verfügbare Treiber
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
| ----------- | -------------------------- | -------------------------------------------------- | ---------------------------------- |
| Deaktiviert | `SERVERLESS_TYPE=DISABLED` | Logikfunktionen vollständig deaktivieren | N/A |
| Lokal | `SERVERLESS_TYPE=LOCAL` | Entwicklung und vertrauenswürdige Umgebungen | Niedrig (keine Sandbox) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Produktivbetrieb mit nicht vertrauenswürdigem Code | Hoch (Isolation auf Hardwareebene) |
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
| ----------- | ------------------------------ | -------------------------------------------------- | ---------------------------------- |
| Deaktiviert | `LOGIC_FUNCTION_TYPE=DISABLED` | Logikfunktionen vollständig deaktivieren | N/A |
| Lokal | `LOGIC_FUNCTION_TYPE=LOCAL` | Entwicklung und vertrauenswürdige Umgebungen | Niedrig (keine Sandbox) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Produktivbetrieb mit nicht vertrauenswürdigem Code | Hoch (Isolation auf Hardwareebene) |
### Empfohlene Konfiguration
### Logikfunktionen - Empfohlene Konfiguration
**Für die Entwicklung:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**Für den Produktivbetrieb (AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**Zum Deaktivieren von Logikfunktionen:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Code-Interpreter - Verfügbare Treiber
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
| ----------- | -------------------------------- | ------------------------------------------ | ------------------------ |
| Deaktiviert | `CODE_INTERPRETER_TYPE=DISABLED` | KI-Codeausführung deaktivieren | N/A |
| Lokal | `CODE_INTERPRETER_TYPE=LOCAL` | Nur für die Entwicklung | Niedrig (keine Sandbox) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Produktion mit Ausführung in einer Sandbox | Hoch (isolierte Sandbox) |
<Note>
Bei Verwendung von `SERVERLESS_TYPE=DISABLED` führt jeder Versuch, eine Logikfunktion auszuführen, zu einem Fehler. Dies ist nützlich, wenn Sie Twenty ohne Unterstützung für Logikfunktionen betreiben möchten.
Bei Verwendung von `LOGIC_FUNCTION_TYPE=DISABLED` oder `CODE_INTERPRETER_TYPE=DISABLED` führt jeder Ausführungsversuch zu einem Fehler. Dies ist nützlich, wenn Sie Twenty ohne diese Funktionen betreiben möchten.
</Note>
@@ -0,0 +1,142 @@
---
title: MCP-Server
description: Verbinden Sie KI-Assistenten mit Ihrem Twenty-Workspace über das Model Context Protocol.
---
<Warning>
MCP befindet sich derzeit in **alpha** und ist nur in einigen Workspaces verfügbar. Möglicherweise ist es für Ihren Workspace noch nicht aktiviert.
</Warning>
Twenty stellt einen [MCP](https://modelcontextprotocol.io/)-Server bereit, damit KI-Assistenten — Claude Desktop, Claude Code, Cursor, ChatGPT und andere — Ihre CRM-Daten in natürlicher Sprache lesen und schreiben können.
Verwenden Sie Ihre **Workspace-URL** (die URL, mit der Sie auf Twenty zugreifen) als MCP-Endpunkt. In Twenty Cloud kann Ihre Workspace-URL `https://{mycompany}.twenty.com` oder eine benutzerdefinierte Domain sein. Der Server ist verfügbar unter:
| Umgebung | MCP-Endpunkt |
| ----------------- | ----------------------------------------------------------------------------- |
| **Cloud** | `https://{your-workspace-url}/mcp` (z. B. `https://mycompany.twenty.com/mcp`) |
| **Selbsthosting** | `https://{your-domain}/mcp` |
## Authentifizierungsmethoden
Sie haben zwei Möglichkeiten, Ihren MCP-Client zu authentifizieren: **OAuth** (empfohlen) oder **API-Schlüssel**.
### Option A — OAuth (empfohlen)
Mit OAuth öffnet Ihr MCP-Client ein Browserfenster, damit Sie sich anmelden können. Es werden keine geheimen Informationen in Konfigurationsdateien gespeichert, und Token werden automatisch erneuert.
<Note>
OAuth erfordert einen MCP-Client, der die [MCP-Autorisierungsspezifikation](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) unterstützt. Claude Desktop, Claude Code, Cursor und ChatGPT unterstützen dies.
</Note>
Fügen Sie dies zu Ihrer MCP-Client-Konfiguration hinzu und ersetzen Sie `{your-workspace-url}` durch den Host Ihrer Workspace-URL (z. B. `mycompany.twenty.com`):
```json
{
"mcpServers": {
"twenty": {
"type": "streamable-http",
"url": "https://{your-workspace-url}/mcp"
}
}
}
```
Das ist alles — kein API-Schlüssel erforderlich. Wenn der Client sich zum ersten Mal verbindet, wird er:
1. Die OAuth-Metadaten von Twenty über `/.well-known/oauth-protected-resource` und `/.well-known/oauth-authorization-server` ermitteln
2. Sich über die dynamische Client-Registrierung (RFC 7591) als OAuth-Client registrieren
3. Ihren Browser öffnen, um den Zugriff zu autorisieren
4. Token empfangen und eine Verbindung zum MCP-Server herstellen
Nachfolgende Verbindungen verwenden die gespeicherten Token erneut und erneuern sie automatisch.
### Option B — API-Schlüssel
Wenn Ihr MCP-Client OAuth nicht unterstützt oder Sie statische Anmeldeinformationen bevorzugen, übergeben Sie einen API-Schlüssel im `Authorization`-Header:
```json
{
"mcpServers": {
"twenty": {
"type": "streamable-http",
"url": "https://{your-workspace-url}/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
<Warning>
Ihr API-Schlüssel gewährt Zugriff auf Workspace-Daten. Halten Sie es von der Versionskontrolle und von gemeinsam genutzten Dotfiles fern.
</Warning>
Um einen API-Schlüssel zu erstellen, gehen Sie zu **Settings > APIs & Webhooks > + Create key**. Details finden Sie unter [APIs](/l/de/developers/extend/api#create-an-api-key).
## Schnellstart
### 1. Konfiguration kopieren
Gehen Sie in Twenty zu **Settings > AI > More > MCP Server**. Wählen Sie Ihre Authentifizierungsmethode (OAuth oder API-Schlüssel), kopieren Sie das JSON-Snippet (es verwendet bereits Ihre Workspace-URL) und fügen Sie es in die Konfigurationsdatei Ihres MCP-Clients ein.
| Client | Speicherort der Konfigurationsdatei |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Claude Desktop** | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) oder `%APPDATA%\Claude\claude_desktop_config.json` (Windows) |
| **Claude Code** | `~/.claude.json` (Benutzer) oder `.mcp.json` (Projekt) |
| **Cursor** | `.cursor/mcp.json` in Ihrem Projekt oder `~/.cursor/mcp.json` global |
| **ChatGPT** | Aktivieren Sie den Entwicklermodus in **Settings > Apps & Connectors > Advanced settings** und verwenden Sie dann **Create** in **Settings > Apps & Connectors**, um den MCP-Server hinzuzufügen |
### 2. Verbinden
Starten Sie Ihren MCP-Client neu (oder laden Sie die Konfiguration neu). Bei Verwendung von OAuth werden Sie zu Twenty weitergeleitet, um den Zugriff zu autorisieren. Bei Verwendung eines API-Schlüssels wird die Verbindung sofort hergestellt.
### 3. Jetzt loslegen
Bitten Sie Ihren KI-Assistenten, mit Ihrem CRM zu interagieren:
* *"Zeige mir die 5 zuletzt erstellten Unternehmen"*
* *"Erstelle eine neue Person namens Jane Doe bei Acme Corp"*
* *"Finde alle offenen Verkaufschancen mit einem Wert von mehr als $10k"*
## Verfügbare Tools
Nach der Verbindung stellt der MCP-Server Tools bereit, die die Twenty-API widerspiegeln. Der empfohlene Workflow ist:
1. **`get_tool_catalog`** — alle verfügbaren Tools entdecken
2. **`learn_tools`** — das Eingabeschema für bestimmte Tools abrufen
3. **`execute_tool`** — ein Tool ausführen
Sie müssen sich die Tool-Namen nicht merken. Fragen Sie Ihren KI-Assistenten, was er tun kann, und er ruft `get_tool_catalog` automatisch auf.
## Berechtigungen
MCP-Verbindungen erben die Berechtigungen des authentifizierten Benutzers (OAuth) oder die dem API-Schlüssel zugewiesene Rolle. So beschränken Sie, was der MCP-Server tun darf:
* **OAuth**: Es gilt die Workspace-Rolle des Benutzers.
* **API-Schlüssel**: Weisen Sie dem API-Schlüssel unter **Settings > Roles** eine Rolle zu. Siehe [Berechtigungen](/l/de/user-guide/permissions-access/capabilities/permissions).
## Selbstgehostete Konfiguration
Für selbstgehostete Instanzen ersetzen Sie `{your-workspace-url}` durch die URL Ihres Servers. Stellen Sie sicher, dass `SERVER_URL` in Ihrer Umgebung der öffentlichen URL Ihrer Twenty-Instanz entspricht — dieser Wert wird verwendet, um die OAuth-Discovery-Metadaten zu generieren.
```bash
SERVER_URL=https://twenty.yourcompany.com
```
Der MCP-Endpunkt, die OAuth-Endpunkte und die Discovery-Metadaten leiten sich alle von diesem Wert ab.
## Fehlerbehebung
**"Unauthorized"- oder 401-Fehler**
* OAuth: Autorisieren Sie erneut, indem Sie die gespeicherten Token in Ihrem MCP-Client löschen und die Verbindung wiederherstellen.
* API-Schlüssel: Überprüfen Sie, ob der Schlüssel gültig ist und nicht abgelaufen ist. Generieren Sie ihn bei Bedarf neu.
**Der OAuth-Flow öffnet keinen Browser**
* Stellen Sie sicher, dass Ihr MCP-Client MCP Authorization unterstützt. Wechseln Sie andernfalls zur API-Schlüssel-Methode.
**Verbindungszeitüberschreitung**
* Stellen Sie sicher, dass die MCP-Endpunkt-URL von Ihrem Rechner aus erreichbar ist. Bei selbstgehosteten Instanzen prüfen Sie, ob der Server läuft und `SERVER_URL` korrekt gesetzt ist.

Some files were not shown because too many files have changed in this diff Show More