Compare commits

...
210 Commits
Author SHA1 Message Date
Weiko 98727eb40e Fix indexFieldMetadata select missing workspaceId 2026-04-17 17:12:35 +02:00
prastoin 891d6a375c fix(server): set AddIsInitialToUpgradeMigration1775909335324 name for upgrade backfill 2026-04-17 08:35:45 +02:00
Etienne 1c0a7029c3 Fix slow db query issue (#19770)
https://github.com/twentyhq/twenty/pull/19586#discussion_r3074136617
2026-04-16 17:57:15 +02:00
Charles BochetandEtienne 613be773cb fix: replace slow deep-equal with fastDeepEqual to resolve CPU bottleneck (#19771)
## Summary

- Replaced the `deep-equal` npm package with the existing
`fastDeepEqual` from `twenty-shared/utils` across 5 files in the server
and shared packages
- `deep-equal` was causing severe CPU overhead in the record update hot
path (`executeMany` → `formatTwentyOrmEventToDatabaseBatchEvent` →
`objectRecordChangedValues` → `deepEqual`, called **per field per
record**)
- `fastDeepEqual` is ~100x faster for plain JSON database records since
it skips unnecessary prototype chain inspection and edge-case handling
- Removed the now-unnecessary `LARGE_JSON_FIELDS` branching in
`objectRecordChangedValues` since all fields now use the fast
implementation
2026-04-16 17:56:56 +02:00
prastoin 2dd1fffe02 fix(server): cherry-pick breaking changes 2026-04-15 17:55:17 +02:00
Paul Rastoinandprastoin e13c9ef46c [Upgrade] Fix workspace creation cursor (#19701)
The upgrade migration system required new workspaces to always start
from a workspace command, which was too rigid. When the system was
mid-upgrade within an instance command (IC) segment, workspace creation
would fail or produce inconsistent state.

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

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

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

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

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

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

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

created empty workspaces to allow testing upgrade with several active
workspaces
2026-04-15 17:51:35 +02:00
prastoin 438d502988 fid(server): workspace activation set workspace.version 2026-04-14 16:46:56 +02:00
edc47bd458 i18n - docs translations (#19677)
Created by Github action

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

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

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

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

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

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

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

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

### Root cause

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

### Fix

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

## Before


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

## After


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

Fixes #19595

---------

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

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

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

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

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

## Changes

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

## Test plan

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

---------

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

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

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

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

---------

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

## Testing
- Not run (not requested)

---------

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

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

Closes #15544

---------

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

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

---------

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

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

---------

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

## Key Changes

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

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

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

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

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

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

## Implementation Details

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

https://claude.ai/code/session_01VksTSrYLXJVCPVBQhQdBTe

---------

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

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

---------

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

---------

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

---------

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

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

## Video QA


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

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


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

### After


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

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


**Duplicated IDs fix**


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


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

---------

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

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

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

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

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

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

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

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

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

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

---------

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


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

---------

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

---------

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

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

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


## After

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

## After

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

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

**The upgrade sequence model:**

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

**Segments:**

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

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


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

## Version constants & type-level deprecation

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

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

### `workspace.version` column deprecation

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

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

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

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

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

---------

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

## Changes

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

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

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

---------

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

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

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

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

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


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

---------

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

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

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

---------

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

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

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

### Added re-exports

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

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

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

## Test plan

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

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

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

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

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


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

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

## Root cause

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

## Key Changes

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

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

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

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

## Implementation Details

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

https://claude.ai/code/session_01D8syiJtCXu5bEHSr5KYkCt

---------

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

---------

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

## Key Changes

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

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

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

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

## Implementation Details

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

https://claude.ai/code/session_01JiTckmuVJMQWpJ7TUGwsqb

---------

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

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

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

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

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

## Root cause

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

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

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

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

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

https://claude.ai/code/session_0139Nb5bZykacNE69GQsFSrr

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

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

## Context

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

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

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


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

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

## Motivation

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

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

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

---------

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

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

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

https://claude.ai/code/session_01MRFVXtquL3wS7qmQkDU3AT

---------

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

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

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

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

### Root cause

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

### Fix

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


## Before


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




## After


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

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

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

https://claude.ai/code/session_01Pz8KCygSNgBPYsndbMq8f7

---------

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

## Bug description

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

## Fix

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

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

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

      With additional flags:
        --name=martmull

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

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

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

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

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

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

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

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

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

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

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

https://claude.ai/code/session_016DQodL32HYv6CR1cMASNHZ

---------

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



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

---------

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

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

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

## What changed

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

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

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

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

### Navigation module cleanup

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

### Product surfaces updated (non-exhaustive)

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

---------

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

## Testing
- Not run (not requested)

---------

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

---------

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



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

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


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

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

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-10 10:09:01 +02:00
Félix MalfaitGitHubClaude Opus 4.6claude[bot] <41898282+claude[bot]@users.noreply.github.com>
67e7f05a68 feat: email attachments and open-in-app click action (#19485)
## Changes

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

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

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

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

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

---------

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

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

## Changes

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

## Test plan

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

https://claude.ai/code/session_01QrqjBUXePJkPMd6gBAoWaR

---------

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

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

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

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

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

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

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

## Changes

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

## Why

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

## Validation

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

---------

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

---------

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

## Key Changes

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

## Implementation Details

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

https://claude.ai/code/session_01TAdN1gBzeiYELX4XDrrYY1

---------

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

---------

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

---------

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

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

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

https://claude.ai/code/session_01EnYKwVaCJyhgNPsi7p3YSq

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

---------

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

## Key Changes

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

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

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

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

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

## Implementation Details

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

https://claude.ai/code/session_012fXeP3bysaEgWsbkyu4ism

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

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

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

---------

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

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

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

---------

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

---------

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

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

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

## Details

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

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

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

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

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

---------

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

---------

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

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

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

---------

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

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

---------

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

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

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


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

## Fix


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

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

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

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

https://claude.ai/code/session_01BVMacfAXDMNx5WAFtP7GgW

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

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

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 12:26:14 +02:00
martmullGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
a78a843419 Fix install and deploy commands (#19481)
- disable publish with existing version
- disable installation of app version already installed

---------

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

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

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

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

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

https://claude.ai/code/session_011EyhBJ56RGuZHxBVWwzEQy

---------

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


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

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

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

---

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

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


</details>

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


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

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

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

---

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

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


</details>

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


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

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

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

---

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

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


</details>

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

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

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

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

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

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

---------

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

Fixes #19422.

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

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

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

## Test plan

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

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

---------

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

---------

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

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



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

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

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

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

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

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

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

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

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

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


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

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

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

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


Made with [Cursor](https://cursor.com)
2026-04-08 13:03:35 +00:00
Thomas des FrancsandGitHub 0e0fb246e6 Refactor the website hero into an interactive multi-view illustration (#19429)
## Summary
- replace the home hero visual with an interactive multi-view experience
for table, kanban, workflow, and dashboard states
- add the supporting hero data model, page normalizers, loaders, chips,
and sales dashboard assets
- update related website illustration components and ignore local Claude
worktrees in `.gitignore`

## Testing
- Not run (not requested)
2026-04-08 13:00:41 +00:00
Abdullah.andGitHub 83917f0dca Isolate illustrations from sections to style them independently. (#19447)
This PR moves illustrations from sections to a separate folder of their
own and accesses which illustration to load on a specific page using a
registry.

The reason for this change is that we want to be able to independently
style each illustration since shared styles being applied to nine
illustrations of ThreeCards, for example, does not get us the eventual
output visually that we're after because the underlying assets have
different lighting, angles etc.

Once we're at a position where we know what can be reused, I will
refactor. For now, Thomas would to take illustration styling to try and
implement what he has in mind, so isolating these files for him to work
without breaking anything.
2026-04-08 12:58:46 +00:00
ac4819758d i18n - translations (#19451)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 15:03:32 +02:00
Raphaël BosiandGitHub d07c27a907 [COMMAND MENU ITEMS] Create union type for command menu item payload (#19432)
Replace generic JSON scalar with a typed GraphQL union
CommandMenuItemPayload (PathNavigationPayload |
ObjectMetadataNavigationPayload) for the CommandMenuItem.payload field
2026-04-08 12:47:34 +00:00
265d859c6e i18n - docs translations (#19446)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 14:42:19 +02:00
Charles BochetandGitHub 1ae88f4e4f chore: add CD workflow template and point spawn action to main (#19430)
## Summary

- Adds reusable composite GitHub Actions for Twenty app deployment:
- `.github/actions/deploy` — builds and deploys to a remote instance
(`api-url`, `api-key` inputs)
- `.github/actions/install` — installs/upgrades on a specific workspace
(`api-url`, `api-key` inputs)
- Adds a `cd.yml` CD workflow that calls both actions in sequence. The
workflow:
  - Deploys on push to `main`
  - Can be triggered from a PR by adding a `deploy` label
- Configures a named remote via `TWENTY_DEPLOY_URL` env var and
`TWENTY_DEPLOY_API_KEY` secret
- Applied to: `create-twenty-app` template, `postcard` example,
`hello-world` example
- Updates the `spawn-twenty-app-dev-test` action ref from
`@feature/sdk-config-file-source-of-truth` to `@main` in all `ci.yml`
files
2026-04-08 12:31:38 +00:00
Paul RastoinandGitHub 85be463487 Slow instance commands (#19431)
# Introduction
This PR introduces the slow instance commands pattern, that allow
migrating data in prior of the schema migration, that would fail if not.

Slow instance commands runs after the fast instance commands and before
the workspace commands.
On twenty instance that do not has any active or suspended workspace the
data migration part is skipped but the migration still runs, especially
for fresh installs

We were previously hacking through typeorm transaction system to gain
such granularity using save points:
```ts
export class AddPayloadToCommandMenuItem1775129635528
  implements MigrationInterface
{
  name = 'AddPayloadToCommandMenuItem1775129635528';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `ALTER TABLE "core"."commandMenuItem" ADD "payload" jsonb`,
    );

    const savepointName =
      'sp_add_payload_check_constraint_to_command_menu_item';

    try {
      await queryRunner.query(`SAVEPOINT ${savepointName}`);

      await addPayloadCheckConstraintToCommandMenuItem(queryRunner);

      await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
    } catch (e) {
      try {
        await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
        await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
      } catch (rollbackError) {
        // oxlint-disable-next-line no-console
        console.error(
          'Failed to rollback to savepoint in AddPayloadToCommandMenuItem1775129635528',
          rollbackError,
        );
        throw rollbackError;
      }

      // oxlint-disable-next-line no-console
      console.error(
        'Swallowing AddPayloadToCommandMenuItem1775129635528 error',
        e,
      );
    }
  }
```

It was afterwards re-applied within an workspace commands, it was hacky
and missleading for the self host having false positive in logs

## New pattern

Generate the slow instance command
```
npx nx database:migrate:generate twenty-server -- --name add-foo-bar-columns --type slow
```

```ts
import { DataSource, QueryRunner } from 'typeorm';

import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';

@RegisteredInstanceCommand('1.21.0', 1775640902366, { type: 'slow' })
export class AddPrastoinColToWorkspaceSlowInstanceCommand implements SlowInstanceCommand {
  async runDataMigration(dataSource: DataSource): Promise<void> {
    // TODO: implement data backfill before the DDL migration
  }

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query('ALTER TABLE "core"."workspace" ADD "prastoin" character varying');
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query('ALTER TABLE "core"."workspace" DROP COLUMN "prastoin"');
  }
}
```

### Why `run-instance-commands` and `upgrade` remain separate commands

These two commands serve fundamentally different purposes with
incompatible scoping semantics:
The run-instance-commands iterates over all the legacy typeorm and the
instance commands of all versions, used for database init and so on. we
could be centralizing both but the readability tradeoff isn't worth it

In the future thanks to the cross-version pattern we will be able to
centralize them but not right now

Please note that by default the `run-instance-commands` only run the
fast instance commands which is expected for our cloud prod CD
2026-04-08 12:09:29 +00:00
BugIsGodandGitHub 8a84e32cf6 fix(ai): use @ai-sdk/openai-compatible for third-party providers (#19438)
## Summary:

I found a bug: 404 call when I use third-party providers (I used
deepseek). I found the final request url is:
`https://api.deepseek.com/v1/responses' `if we use createOpenAI. But the
correct one should be `https://api.provider.com/v1/chat/completions` for
the thirty provider. So I replace createOpenAI with
createOpenAICompatible in the openai-compatible provider path.

**Reproduction**: Add DeepSeek as a new AI provider (deepseek-chat,
deepseek-reasoner)

Also check the source code in Open Code project, they also use
createOpenAICompatible for the @ai-sdk/openai-compatible scenario
<img width="703" height="369" alt="image"
src="https://github.com/user-attachments/assets/90c4f924-6f1a-4fe7-821b-f13ee86a7a39"
/>


official docs: https://ai-sdk.dev/providers/openai-compatible-providers
https://ai-sdk.dev/providers/ai-sdk-providers/openai

<img width="860" height="293" alt="image"
src="https://github.com/user-attachments/assets/283b9b91-f1d6-4b1c-bb91-16ddccdff8b4"
/>



## Before
<img width="473" height="750" alt="deepseek before"
src="https://github.com/user-attachments/assets/f6b89294-1fa7-4ddc-a6a8-d396070caaca"
/>

## After

<img width="468" height="753" alt="deepseek after"
src="https://github.com/user-attachments/assets/0d170b70-e829-4a4c-abad-38879427c2df"
/>

## Backend error log

```json
[1] [Nest] 50907  - 07/04/2026, 23:48:57     LOG [ChatExecutionService] Starting chat execution with model deepseek/deepseek-chat, 4 active tools
[1] APICallError [AI_APICallError]: Not Found
[1]     at /Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/provider-utils/dist/index.js:2512:14
[1]     at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
[1]     at async postToApi (/Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/provider-utils/dist/index.js:2373:28)
[1]     at async OpenAIResponsesLanguageModel.doStream (/Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/openai/dist/index.js:4925:50)
[1]     at async fn (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7106:27)
[1]     at async /Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2340:24
[1]     at async _retryWithExponentialBackoff (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2599:12)
[1]     at async streamStep (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7063:17)
[1]     at async fn (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7455:9)
[1]     at async /Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2340:24 {
[1]   cause: undefined,
[1]   url: 'https://api.deepseek.com/v1/responses',
[1]   requestBodyValues: {
[1]     model: 'deepseek-chat',
[1]     input: [ [Object], [Object] ],
[1]     temperature: undefined,
[1]     top_p: undefined,
[1]     max_output_tokens: undefined,
[1]     conversation: undefined,
[1]     max_tool_calls: undefined,
[1]     metadata: undefined,
[1]     parallel_tool_calls: undefined,
[1]     previous_response_id: undefined,
[1]     store: undefined,
[1]     user: undefined,
[1]     instructions: undefined,
[1]     service_tier: undefined,
[1]     include: undefined,
[1]     prompt_cache_key: undefined,
[1]     prompt_cache_retention: undefined,
[1]     safety_identifier: undefined,
[1]     top_logprobs: undefined,
[1]     truncation: undefined,
[1]     tools: [ [Object], [Object], [Object], [Object] ],
[1]     tool_choice: 'auto',
[1]     stream: true
[1]   },
[1]   statusCode: 404,
[1]   responseHeaders: {
[1]     'access-control-allow-credentials': 'true',
[1]     connection: 'keep-alive',
[1]     'content-length': '0',
[1]     date: 'Tue, 07 Apr 2026 22:48:57 GMT',
[1]     server: 'elb',
[1]     'strict-transport-security': 'max-age=31536000; includeSubDomains; preload',
[1]     vary: 'origin, access-control-request-method, access-control-request-headers',
[1]     via: '1.1 83867089cd39052cd05f9e04909bedde.cloudfront.net (CloudFront)',
[1]     'x-amz-cf-id': 'O4b0VTi9Q1VVmTmq6czGlEWst7IPnAQl544hB7uIvfnSphBvUKbZTw==',
[1]     'x-amz-cf-pop': 'DUB56-P3',
[1]     'x-cache': 'Error from cloudfront',
[1]     'x-content-type-options': 'nosniff',
[1]     'x-ds-trace-id': 'a90e91809d285170338ef077f67ae2be'
[1]   },
[1]   responseBody: '',
[1]   isRetryable: false,
[1]   data: undefined,
[1]   Symbol(vercel.ai.error): true,
[1]   Symbol(vercel.ai.error.AI_APICallError): true
[1] }
[1] Exception Captured
[1]   undefined
[1]   [
[1]     NoOutputGeneratedError [AI_NoOutputGeneratedError]: No output generated. Check the stream for errors.
[1]         at Object.flush (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:6656:103)
[1]         at invokePromiseCallback (node:internal/webstreams/util:172:10)
[1]         at Object.<anonymous> (node:internal/webstreams/util:177:23)
[1]         at transformStreamDefaultSinkCloseAlgorithm (node:internal/webstreams/transformstream:621:43)
[1]         at node:internal/webstreams/transformstream:379:11
[1]         at writableStreamDefaultControllerProcessClose (node:internal/webstreams/writablestream:1162:28)
[1]         at writableStreamDefaultControllerAdvanceQueueIfNeeded (node:internal/webstreams/writablestream:1253:5)
[1]         at writableStreamDefaultControllerClose (node:internal/webstreams/writablestream:1220:3)
[1]         at writableStreamClose (node:internal/webstreams/writablestream:722:3)
[1]         at writableStreamDefaultWriterClose (node:internal/webstreams/writablestream:1091:10) {
[1]       cause: undefined,
[1]       Symbol(vercel.ai.error): true,
[1]       Symbol(vercel.ai.error.AI_NoOutputGeneratedError): true
[1]     }
[1]   ]
[1] [Nest] 50907  - 07/04/2026, 23:48:59     LOG [BullMQDriver] Job 24 with name StreamAgentChatJob processed on queue ai-stream-queue in 2756.63ms
2026-04-08 12:01:49 +00:00
WeikoandGitHub 8aa208fc93 Fix overridable entities logic for SSE (#19433)
## Context
SSE metadata events for overridable entities (viewField, viewFieldGroup,
pageLayoutWidget, pageLayoutTab) were sending raw flat entity data
without resolving overrides into base properties or filtering isActive:
false entities. This caused the frontend to display stale/incorrect
values (e.g., a hidden viewField still appearing visible).

## Implementation
Add a sanitization step in
MetadataEventPublisher.enrichMetadataEventBatch that resolves overrides
and converts isActive transitions into the appropriate event types
(deactivated -> delete, reactivated -> create), matching what GraphQL
resolvers already do
2026-04-08 11:53:55 +00:00
Thomas TrompetteandGitHub 9a58f3d459 Add a lock on function creation (#19428)
Fixes
https://twenty-v7.sentry.io/issues/7368127776/?environment=prod&project=4507072499810304&query=is%3Aunresolved&referrer=issue-stream

- Add a distributed lock (via CacheLockService) around the Lambda
executor build in LambdaDriver to prevent concurrent workflow runs from
racing on CreateFunctionCommand for the same logic function
- Use double-checked locking: isAlreadyBuilt is checked lock-free first
(fast path), then re-checked inside the lock to avoid redundant rebuilds
2026-04-08 11:50:57 +00:00
Baptiste DevessierandGitHub 9d96e529c8 Fix last widget bottom bar inconsistencies (#19440)
Widgets weren't sorted
2026-04-08 11:29:11 +00:00
Baptiste DevessierandGitHub e9310555fb Fix tab selection in layout edit mode (#19436)
https://github.com/user-attachments/assets/20e4a506-5f22-4017-8300-a494b70e8d51
2026-04-08 11:12:46 +00:00
8090fa4364 i18n - docs translations (#19437)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 12:40:41 +02:00
martmullandGitHub 90de1d4a34 Add twenty sync command (#19413)
Add one shot app synchronisation `twenty sync command` command

Complementary with `twenty app dev` command which is watch mode

Fixes
https://discord.com/channels/1130383047699738754/1489644493106839663
2026-04-08 09:26:42 +00:00
8026451220 chore: sync AI model catalog from models.dev (#19426)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-08 08:27:14 +02:00
Arturo MantinettiandGitHub b3d46b0fa3 feat: Add support for CLF currency code (#19420)
## Summary
This PR adds support for the **CLF (Unidad de Fomento)** currency code
across the application.

## Changes
- Added `CLF` to the supported currency list
- Updated validation logic to recognize CLF as a valid currency
- Adjusted formatting and handling where applicable

## Motivation
CLF is a widely used unit of account in Chile, commonly used for
financial operations such as real estate, contracts, and indexed
payments.
Supporting CLF improves localization and enables better adoption of
Twenty CRM in the Chilean market.

## Files Modified
- Updated 3 files to integrate CLF support (currency configuration,
validation, and related logic)

## Testing
- Verified that CLF can be selected and processed correctly
- Confirmed no regression in existing currency behavior
2026-04-08 05:10:36 +00:00
a8085db5fd i18n - translations (#19425)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 07:09:03 +02:00
b540ae9735 i18n - translations (#19424)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 07:05:51 +02:00
2010 changed files with 111016 additions and 67239 deletions
+22
View File
@@ -0,0 +1,22 @@
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "bash",
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
"env": {}
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["@playwright/mcp@latest", "--no-sandbox", "--headless"],
"env": {}
},
"context7": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"],
"env": {}
}
}
}
+223
View File
@@ -0,0 +1,223 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Twenty is an open-source CRM built with modern technologies in a monorepo structure. The codebase is organized as an Nx workspace with multiple packages.
## Key Commands
### Development
```bash
# Start development environment (frontend + backend + worker)
yarn start
# Individual package development
npx nx start twenty-front # Start frontend dev server
npx nx start twenty-server # Start backend server
npx nx run twenty-server:worker # Start background worker
```
### Testing
```bash
# Preferred: run a single test file (fast)
npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
# Run all tests for a package
npx nx test twenty-front # Frontend unit tests
npx nx test twenty-server # Backend unit tests
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
# To run an indivual test or a pattern of tests, use the following command:
cd packages/{workspace} && npx jest "pattern or filename"
# Storybook
npx nx storybook:build twenty-front
npx nx storybook:test twenty-front
# When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
```
### Code Quality
```bash
# Linting (diff with main - fastest, always prefer this)
npx nx lint:diff-with-main twenty-front
npx nx lint:diff-with-main twenty-server
npx nx lint:diff-with-main twenty-front --configuration=fix # Auto-fix
# Linting (full project - slower, use only when needed)
npx nx lint twenty-front
npx nx lint twenty-server
# Type checking
npx nx typecheck twenty-front
npx nx typecheck twenty-server
# Format code
npx nx fmt twenty-front
npx nx fmt twenty-server
```
### Build
```bash
# Build packages (twenty-shared must be built first)
npx nx build twenty-shared
npx nx build twenty-front
npx nx build twenty-server
```
### Database Operations
```bash
# Database management
npx nx database:reset twenty-server # Reset database
npx nx run twenty-server:database:init:prod # Initialize database
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
# Generate an instance command (fast or slow)
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
```
### Database Inspection (Postgres MCP)
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
- Inspect workspace data, metadata, and object definitions while developing
- Verify migration results (columns, types, constraints) after running migrations
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
- Inspect metadata tables to debug GraphQL schema generation issues
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
### GraphQL
```bash
# Generate GraphQL types (run after schema changes)
npx nx run twenty-front:graphql:generate
npx nx run twenty-front:graphql:generate --configuration=metadata
```
## Architecture Overview
### Tech Stack
- **Frontend**: React 18, TypeScript, Jotai (state management), Linaria (styling), Vite
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
- **Monorepo**: Nx workspace managed with Yarn 4
### Package Structure
```
packages/
├── twenty-front/ # React frontend application
├── twenty-server/ # NestJS backend API
├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email
├── twenty-website/ # Next.js documentation website
├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests
```
### Key Development Principles
- **Functional components only** (no class components)
- **Named exports only** (no default exports)
- **Types over interfaces** (except when extending third-party interfaces)
- **String literals over enums** (except for GraphQL enums)
- **No 'any' type allowed** — strict TypeScript enforced
- **Event handlers preferred over useEffect** for state updates
- **Props down, events up** — unidirectional data flow
- **Composition over inheritance**
- **No abbreviations** in variable names (`user` not `u`, `fieldMetadata` not `fm`)
### Naming Conventions
- **Variables/functions**: camelCase
- **Constants**: SCREAMING_SNAKE_CASE
- **Types/Classes**: PascalCase (suffix component props with `Props`, e.g. `ButtonProps`)
- **Files/directories**: kebab-case with descriptive suffixes (`.component.tsx`, `.service.ts`, `.entity.ts`, `.dto.ts`, `.module.ts`)
- **TypeScript generics**: descriptive names (`TData` not `T`)
### File Structure
- Components under 300 lines, services under 500 lines
- Components in their own directories with tests and stories
- Use `index.ts` barrel exports for clean imports
- Import order: external libraries first, then internal (`@/`), then relative
### Comments
- Use short-form comments (`//`), not JSDoc blocks
- Explain WHY (business logic), not WHAT
- Do not comment obvious code
- Multi-line comments use multiple `//` lines, not `/** */`
### State Management
- **Jotai** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
- Component-specific state with React hooks (`useState`, `useReducer` for complex logic)
- GraphQL cache managed by Apollo Client
- Use functional state updates: `setState(prev => prev + 1)`
### Backend Architecture
- **NestJS modules** for feature organization
- **TypeORM** for database ORM with PostgreSQL
- **GraphQL** API with code-first approach
- **Redis** for caching and session management
- **BullMQ** for background job processing
### Database & Upgrade Commands
- **PostgreSQL** as primary database
- **Redis** for caching and sessions
- **ClickHouse** for analytics (when enabled)
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
- Include both `up` and `down` logic in instance commands
- Never delete or rewrite committed instance command `up`/`down` logic
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
### Utility Helpers
Use existing helpers from `twenty-shared` instead of manual type guards:
- `isDefined()`, `isNonEmptyString()`, `isNonEmptyArray()`
## Development Workflow
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
### Before Making Changes
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
2. Test changes with relevant test suites (prefer single-file test runs)
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
4. Check that GraphQL schema changes are backward compatible
5. Run `graphql:generate` after any GraphQL schema changes
### Code Style Notes
- Use **Linaria** for styling with zero-runtime CSS-in-JS (styled-components pattern)
- Follow **Nx** workspace conventions for imports
- Use **Lingui** for internationalization
- Apply security first, then formatting (sanitize before format)
### Testing Strategy
- **Test behavior, not implementation** — focus on user perspective
- **Test pyramid**: 70% unit, 20% integration, 10% E2E
- Query by user-visible elements (text, roles, labels) over test IDs
- Use `@testing-library/user-event` for realistic interactions
- Descriptive test names: "should [behavior] when [condition]"
- Clear mocks between tests with `jest.clearAllMocks()`
## Dev Environment Setup
All dev environments (Claude Code web, Cursor, local) use one script:
```bash
bash packages/twenty-utils/setup-dev-env.sh
```
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times.
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
- `--down` — stop services
- `--reset` — wipe data and restart fresh
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script.
## Important Files
- `nx.json` - Nx workspace configuration with task definitions
- `tsconfig.base.json` - Base TypeScript configuration
- `package.json` - Root package with workspace definitions
- `.cursor/rules/` - Detailed development guidelines and best practices
+3 -5
View File
@@ -12,7 +12,7 @@ This directory contains Twenty's development guidelines and best practices in th
### Core Guidelines
- **architecture.mdc** - Project overview, technology stack, and infrastructure setup (Always Applied)
- **nx-rules.mdc** - Nx workspace guidelines and best practices (Auto-attached to Nx files)
- **server-migrations.mdc** - Backend migration and TypeORM guidelines for `twenty-server` (Auto-attached to server entities and migration files)
- **server-migrations.mdc** - Upgrade command guidelines (instance commands and workspace commands) for `twenty-server` (Auto-attached to server entities and upgrade command files)
- **creating-syncable-entity.mdc** - Comprehensive guide for creating new syncable entities (with universalIdentifier and applicationId) in the workspace migration system (Agent-requested for metadata-modules and workspace-migration files)
### Code Quality
@@ -81,10 +81,8 @@ npx nx run twenty-server:typecheck # Type checking
npx nx run twenty-server:test # Run unit tests
npx nx run twenty-server:test:integration:with-db-reset # Run integration tests
# Migrations
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts
# Workspace
# Upgrade commands (instance + workspace)
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
```
## Usage Guidelines
+2 -1
View File
@@ -55,7 +55,8 @@ If feature descriptions are not provided or need enhancement, research the codeb
- Services: Look for `*.service.ts` files
**For Database/ORM Changes:**
- Migrations: `packages/twenty-server/src/database/typeorm/`
- Instance commands (fast/slow): `packages/twenty-server/src/database/commands/upgrade-version-command/`
- Legacy TypeORM migrations: `packages/twenty-server/src/database/typeorm/`
- Entities: `packages/twenty-server/src/entities/`
### Research Commands
+1 -1
View File
@@ -22,7 +22,7 @@ This main guide provides a high-level overview and navigation hub.
A syncable entity is a metadata entity that:
- Has a **`universalIdentifier`**: A unique identifier used for syncing entities across workspaces/applications
- Has an **`applicationId`**: Links the entity to an application (Twenty Standard or Custom applications)
- Has an **`applicationId`**: Links the entity to an application (Standard or Custom applications)
- Participates in the **workspace migration system**: Can be created, updated, and deleted through the migration pipeline
- Is **cached as a flat entity**: Denormalized representation for efficient validation and change detection
+31 -14
View File
@@ -1,29 +1,46 @@
---
description: Guidelines for generating and managing TypeORM migrations in twenty-server
description: Guidelines for generating and managing upgrade commands (instance commands and workspace commands) in twenty-server
globs: [
"packages/twenty-server/src/**/*.entity.ts",
"packages/twenty-server/src/database/typeorm/**/*.ts"
"packages/twenty-server/src/database/commands/upgrade-version-command/**/*.ts"
]
alwaysApply: false
---
## Server Migrations (twenty-server)
## Upgrade Commands (twenty-server)
- **When changing an entity, always generate a migration**
- If you modify a `*.entity.ts` file in `packages/twenty-server/src`, you **must** generate a corresponding TypeORM migration instead of manually editing the database schema.
- Use the Nx + TypeORM command from the project root:
The upgrade system uses two types of commands instead of raw TypeORM migrations:
- **Instance commands** — schema and data migrations that run once at the instance level.
- **Workspace commands** — commands that iterate over all active/suspended workspaces.
See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation.
### Instance Commands
- **When changing a `*.entity.ts` file**, generate an instance command:
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
```
- Replace `[name]` with a descriptive, kebab-case migration name that reflects the change (for example, `add-agent-turn-evaluation`).
- **Fast commands** (`--type fast`, default) are for schema-only changes that must run immediately. They implement `FastInstanceCommand` with `up`/`down` methods and use the `@RegisteredInstanceCommand` decorator.
- **Prefer generated migrations over manual edits**
- Let TypeORM infer schema changes from the updated entities; only adjust the generated migration file manually if absolutely necessary (for example, for data backfills or complex constraints).
- Keep schema changes (DDL) in these generated migrations and avoid mixing in heavy data migrations unless there is a strong reason and clear comments.
- **Slow commands** (`--type slow`) add a `runDataMigration` method for potentially long-running data backfills that execute before `up`. They only run when `--include-slow` is passed. Use the decorator with `{ type: 'slow' }`.
- **Keep migrations consistent and reversible**
- Ensure the generated migration includes both `up` and `down` logic that correctly applies and reverts the entity change when possible.
- Do not delete or rewrite existing, committed migrations unless you are explicitly working on a pre-release branch where history rewrites are allowed by team conventions.
- The generator auto-registers the command in `instance-commands.constant.ts` — do not edit that file manually.
- **Keep commands consistent and reversible**: include both `up` and `down` logic. Do not delete or rewrite existing, committed commands unless on a pre-release branch.
### Workspace Commands
- Use the `@RegisteredWorkspaceCommand` decorator alongside nest-commander's `@Command` decorator.
- Extend `ActiveOrSuspendedWorkspaceCommandRunner` and implement `runOnWorkspace`.
- The base class provides `--dry-run`, `--verbose`, and workspace filter options automatically.
### Execution Order
Within a given version, commands run in this order (timestamp-sorted within each group):
1. Instance fast commands
2. Instance slow commands (only with `--include-slow`)
3. Workspace commands
@@ -0,0 +1,53 @@
name: Deploy Twenty App
description: Build and deploy a Twenty app to a remote instance
inputs:
api-url:
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
required: true
api-key:
description: API key or access token for the target instance
required: true
app-path:
description: Path to the app directory (relative to repo root). Defaults to repo root.
required: false
default: '.'
runs:
using: composite
steps:
- name: Enable Corepack
shell: bash
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn install --immutable
- name: Configure remote
shell: bash
run: |
mkdir -p ~/.twenty
node -e "
const fs = require('fs'), path = require('path'), os = require('os');
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
version: 1,
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY } }
}, null, 2));
"
env:
API_URL: ${{ inputs.api-url }}
API_KEY: ${{ inputs.api-key }}
- name: Deploy
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty deploy --remote target
@@ -0,0 +1,53 @@
name: Install Twenty App
description: Install (or upgrade) a Twenty app on a specific workspace
inputs:
api-url:
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
required: true
api-key:
description: API key or access token for the target workspace
required: true
app-path:
description: Path to the app directory (relative to repo root). Defaults to repo root.
required: false
default: '.'
runs:
using: composite
steps:
- name: Enable Corepack
shell: bash
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn install --immutable
- name: Configure remote
shell: bash
run: |
mkdir -p ~/.twenty
node -e "
const fs = require('fs'), path = require('path'), os = require('os');
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
version: 1,
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY } }
}, null, 2));
"
env:
API_URL: ${{ inputs.api-url }}
API_KEY: ${{ inputs.api-key }}
- name: Install
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty install --remote target
@@ -15,8 +15,8 @@ outputs:
description: 'URL where the Twenty test server can be reached'
value: http://localhost:2021
api-key:
description: 'API key for the Twenty test instance'
value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4
description: 'API key (type: API_KEY) for the seeded Twenty dev workspace'
value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc
runs:
using: 'composite'
@@ -251,6 +251,14 @@ jobs:
rm -f /tmp/current-server.pid
fi
- name: Flush Redis between server runs
run: |
# Clear all Redis caches to prevent stale data from the current branch
# server contaminating the main branch server. Both servers share the
# same Redis instance, and CoreEntityCacheService/WorkspaceCacheService
# persist cached entities across process restarts.
redis-cli -h localhost -p 6379 FLUSHALL || echo "::warning::Failed to flush Redis"
- name: Checkout main branch
run: |
git stash
@@ -34,8 +34,27 @@ jobs:
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
@@ -120,14 +139,27 @@ jobs:
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Spawn Twenty test instance
id: twenty
uses: ./.github/actions/spawn-twenty-app-dev-test
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ steps.twenty.outputs.api-key }} --api-url ${{ steps.twenty.outputs.server-url }}
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
@@ -154,9 +186,6 @@ jobs:
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
@@ -32,8 +32,27 @@ jobs:
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
@@ -114,29 +133,29 @@ jobs:
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Spawn Twenty test instance
id: twenty
uses: ./.github/actions/spawn-twenty-app-dev-test
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ steps.twenty.outputs.api-key }} --api-url ${{ steps.twenty.outputs.server-url }}
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty deploy
- name: Install scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty install
- name: Run scaffolded app integration test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
- name: Run scaffolded app integration test (deploys, installs, and verifies the app)
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
@@ -34,8 +34,27 @@ jobs:
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
@@ -118,14 +137,27 @@ jobs:
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Spawn Twenty test instance
id: twenty
uses: ./.github/actions/spawn-twenty-app-dev-test
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ steps.twenty.outputs.api-key }} --api-url ${{ steps.twenty.outputs.server-url }}
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
@@ -152,9 +184,6 @@ jobs:
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
@@ -30,8 +30,28 @@ jobs:
example-app-hello-world:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 15
timeout-minutes: 30
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -42,15 +62,25 @@ jobs:
- name: Build SDK packages
run: npx nx build twenty-sdk
- name: Spawn Twenty test instance
id: twenty
uses: ./.github/actions/spawn-twenty-app-dev-test
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Run integration tests
working-directory: packages/twenty-apps/examples/hello-world
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
run: npx vitest run
ci-example-app-hello-world-status-check:
+37 -7
View File
@@ -30,8 +30,28 @@ jobs:
example-app-postcard:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 15
timeout-minutes: 30
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -42,15 +62,25 @@ jobs:
- name: Build SDK packages
run: npx nx build twenty-sdk
- name: Spawn Twenty test instance
id: twenty
uses: ./.github/actions/spawn-twenty-app-dev-test
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Run integration tests
working-directory: packages/twenty-apps/examples/postcard
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
run: npx vitest run
ci-example-app-postcard-status-check:
+17 -7
View File
@@ -19,6 +19,7 @@ jobs:
files: |
packages/twenty-sdk/**
packages/twenty-server/**
.github/workflows/ci-sdk.yaml
!packages/twenty-sdk/package.json
sdk-test:
needs: changed-files-check
@@ -69,7 +70,6 @@ jobs:
ports:
- 6379:6379
env:
NODE_ENV: test
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
@@ -79,16 +79,26 @@ jobs:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
- name: Build SDK
run: npx nx build twenty-sdk
- name: Server / Create Test DB
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server > /tmp/twenty-server.log 2>&1 &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: SDK / Run e2e Tests
uses: ./.github/actions/nx-affected
with:
tag: scope:sdk
tasks: test:e2e
run: NODE_ENV=test npx vitest run --config ./vitest.e2e.config.ts
working-directory: packages/twenty-sdk
- name: Server / Dump logs on failure
if: failure()
run: tail -100 /tmp/twenty-server.log || echo "No server log file found"
ci-sdk-status-check:
if: always() && !cancelled()
timeout-minutes: 5
+9 -6
View File
@@ -144,15 +144,18 @@ jobs:
exit 1
- name: Server / Check for Pending Migrations
run: |
CORE_MIGRATION_OUTPUT=$(npx nx database:migrate:generate twenty-server -- --name core-migration-check || true)
npx nx database:migrate:generate twenty-server -- --name pending-migration-check || true
CORE_MIGRATION_FILE=$(ls packages/twenty-server/src/database/typeorm/core/migrations/common/*core-migration-check.ts 2>/dev/null || echo "")
if [ -n "$CORE_MIGRATION_FILE" ]; then
if ! git diff --quiet; then
echo "::error::Unexpected migration files were generated. Please run 'npx nx database:migrate:generate twenty-server -- --name <migration-name>' and commit the result."
echo "$CORE_MIGRATION_OUTPUT"
echo ""
echo "The following migration changes were detected:"
echo "==================================================="
git diff
echo "==================================================="
echo ""
rm -f packages/twenty-server/src/database/typeorm/core/migrations/common/*core-migration-check.ts
git checkout -- .
exit 1
fi
+4 -3
View File
@@ -69,13 +69,14 @@ jobs:
- name: Server / Start
run: |
npx nx start twenty-server &
npx nx run twenty-server:start:ci &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
timeout 60 bash -c 'until curl -sf http://localhost:3000/healthz; do sleep 2; done'
- name: Start worker
working-directory: packages/twenty-server
run: |
npx nx run twenty-server:worker &
NODE_ENV=development node dist/queue-worker/queue-worker.js &
echo "Worker started"
- name: Zapier / Build
+1
View File
@@ -53,3 +53,4 @@ mcp.json
TRANSLATION_QA_REPORT.md
.playwright-mcp/
.playwright-cli/
output/playwright/
+12 -9
View File
@@ -71,10 +71,10 @@ npx nx build twenty-server
# Database management
npx nx database:reset twenty-server # Reset database
npx nx run twenty-server:database:init:prod # Initialize database
npx nx run twenty-server:database:migrate:prod # Run migrations
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
# Generate migration (replace [name] with kebab-case descriptive name)
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
# Generate an instance command (fast or slow)
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
```
### Database Inspection (Postgres MCP)
@@ -158,14 +158,17 @@ packages/
- **Redis** for caching and session management
- **BullMQ** for background job processing
### Database & Migrations
### Database & Upgrade Commands
- **PostgreSQL** as primary database
- **Redis** for caching and sessions
- **ClickHouse** for analytics (when enabled)
- Always generate migrations when changing entity files
- Migration names must be kebab-case (e.g. `add-agent-turn-evaluation`)
- Include both `up` and `down` logic in migrations
- Never delete or rewrite committed migrations
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
- Include both `up` and `down` logic in instance commands
- Never delete or rewrite committed instance command `up`/`down` logic
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
### Utility Helpers
Use existing helpers from `twenty-shared` instead of manual type guards:
@@ -178,7 +181,7 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
### Before Making Changes
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
2. Test changes with relevant test suites (prefer single-file test runs)
3. Ensure database migrations are generated for entity changes
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
4. Check that GraphQL schema changes are backward compatible
5. Run `graphql:generate` after any GraphQL schema changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.8.0",
"version": "1.22.0-canary.6",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -0,0 +1,42 @@
name: CD
on:
push:
branches:
- main
pull_request:
types: [labeled]
permissions:
contents: read
env:
TWENTY_DEPLOY_URL: http://localhost:3000
concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy-and-install:
if: >-
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
- name: Install
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
@@ -25,7 +25,7 @@ jobs:
- name: Spawn Twenty test instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@feature/sdk-config-file-source-of-truth
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
@@ -7,9 +7,7 @@
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"keywords": [
"twenty-app"
],
"keywords": [],
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty",
@@ -1,70 +0,0 @@
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(
`Build failed: ${buildResult.error?.message ?? 'Unknown error'}`,
);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(
`Deploy failed: ${deployResult.error?.message ?? 'Unknown error'}`,
);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(
`Install failed: ${installResult.error?.message ?? 'Unknown error'}`,
);
}
});
afterAll(async () => {
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
);
}
});
it('should find the installed app in the applications list', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(application: { universalIdentifier: string }) =>
application.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
@@ -0,0 +1,87 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
function validateEnv(): { apiUrl: string; apiKey: string } {
const apiUrl = process.env.TWENTY_API_URL;
const apiKey = process.env.TWENTY_API_KEY;
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
return { apiUrl, apiKey };
}
async function checkServer(apiUrl: string) {
let response: Response;
try {
response = await fetch(`${apiUrl}/healthz`);
} catch {
throw new Error(
`Twenty server is not reachable at ${apiUrl}. ` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
}
}
function writeConfig(apiUrl: string, apiKey: string) {
const payload = JSON.stringify(
{
remotes: {
local: { apiUrl, apiKey, accessToken: apiKey },
},
defaultRemote: 'local',
},
null,
2,
);
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload);
}
export async function setup() {
const { apiUrl, apiKey } = validateEnv();
await checkServer(apiUrl);
writeConfig(apiUrl, apiKey);
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({
appPath: APP_PATH,
onProgress: (message: string) => console.log(`[dev] ${message}`),
});
if (!result.success) {
throw new Error(
`Dev sync failed: ${result.error?.message ?? 'Unknown error'}`,
);
}
}
export async function teardown() {
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
);
}
}
@@ -0,0 +1,46 @@
import { CoreApiClient } from 'twenty-client-sdk/core';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { describe, expect, it } from 'vitest';
describe('App installation', () => {
it('should find the installed app in the applications list', async () => {
const client = new MetadataApiClient();
const result = await client.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const app = result.findManyApplications.find(
(a: { universalIdentifier: string }) =>
a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(app).toBeDefined();
});
});
describe('CoreApiClient', () => {
it('should support CRUD on standard objects', async () => {
const client = new CoreApiClient();
const created = await client.mutation({
createNote: {
__args: { data: { title: 'Integration test note' } },
id: true,
},
});
expect(created.createNote.id).toBeDefined();
await client.mutation({
destroyNote: {
__args: { id: created.createNote.id },
id: true,
},
});
});
});
@@ -1,53 +0,0 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json');
beforeAll(async () => {
const apiUrl = process.env.TWENTY_API_URL!;
const token = process.env.TWENTY_API_KEY!;
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
let response: Response;
try {
response = await fetch(`${apiUrl}/healthz`);
} catch {
throw new Error(
`Twenty server is not reachable at ${apiUrl}. ` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
}
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
CONFIG_PATH,
JSON.stringify(
{
remotes: {
local: { apiUrl, apiKey: token },
},
defaultRemote: 'local',
},
null,
2,
),
);
process.env.TWENTY_APP_ACCESS_TOKEN ??= token;
});
@@ -1,6 +1,15 @@
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY =
process.env.TWENTY_API_KEY ??
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
@@ -11,14 +20,12 @@ export default defineConfig({
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
TWENTY_API_KEY:
process.env.TWENTY_API_KEY ??
// Tim Apple (admin) access token for twenty-app-dev
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4',
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
@@ -0,0 +1,42 @@
name: CD
on:
push:
branches:
- main
pull_request:
types: [labeled]
permissions:
contents: read
env:
TWENTY_DEPLOY_URL: http://localhost:3000
concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy-and-install:
if: >-
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
- name: Install
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
@@ -25,7 +25,7 @@ jobs:
- name: Spawn Twenty test instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@feature/sdk-config-file-source-of-truth
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
@@ -16,8 +16,8 @@
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "0.8.0-canary.5",
"twenty-sdk": "0.8.0-canary.5"
"twenty-client-sdk": "0.9.0",
"twenty-sdk": "0.9.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
@@ -1,13 +1,19 @@
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import {
definePostInstallLogicFunction,
type InstallLogicFunctionPayload,
} from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
console.log(
'Post install logic function executed successfully!',
payload.previousVersion,
);
};
export default definePostInstallLogicFunction({
universalIdentifier: '8c726dcc-1709-4eac-aa8b-f99960a9ec1b',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
timeoutSeconds: 30,
handler,
});
@@ -17,8 +17,7 @@ export default defineConfig({
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
TWENTY_API_KEY:
process.env.TWENTY_API_KEY ??
// Tim Apple (admin) access token for twenty-app-dev
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc',
},
},
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
name: CD
on:
push:
branches:
- main
pull_request:
types: [labeled]
permissions:
contents: read
env:
TWENTY_DEPLOY_URL: http://localhost:3000
concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy-and-install:
if: >-
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
- name: Install
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
@@ -25,7 +25,7 @@ jobs:
- name: Spawn Twenty test instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@feature/sdk-config-file-source-of-truth
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
@@ -19,8 +19,8 @@
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "0.8.0-canary.8",
"twenty-sdk": "0.8.0-canary.8"
"twenty-client-sdk": "0.9.0",
"twenty-sdk": "0.9.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
@@ -1,70 +0,0 @@
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application.config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(
`Build failed: ${buildResult.error?.message ?? 'Unknown error'}`,
);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(
`Deploy failed: ${deployResult.error?.message ?? 'Unknown error'}`,
);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(
`Install failed: ${installResult.error?.message ?? 'Unknown error'}`,
);
}
});
afterAll(async () => {
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
);
}
});
it('should find the installed app in the applications list', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(application: { universalIdentifier: string }) =>
application.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
@@ -0,0 +1,87 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
function validateEnv(): { apiUrl: string; apiKey: string } {
const apiUrl = process.env.TWENTY_API_URL;
const apiKey = process.env.TWENTY_API_KEY;
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
return { apiUrl, apiKey };
}
async function checkServer(apiUrl: string) {
let response: Response;
try {
response = await fetch(`${apiUrl}/healthz`);
} catch {
throw new Error(
`Twenty server is not reachable at ${apiUrl}. ` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
}
}
function writeConfig(apiUrl: string, apiKey: string) {
const payload = JSON.stringify(
{
remotes: {
local: { apiUrl, apiKey, accessToken: apiKey },
},
defaultRemote: 'local',
},
null,
2,
);
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload);
}
export async function setup() {
const { apiUrl, apiKey } = validateEnv();
await checkServer(apiUrl);
writeConfig(apiUrl, apiKey);
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({
appPath: APP_PATH,
onProgress: (message: string) => console.log(`[dev] ${message}`),
});
if (!result.success) {
throw new Error(
`Dev sync failed: ${result.error?.message ?? 'Unknown error'}`,
);
}
}
export async function teardown() {
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
);
}
}
@@ -0,0 +1,63 @@
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application.config';
import { describe, expect, it } from 'vitest';
describe('App installation', () => {
it('should find the installed app in the applications list', async () => {
const client = new MetadataApiClient();
const result = await client.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const app = result.findManyApplications.find(
(a: { universalIdentifier: string }) =>
a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(app).toBeDefined();
});
});
describe('PostCard object', () => {
it('should exist with expected fields and relations', async () => {
const client = new MetadataApiClient();
const { objects } = await client.query({
objects: {
__args: {
filter: { isCustom: { is: true } },
paging: { first: 50 },
},
edges: {
node: {
nameSingular: true,
fields: {
__args: { paging: { first: 500 } },
edges: { node: { name: true } },
},
},
},
},
});
const obj = objects.edges
.map((e: { node: { nameSingular: string } }) => e.node)
.find((n: { nameSingular: string }) => n.nameSingular === 'postCard');
expect(obj).toBeDefined();
const names = obj!.fields.edges.map(
(e: { node: { name: string } }) => e.node.name,
);
expect(names).toContain('name');
expect(names).toContain('content');
expect(names).toContain('status');
expect(names).toContain('deliveredAt');
expect(names).toContain('recipient');
});
});
@@ -1,53 +0,0 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json');
beforeAll(async () => {
const apiUrl = process.env.TWENTY_API_URL!;
const token = process.env.TWENTY_API_KEY!;
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
let response: Response;
try {
response = await fetch(`${apiUrl}/healthz`);
} catch {
throw new Error(
`Twenty server is not reachable at ${apiUrl}. ` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
}
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
CONFIG_PATH,
JSON.stringify(
{
remotes: {
local: { apiUrl, apiKey: token },
},
defaultRemote: 'local',
},
null,
2,
),
);
process.env.TWENTY_APP_ACCESS_TOKEN ??= token;
});
@@ -0,0 +1,35 @@
import { CoreApiClient } from 'twenty-client-sdk/core';
import { definePostInstallLogicFunction } from 'twenty-sdk';
const SEED_POST_CARDS = [
{
name: 'Greetings from Paris',
content: 'Wish you were here! The Eiffel Tower is breathtaking.',
},
{
name: 'Hello from Tokyo',
content: 'Cherry blossoms are in full bloom. Sending love!',
},
];
const handler = async () => {
const client = new CoreApiClient();
await client.mutation({
createPostCards: {
__args: { data: SEED_POST_CARDS as any },
id: true,
},
} as any);
console.log(`Seeded ${SEED_POST_CARDS.length} post cards on install.`);
return {};
};
export default definePostInstallLogicFunction({
universalIdentifier: '852c6321-1563-4396-b7c5-9d370f3d30a9',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 30,
handler,
});
@@ -0,0 +1,17 @@
import { definePreInstallLogicFunction } from 'twenty-sdk';
const handler = async (params: any) => {
console.log(
`Pre-install logic function executed successfully with params`,
params,
);
return {};
};
export default definePreInstallLogicFunction({
universalIdentifier: 'bf27f558-4ec6-481f-b76e-1dbcd05aef1f',
name: 'pre-install',
description: 'Runs before migrations to set up the application.',
timeoutSeconds: 10,
handler,
});
@@ -33,7 +33,7 @@ export default defineRole({
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fieldUniversalIdentifier: CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
canReadFieldValue: false,
canUpdateFieldValue: false,
canUpdateFieldValue: true,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
@@ -1,6 +1,15 @@
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY =
process.env.TWENTY_API_KEY ??
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
@@ -11,14 +20,12 @@ export default defineConfig({
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
TWENTY_API_KEY:
process.env.TWENTY_API_KEY ??
// Tim Apple (admin) access token for twenty-app-dev
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4',
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
@@ -3317,8 +3317,8 @@ __metadata:
oxlint: "npm:^0.16.0"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
twenty-client-sdk: "npm:0.8.0-canary.8"
twenty-sdk: "npm:0.8.0-canary.8"
twenty-client-sdk: "npm:0.9.0"
twenty-sdk: "npm:0.9.0"
typescript: "npm:^5.9.3"
vite-tsconfig-paths: "npm:^4.2.1"
vitest: "npm:^3.1.1"
@@ -4059,21 +4059,21 @@ __metadata:
languageName: node
linkType: hard
"twenty-client-sdk@npm:0.8.0-canary.8":
version: 0.8.0-canary.8
resolution: "twenty-client-sdk@npm:0.8.0-canary.8"
"twenty-client-sdk@npm:0.9.0":
version: 0.9.0
resolution: "twenty-client-sdk@npm:0.9.0"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
esbuild: "npm:^0.25.0"
graphql: "npm:^16.8.1"
checksum: 10c0/acb5bf952a9729811235a3474ccb4db82630c53a2caed03176bfb4f442576ee2ef7e625886f63714e2534f7ec83b03d83e4c0b1170a0eb816c531acfc2c98010
checksum: 10c0/4b42a6622a9852fc3eca50c1131b116c5602af006ee5f1d3b4f1e97721bbc8ef5c2f3b60d9dee3ca9ca8c3c7ad4b292a7b55007b779b4c042997dbc29940ba54
languageName: node
linkType: hard
"twenty-sdk@npm:0.8.0-canary.8":
version: 0.8.0-canary.8
resolution: "twenty-sdk@npm:0.8.0-canary.8"
"twenty-sdk@npm:0.9.0":
version: 0.9.0
resolution: "twenty-sdk@npm:0.9.0"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
@@ -4093,7 +4093,7 @@ __metadata:
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
tinyglobby: "npm:^0.2.15"
twenty-client-sdk: "npm:0.8.0-canary.8"
twenty-client-sdk: "npm:0.9.0"
typescript: "npm:^5.9.2"
uuid: "npm:^13.0.0"
vite: "npm:^7.0.0"
@@ -4101,7 +4101,7 @@ __metadata:
zod: "npm:^4.1.11"
bin:
twenty: dist/cli.cjs
checksum: 10c0/a22cf071f41c19d769e68a995912a0bda0f087bd9a295534bf9e545544f6dfc84f284a477f9011fc135f2a44de9372b977a66de06454e760822119e76921ea69
checksum: 10c0/27f93e5edac3265f819abacc853598435718020258a95d55518562e086e42daabae784964b42005d8e4ff30d1d6f13a12a38c656e18c60bad98da3f579a8e1c3
languageName: node
linkType: hard
@@ -16,7 +16,8 @@
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
"dependencies": {
"twenty-sdk": "latest"
"twenty-sdk": "latest",
"twenty-client-sdk": "latest"
},
"devDependencies": {
"typescript": "^5.9.3",
@@ -14,7 +14,8 @@
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
"dependencies": {
"twenty-sdk": "latest"
"twenty-sdk": "latest",
"twenty-client-sdk": "latest"
},
"devDependencies": {
"typescript": "^5.9.3",
@@ -14,7 +14,8 @@
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
"dependencies": {
"twenty-sdk": "latest"
"twenty-sdk": "latest",
"twenty-client-sdk": "latest"
},
"devDependencies": {
"typescript": "^5.9.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-client-sdk",
"version": "0.8.0",
"version": "1.22.0-canary.6",
"sideEffects": false,
"license": "AGPL-3.0",
"scripts": {
@@ -583,8 +583,9 @@ type ViewField {
workspaceId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
isActive: Boolean!
deletedAt: DateTime
isOverridden: Boolean!
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
}
enum AggregateOperations {
@@ -691,9 +692,10 @@ type ViewFieldGroup {
workspaceId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
isActive: Boolean!
deletedAt: DateTime
viewFields: [ViewField!]!
isOverridden: Boolean!
isOverridden: Boolean! @deprecated(reason: "isOverridden is deprecated")
}
type View {
@@ -896,10 +898,12 @@ type PageLayoutWidget {
position: PageLayoutWidgetPosition
configuration: WidgetConfiguration!
conditionalDisplay: JSON
conditionalAvailabilityExpression: String
createdAt: DateTime!
updatedAt: DateTime!
isActive: Boolean!
deletedAt: DateTime
isOverridden: Boolean!
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
}
enum WidgetType {
@@ -1169,6 +1173,7 @@ type FieldConfiguration {
"""Display mode for field configuration widgets"""
enum FieldDisplayMode {
CARD
EDITOR
FIELD
VIEW
}
@@ -1232,8 +1237,9 @@ type PageLayoutTab {
layoutMode: PageLayoutTabLayoutMode
createdAt: DateTime!
updatedAt: DateTime!
isActive: Boolean!
deletedAt: DateTime
isOverridden: Boolean!
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
}
type PageLayout {
@@ -1571,9 +1577,26 @@ enum LogicFunctionExecutionStatus {
ERROR
}
type BillingTrialPeriod {
duration: Float!
isCreditCardRequired: Boolean!
type FeatureFlag {
key: FeatureFlagKey!
value: Boolean!
}
enum FeatureFlagKey {
IS_UNIQUE_INDEXES_ENABLED
IS_JSON_FILTER_ENABLED
IS_AI_ENABLED
IS_COMMAND_MENU_ITEM_ENABLED
IS_MARKETPLACE_SETTING_TAB_VISIBLE
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
IS_PUBLIC_DOMAIN_ENABLED
IS_EMAILING_DOMAIN_ENABLED
IS_JUNCTION_RELATIONS_ENABLED
IS_DRAFT_EMAIL_ENABLED
IS_CONNECTED_ACCOUNT_MIGRATED
IS_RICH_TEXT_V1_MIGRATED
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
IS_DATASOURCE_MIGRATED
}
type WorkspaceUrls {
@@ -1581,6 +1604,89 @@ type WorkspaceUrls {
subdomainUrl: String!
}
type UserInfo {
id: UUID!
email: String!
firstName: String
lastName: String
createdAt: DateTime!
}
type WorkspaceInfo {
id: UUID!
name: String!
allowImpersonation: Boolean!
logo: String
totalUsers: Float!
activationStatus: WorkspaceActivationStatus!
createdAt: DateTime!
workspaceUrls: WorkspaceUrls!
users: [UserInfo!]!
featureFlags: [FeatureFlag!]!
}
type UserLookup {
user: UserInfo!
workspaces: [WorkspaceInfo!]!
}
type AdminPanelRecentUser {
id: UUID!
email: String!
firstName: String
lastName: String
createdAt: DateTime!
workspaceName: String
workspaceId: UUID
}
type AdminPanelTopWorkspace {
id: UUID!
name: String!
totalUsers: Int!
subdomain: String!
}
type AdminWorkspaceChatThread {
id: UUID!
title: String
totalInputTokens: Int!
totalOutputTokens: Int!
conversationSize: Int!
createdAt: DateTime!
updatedAt: DateTime!
}
type AdminChatMessagePart {
type: String!
textContent: String
toolName: String
}
type AdminChatMessage {
id: UUID!
role: AgentMessageRole!
parts: [AdminChatMessagePart!]!
createdAt: DateTime!
}
"""Role of a message in a chat thread"""
enum AgentMessageRole {
SYSTEM
USER
ASSISTANT
}
type AdminChatThreadMessages {
thread: AdminWorkspaceChatThread!
messages: [AdminChatMessage!]!
}
type BillingTrialPeriod {
duration: Float!
isCreditCardRequired: Boolean!
}
type SSOIdentityProvider {
id: UUID!
name: String!
@@ -1634,12 +1740,15 @@ type ClientAIModelConfig {
modelFamily: ModelFamily
modelFamilyLabel: String
sdkPackage: String
inputCostPerMillionTokensInCredits: Float!
outputCostPerMillionTokensInCredits: Float!
inputCostPerMillionTokens: Float
outputCostPerMillionTokens: Float
nativeCapabilities: NativeModelCapabilities
isDeprecated: Boolean
isRecommended: Boolean
providerName: String
providerLabel: String
contextWindowTokens: Float
maxOutputTokens: Float
dataResidency: String
}
@@ -1724,25 +1833,6 @@ type PublicFeatureFlag {
metadata: PublicFeatureFlagMetadata!
}
enum FeatureFlagKey {
IS_UNIQUE_INDEXES_ENABLED
IS_JSON_FILTER_ENABLED
IS_AI_ENABLED
IS_COMMAND_MENU_ITEM_ENABLED
IS_MARKETPLACE_SETTING_TAB_VISIBLE
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
IS_PUBLIC_DOMAIN_ENABLED
IS_EMAILING_DOMAIN_ENABLED
IS_JUNCTION_RELATIONS_ENABLED
IS_DRAFT_EMAIL_ENABLED
IS_CONNECTED_ACCOUNT_MIGRATED
IS_USAGE_ANALYTICS_ENABLED
IS_RICH_TEXT_V1_MIGRATED
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
IS_RECORD_TABLE_WIDGET_ENABLED
IS_DATASOURCE_MIGRATED
}
type ClientConfigMaintenanceMode {
startAt: DateTime!
endAt: DateTime!
@@ -1926,34 +2016,6 @@ type SystemHealth {
services: [SystemHealthService!]!
}
type FeatureFlag {
key: FeatureFlagKey!
value: Boolean!
}
type UserInfo {
id: UUID!
email: String!
firstName: String
lastName: String
}
type WorkspaceInfo {
id: UUID!
name: String!
allowImpersonation: Boolean!
logo: String
totalUsers: Float!
workspaceUrls: WorkspaceUrls!
users: [UserInfo!]!
featureFlags: [FeatureFlag!]!
}
type UserLookup {
user: UserInfo!
workspaces: [WorkspaceInfo!]!
}
type VersionInfo {
currentVersion: String
latestVersion: String!
@@ -2135,30 +2197,18 @@ type DeletedWorkspaceMember {
userWorkspaceId: UUID
}
type BillingEntitlement {
key: BillingEntitlementKey!
value: Boolean!
type Relation {
type: RelationType!
sourceObjectMetadata: Object!
targetObjectMetadata: Object!
sourceFieldMetadata: Field!
targetFieldMetadata: Field!
}
enum BillingEntitlementKey {
SSO
CUSTOM_DOMAIN
RLS
AUDIT_LOGS
}
type DomainRecord {
validationType: String!
type: String!
status: String!
key: String!
value: String!
}
type DomainValidRecords {
id: UUID!
domain: String!
records: [DomainRecord!]!
"""Relation type"""
enum RelationType {
ONE_TO_MANY
MANY_TO_ONE
}
type IndexEdge {
@@ -2260,25 +2310,6 @@ type ObjectFieldsConnection {
edges: [FieldEdge!]!
}
type UpsertRowLevelPermissionPredicatesResult {
predicates: [RowLevelPermissionPredicate!]!
predicateGroups: [RowLevelPermissionPredicateGroup!]!
}
type Relation {
type: RelationType!
sourceObjectMetadata: Object!
targetObjectMetadata: Object!
sourceFieldMetadata: Field!
targetFieldMetadata: Field!
}
"""Relation type"""
enum RelationType {
ONE_TO_MANY
MANY_TO_ONE
}
type FieldConnection {
"""Paging information"""
pageInfo: PageInfo!
@@ -2287,6 +2318,37 @@ type FieldConnection {
edges: [FieldEdge!]!
}
type BillingEntitlement {
key: BillingEntitlementKey!
value: Boolean!
}
enum BillingEntitlementKey {
SSO
CUSTOM_DOMAIN
RLS
AUDIT_LOGS
}
type DomainRecord {
validationType: String!
type: String!
status: String!
key: String!
value: String!
}
type DomainValidRecords {
id: UUID!
domain: String!
records: [DomainRecord!]!
}
type UpsertRowLevelPermissionPredicatesResult {
predicates: [RowLevelPermissionPredicate!]!
predicateGroups: [RowLevelPermissionPredicateGroup!]!
}
type AuthToken {
token: String!
expiresAt: DateTime!
@@ -2575,7 +2637,7 @@ type CommandMenuItem {
position: Float!
isPinned: Boolean!
availabilityType: CommandMenuItemAvailabilityType!
payload: JSON
payload: CommandMenuItemPayload
hotKeys: [String!]
conditionalAvailabilityExpression: String
availabilityObjectMetadataId: UUID
@@ -2659,6 +2721,16 @@ enum CommandMenuItemAvailabilityType {
FALLBACK
}
union CommandMenuItemPayload = PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload
type PathCommandMenuItemPayload {
path: String!
}
type ObjectMetadataCommandMenuItemPayload {
objectMetadataItemId: UUID!
}
type ToolIndexEntry {
name: String!
description: String!
@@ -2780,6 +2852,34 @@ type ConnectedAccountDTO {
updatedAt: DateTime!
}
type PublicConnectionParametersOutput {
host: String!
port: Float!
username: String
secure: Boolean
}
type PublicImapSmtpCaldavConnectionParameters {
IMAP: PublicConnectionParametersOutput
SMTP: PublicConnectionParametersOutput
CALDAV: PublicConnectionParametersOutput
}
type ConnectedAccountPublicDTO {
id: UUID!
handle: String!
provider: String!
lastCredentialsRefreshedAt: DateTime
authFailedAt: DateTime
handleAliases: [String!]
scopes: [String!]
lastSignedInAt: DateTime
userWorkspaceId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
connectionParameters: PublicImapSmtpCaldavConnectionParameters
}
type SendEmailOutput {
success: Boolean!
error: String
@@ -3198,6 +3298,7 @@ type Query {
myMessageFolders(messageChannelId: UUID): [MessageFolder!]!
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
myConnectedAccounts: [ConnectedAccountDTO!]!
connectedAccountById(id: UUID!): ConnectedAccountPublicDTO
connectedAccounts: [ConnectedAccountDTO!]!
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
webhooks: [Webhook!]!
@@ -3242,6 +3343,9 @@ type Query {
getConnectedImapSmtpCaldavAccount(id: UUID!): ConnectedImapSmtpCaldavAccount!
getAutoCompleteAddress(address: String!, token: String!, country: String, isFieldCity: Boolean): [AutocompleteResult!]!
getAddressDetails(placeId: String!, token: String!): PlaceDetailsResult!
userLookupAdminPanel(userIdentifier: String!): UserLookup!
adminPanelRecentUsers(searchTerm: String = ""): [AdminPanelRecentUser!]!
adminPanelTopWorkspaces(searchTerm: String = ""): [AdminPanelTopWorkspace!]!
getConfigVariablesGrouped: ConfigVariables!
getSystemHealthStatus: SystemHealth!
getIndicatorHealthStatus(indicatorId: HealthIndicatorId!): AdminPanelHealthServiceData!
@@ -3256,6 +3360,9 @@ type Query {
getModelsDevSuggestions(providerType: String!): [ModelsDevModelSuggestion!]!
getAdminAiUsageByWorkspace(periodStart: DateTime, periodEnd: DateTime): [UsageBreakdownItem!]!
getMaintenanceMode: MaintenanceMode
workspaceLookupAdminPanel(workspaceId: UUID!): UserLookup!
getAdminWorkspaceChatThreads(workspaceId: UUID!): [AdminWorkspaceChatThread!]!
getAdminChatThreadMessages(threadId: UUID!): AdminChatThreadMessages!
getUsageAnalytics(input: UsageAnalyticsInput): UsageAnalytics!
getPostgresCredentials: PostgresCredentials
findManyPublicDomains: [PublicDomain!]!
@@ -3342,6 +3449,7 @@ enum EventLogTable {
PAGEVIEW
OBJECT_EVENT
USAGE_EVENT
APPLICATION_LOG
}
input EventLogFiltersInput {
@@ -3396,6 +3504,7 @@ type Mutation {
updateNavigationMenuItem(input: UpdateOneNavigationMenuItemInput!): NavigationMenuItem!
deleteManyNavigationMenuItems(ids: [UUID!]!): [NavigationMenuItem!]!
deleteNavigationMenuItem(id: UUID!): NavigationMenuItem!
uploadEmailAttachmentFile(file: Upload!): FileWithSignedUrl!
uploadAIChatFile(file: Upload!): FileWithSignedUrl!
uploadWorkflowFile(file: Upload!): FileWithSignedUrl!
uploadWorkspaceLogo(file: Upload!): FileWithSignedUrl!
@@ -3461,6 +3570,7 @@ type Mutation {
destroyPageLayout(id: String!): Boolean!
updatePageLayoutWithTabsAndWidgets(id: String!, input: UpdatePageLayoutWithTabsInput!): PageLayout!
resetPageLayoutWidgetToDefault(id: String!): PageLayoutWidget!
resetPageLayoutTabToDefault(id: String!): PageLayoutTab!
createPageLayoutWidget(input: CreatePageLayoutWidgetInput!): PageLayoutWidget!
updatePageLayoutWidget(id: String!, input: UpdatePageLayoutWidgetInput!): PageLayoutWidget!
destroyPageLayoutWidget(id: String!): Boolean!
@@ -3508,7 +3618,7 @@ type Mutation {
updateWebhook(input: UpdateWebhookInput!): Webhook!
deleteWebhook(id: UUID!): Webhook!
createChatThread: AgentChatThread!
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String): SendChatMessageResult!
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileIds: [UUID!]): SendChatMessageResult!
stopAgentChatStream(threadId: UUID!): Boolean!
deleteQueuedChatMessage(messageId: UUID!): Boolean!
createSkill(input: CreateSkillInput!): Skill!
@@ -3565,10 +3675,11 @@ type Mutation {
startChannelSync(connectedAccountId: UUID!): ChannelSyncSuccess!
saveImapSmtpCaldavAccount(accountOwnerId: UUID!, handle: String!, connectionParameters: EmailAccountConnectionParameters!, id: UUID): ImapSmtpCaldavConnectionSuccess!
updateLabPublicFeatureFlag(input: UpdateLabPublicFeatureFlagInput!): FeatureFlag!
userLookupAdminPanel(userIdentifier: String!): UserLookup!
updateWorkspaceFeatureFlag(workspaceId: UUID!, featureFlag: String!, value: Boolean!): Boolean!
setAdminAiModelEnabled(modelId: String!, enabled: Boolean!): Boolean!
setAdminAiModelsEnabled(modelIds: [String!]!, enabled: Boolean!): Boolean!
setAdminAiModelRecommended(modelId: String!, recommended: Boolean!): Boolean!
setAdminAiModelsRecommended(modelIds: [String!]!, recommended: Boolean!): Boolean!
setAdminDefaultAiModel(role: AiModelRole!, modelId: String!): Boolean!
createDatabaseConfigVariable(key: String!, value: JSON!): Boolean!
updateDatabaseConfigVariable(key: String!, value: JSON!): Boolean!
@@ -3965,6 +4076,7 @@ input UpdatePageLayoutWidgetWithIdInput {
position: JSON
configuration: JSON
conditionalDisplay: JSON
conditionalAvailabilityExpression: String
}
input GridPositionInput {
@@ -3985,6 +4097,7 @@ input CreatePageLayoutWidgetInput {
}
input UpdatePageLayoutWidgetInput {
pageLayoutTabId: UUID
title: String
type: WidgetType
objectMetadataId: UUID
@@ -3992,6 +4105,7 @@ input UpdatePageLayoutWidgetInput {
position: JSON
configuration: JSON
conditionalDisplay: JSON
conditionalAvailabilityExpression: String
}
input CreateLogicFunctionFromSourceInput {
@@ -4541,6 +4655,12 @@ input SendEmailInput {
subject: String!
body: String!
inReplyTo: String
files: [SendEmailAttachmentInput!]
}
input SendEmailAttachmentInput {
id: String!
name: String!
}
input EmailAccountConnectionParameters {
@@ -4607,6 +4727,7 @@ enum FileFolder {
FilesField
Dependencies
Workflow
EmailAttachment
AppTarball
GeneratedSdkClient
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -215,7 +215,8 @@ ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
DISABLE_DB_MIGRATIONS=true \
DISABLE_CRON_JOBS_REGISTRATION=true \
IS_BILLING_ENABLED=false \
SIGN_IN_PREFILLED=true
SIGN_IN_PREFILLED=true \
APPLICATION_LOG_DRIVER=CONSOLE
EXPOSE 2020
VOLUME ["/data/postgres", "/app/packages/twenty-server/.local-storage"]
@@ -47,7 +47,7 @@ npx nx run twenty-server:database:reset
#### For objects in Core/Metadata schemas (TypeORM)
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate
```
## Tech Stack
@@ -476,7 +476,7 @@ export default defineLogicFunction({
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
@@ -644,49 +644,15 @@ export default defineLogicFunction({
**Write a good `description`.** AI agents rely on the function's `description` field to decide when to use the tool. Be specific about what the tool does and when it should be called.
</Note>
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Define a pre-install logic function (one per app)">
A pre-install function is a logic function that runs automatically before your app is installed on a workspace. This is useful for validation tasks, prerequisite checks, or preparing workspace state before the main installation proceeds.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
You can also manually execute the pre-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Key points:
- Pre-install functions use `definePreInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
- The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
- Only one pre-install function is allowed per application. The manifest build will error if more than one is detected.
- The function's `universalIdentifier` is automatically set as `preInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer preparation tasks.
</Accordion>
<Accordion title="definePostInstallLogicFunction" description="Define a post-install logic function (one per app)">
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
A post-install function is a logic function that runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
@@ -695,6 +661,8 @@ export default definePostInstallLogicFunction({
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
@@ -707,10 +675,168 @@ yarn twenty exec --postInstall
Key points:
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
- The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
- The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic.
- **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook.
- **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed.
- `shouldRunSynchronously: false` *(default)* — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window.
- `shouldRunSynchronously: true` — the hook is executed **inline during the install flow** (same executor as pre-install). The install request blocks until the handler finishes, and if it throws, the install caller receives a `POST_INSTALL_ERROR`. No automatic retries. **Use this for fast, must-complete-before-response work** — for example, emitting a validation error to the user, or quick setup that the client will rely on immediately after the install call returns. Keep in mind the metadata migration has already been applied by the time post-install runs, so a sync-mode failure does **not** roll back the schema changes — it only surfaces the error.
- Make sure your handler is idempotent. In async mode the queue may retry up to three times; in either mode the hook may run again on upgrades when `shouldRunOnVersionUpgrade: true`.
- The environment variables `APPLICATION_ID`, `APP_ACCESS_TOKEN`, and `API_URL` are available inside the handler (same as any other logic function), so you can call the Twenty API with an application access token scoped to your app.
- Only one post-install function is allowed per application. The manifest build will error if more than one is detected.
- The function's `universalIdentifier` is automatically set as `postInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
- The function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in `defineApplication()`.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
- **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` to trigger it manually against a running workspace.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Define a pre-install logic function (one per app)">
A pre-install function is a logic function that runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
You can also manually execute the pre-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Key points:
- Pre-install functions use `definePreInstallLogicFunction()` — same specialized config as post-install, just attached to a different lifecycle slot.
- Both pre- and post-install handlers receive the same `InstallPayload` type: `{ previousVersion?: string; newVersion: string }`. Import it once and reuse it for both hooks.
- **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state.
- **Execution model**: pre-install is executed **synchronously** and **blocks the install**. If the handler throws, the install is aborted before any schema changes are applied — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade.
- As with post-install, only one pre-install function is allowed per application. It is attached to the application manifest under `preInstallLogicFunction` automatically during the build.
- **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty exec --preInstall` to trigger it manually.
</Accordion>
<Accordion title="Pre-install vs post-install: when to use which" description="Choosing the right install hook">
Both hooks are part of the same install flow and receive the same `InstallPayload`. The difference is **when** they run relative to the workspace metadata migration, and that changes what data they can safely touch.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
Pre-install is always **synchronous** (it blocks the install and can abort it). Post-install is **asynchronous by default** — enqueued on a worker with automatic retries — but can opt into synchronous execution with `shouldRunSynchronously: true`. See the `definePostInstallLogicFunction` accordion above for when to use each mode.
**Use `post-install` for anything that needs the new schema to exist.** This is the common case:
- Seeding default data (creating initial records, default views, demo content) against newly-added objects and fields.
- Registering webhooks with third-party services now that the app has its credentials.
- Calling your own API to finish setup that depends on the synchronized metadata.
- Idempotent "ensure this exists" logic that should reconcile state on every upgrade — combine with `shouldRunOnVersionUpgrade: true`.
Example — seed a default `PostCard` record after install:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the *previous* schema and its failure rolls back the upgrade, it is the right place for anything risky:
- **Backing up data that is about to be dropped or restructured** — e.g. you are removing a field in v2 and need to copy its values into another field or export them to storage before the migration runs.
- **Archiving records that a new constraint would invalidate** — e.g. a field is becoming `NOT NULL` and you need to delete or fix rows with null values first.
- **Validating compatibility and refusing the upgrade if the current data cannot be migrated cleanly** — throw from the handler and the install aborts with no changes applied. This is safer than discovering the incompatibility mid-migration.
- **Renaming or rekeying data** ahead of a schema change that would lose the association.
Example — archive records before a destructive migration:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**Rule of thumb:**
| You want to… | Use |
|---|---|
| Seed default data, configure the workspace, register external resources | `post-install` |
| Run long-running seeding or third-party calls that shouldn't block the install response | `post-install` (default — `shouldRunSynchronously: false`, with worker retries) |
| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` |
| Read or back up data that the upcoming migration would lose | `pre-install` |
| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) |
| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` |
| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) |
<Note>
If in doubt, default to **post-install**. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone.
</Note>
</Accordion>
<Accordion title="defineFrontComponent" description="Define front components for custom UI" >
@@ -756,7 +882,7 @@ export default defineFrontComponent({
});
```
After syncing with `yarn twenty dev`, the quick action appears in the top-right corner of the page:
After syncing with `yarn twenty dev` (or running a one-shot `yarn twenty dev --once`), the quick action appears in the top-right corner of the page:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Quick action button in the top-right corner" />
@@ -1402,7 +1528,7 @@ export default defineFrontComponent({
### How bundling works
The build step (`yarn twenty dev` or `yarn twenty build`) uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle.
The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle.
**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed.
@@ -1818,8 +1944,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute pre-install or post-install functions
yarn twenty exec --preInstall
# Execute the post-install function
yarn twenty exec --postInstall
```
@@ -98,6 +98,21 @@ Dev mode is only available on Twenty instances running in development (`NODE_ENV
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Dev mode terminal output" />
</div>
#### One-shot sync with `yarn twenty dev --once`
If you do not want a watcher running in the background (for example in a CI pipeline, a git hook, or a scripted workflow), pass the `--once` flag. It runs the same pipeline as `yarn twenty dev` — build manifest, bundle files, upload, sync, regenerate the typed API client — but **exits as soon as the sync completes**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Command | Behavior | When to use |
|---------|----------|-------------|
| `yarn twenty dev` | Watches your source files and re-syncs on every change. Keeps running until you stop it. | Interactive local development — you want the live status panel and instant feedback loop. |
| `yarn twenty dev --once` | Performs a single build + sync, then exits with code `0` on success or `1` on failure. | Scripts, CI, pre-commit hooks, AI agents, and any non-interactive workflow. |
Both modes require a Twenty server running in development mode and an authenticated remote — the same prerequisites apply.
### See your app in Twenty
Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) in your browser. Navigate to **Settings > Apps** and select the **Developer** tab. You should see your app listed under **Your Apps**:
@@ -66,12 +66,18 @@ The share link uses the server's base URL (without any workspace subdomain) so i
### Version management
When updating an already deployed tarball app, the server requires the `version` in `package.json` to be **strictly higher** (per [semver](https://semver.org) ordering) than the currently deployed version. Re-deploying the same version, or pushing a lower one, is rejected before the tarball is stored — you'll see a `VERSION_ALREADY_EXISTS` error from the CLI.
To release an update:
1. Bump the `version` field in your `package.json`
1. Bump the `version` field in your `package.json` (e.g. `1.2.3` → `1.2.4`, `1.3.0`, or `2.0.0`)
2. Run `yarn twenty deploy` (or `yarn twenty deploy --remote production`)
3. Workspaces that have the app installed will see the upgrade available in their settings
<Note>
Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allowed, and a final release like `1.0.0` is correctly recognized as higher than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver string.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
## Publishing to npm
@@ -81,7 +87,7 @@ Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twe
### Requirements
- An [npm](https://www.npmjs.com) account
- The `twenty-app` keyword in your `package.json` `keywords` array (already included when you scaffold with `create-twenty-app`)
- The `twenty-app` keyword in your `package.json` `keywords` array (add it manually — it is not included by default in the `create-twenty-app` template)
```json filename="package.json"
{
@@ -189,3 +195,12 @@ You can also install apps from the command line:
```bash filename="Terminal"
yarn twenty install
```
<Note>
The server enforces semver versioning on install, mirroring the rules on deploy:
- Installing the same version that is already installed in your workspace is rejected with an `APP_ALREADY_INSTALLED` error.
- Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error.
To install a newer version, deploy or publish it first, then re-run `yarn twenty install`.
</Note>
@@ -47,7 +47,7 @@ npx nx run twenty-server:database:reset
#### للكائنات داخل مخططات Core/Metadata (TypeORM)
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate
```
## "التقنية المستخدمة"
@@ -476,7 +476,7 @@ export default defineLogicFunction({
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
@@ -643,49 +643,15 @@ export default defineLogicFunction({
**اكتب `description` جيدًا.** يعتمد وكلاء الذكاء الاصطناعي على حقل `description` الخاص بالدالة لتحديد وقت استخدام الأداة. كن محددًا بشأن ما تفعله الأداة ومتى ينبغي استدعاؤها.
</Note>
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="تعريف دالة منطقية لما قبل التثبيت (واحدة لكل تطبيق)">
دالة ما قبل التثبيت هي دالة منطقية تعمل تلقائيًا قبل تثبيت تطبيقك على مساحة عمل. يفيد ذلك في مهام التحقق، وفحص المتطلبات المسبقة، أو تجهيز حالة مساحة العمل قبل متابعة التثبيت الرئيسي.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
يمكنك أيضًا تنفيذ دالة ما قبل التثبيت يدويًا في أي وقت باستخدام CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
النقاط الرئيسية:
* تستخدم دوال ما قبل التثبيت `definePreInstallLogicFunction()` — وهو إصدار متخصص يستبعد إعدادات المُشغِّل (`cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` و`isTool`).
* يتلقى المُعالج `InstallLogicFunctionPayload` يحوي `{ previousVersion: string }` — إصدار التطبيق الذي كان مُثبّتًا سابقًا (أو سلسلة فارغة للتثبيتات الجديدة).
* يُسمح بدالة ما قبل التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
* يتم تعيين `universalIdentifier` للدالة تلقائيًا كـ `preInstallLogicFunctionUniversalIdentifier` في بيان التطبيق أثناء الإنشاء — لست بحاجة إلى الإشارة إليه في `defineApplication()`.
* تم ضبط المهلة الافتراضية على 300 ثانية (5 دقائق) للسماح بمهام التحضير الأطول.
</Accordion>
<Accordion title="definePostInstallLogicFunction" description="تعريف دالة منطقية لما بعد التثبيت (واحدة لكل تطبيق)">
دالة ما بعد التثبيت هي دالة منطقية تعمل تلقائيًا بعد تثبيت تطبيقك على مساحة عمل. هذا مفيد لمهام الإعداد لمرة واحدة مثل تهيئة البيانات الافتراضية، وإنشاء السجلات الأولية، أو تكوين إعدادات مساحة العمل.
دالة ما بعد التثبيت هي دالة منطقية تعمل تلقائيًا بعد تثبيت تطبيقك على مساحة عمل. ينفّذه الخادم **بعد** مزامنة البيانات الوصفية للتطبيق وإنشاء عميل SDK، بحيث تكون مساحة العمل جاهزة تمامًا للاستخدام ويكون المخطط الجديد مطبَّقًا. تشمل حالات الاستخدام النموذجية تهيئة البيانات الافتراضية، وإنشاء السجلات الأولية، وتكوين إعدادات مساحة العمل، أو توفير الموارد على خدمات جهات خارجية.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
@@ -694,6 +660,8 @@ export default definePostInstallLogicFunction({
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
@@ -706,10 +674,168 @@ yarn twenty exec --postInstall
النقاط الرئيسية:
* تستخدم دوال ما بعد التثبيت `definePostInstallLogicFunction()` — وهو إصدار متخصص يستبعد إعدادات المُشغِّل (`cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` و`isTool`).
* يتلقى المُعالج `InstallLogicFunctionPayload` يحوي `{ previousVersion: string }` — إصدار التطبيق الذي كان مُثبّتًا سابقًا (أو سلسلة فارغة للتثبيتات الجديدة).
* يتلقى المعالج `InstallPayload` يحتوي على `{ previousVersion?: string; newVersion: string }` — حيث إن `newVersion` هو الإصدار الجاري تثبيته، و`previousVersion` هو الإصدار الذي كان مُثبّتًا سابقًا (أو `undefined` عند التثبيت الأولي). استخدم هذه القيم للتمييز بين عمليات التثبيت الجديدة والترقيات ولتشغيل منطق الترحيل الخاص بالإصدار.
* **موعد تشغيل الخطاف**: في عمليات التثبيت الجديدة فقط، افتراضيًا. مرّر `shouldRunOnVersionUpgrade: true` إذا كنت تريد تشغيله أيضًا عند ترقية التطبيق من إصدار سابق. عند إغفاله، تكون القيمة الافتراضية للعلم `false`، وتتجاوز الترقيات هذا الخطاف.
* **نموذج التنفيذ — غير متزامن افتراضيًا، والتزامني اختياري**: يتحكّم العلم `shouldRunSynchronously` في كيفية تنفيذ ما بعد التثبيت.
* `shouldRunSynchronously: false` *(الإعداد الافتراضي)* — يتم **إدراج الخطاف في قائمة الرسائل** مع `retryLimit: 3` ويعمل بشكل غير متزامن داخل عامل عمل. يعود ردّ التثبيت بمجرد وضع المهمة في الطابور، لذا فإن معالجًا بطيئًا أو متعطلًا لا يحجب المستدعي. سيُجرِّب العامل إعادة المحاولة حتى ثلاث مرات. **استخدم هذا للمهام طويلة التشغيل** — بَذر مجموعات بيانات كبيرة، استدعاء واجهات برمجة تطبيقات خارجية بطيئة، تهيئة موارد خارجية، أو أي شيء قد يتجاوز نافذة استجابة HTTP المعقولة.
* `shouldRunSynchronously: true` — يُنفّذ الخطاف **ضمن تدفّق التثبيت مباشرةً** (نفس المنفِّذ كما قبل التثبيت). يَحجُب طلب التثبيت حتى ينتهي المعالج، وإذا رمى استثناءً، سيتلقى مستدعي التثبيت `POST_INSTALL_ERROR`. لا توجد محاولات إعادة تلقائية. **استخدم هذا للمهام السريعة التي يجب إكمالها قبل الاستجابة** — مثل إظهار خطأ تحقق للمستخدم، أو إعداد سريع سيعتمد عليه العميل مباشرةً بعد عودة نداء التثبيت. ضع في اعتبارك أن ترحيل البيانات الوصفية يكون قد طُبِّق بالفعل عند تشغيل ما بعد التثبيت، لذلك فإن فشل الوضع المتزامن **لا** يعيد التغييرات على المخطط إلى الوراء — بل يكتفي بإبراز الخطأ.
* تأكّد من أن معالجك قابل للتنفيذ المتكرر دون آثار جانبية. في الوضع غير المتزامن قد تُعيد قائمة الانتظار المحاولة حتى ثلاث مرات؛ وفي أي من الوضعين قد يعمل الخطاف مجددًا أثناء الترقيات عند ضبط `shouldRunOnVersionUpgrade: true`.
* متغيرات البيئة `APPLICATION_ID` و`APP_ACCESS_TOKEN` و`API_URL` متاحة داخل المعالج (كما في أي دالة منطق أخرى)، لذا يمكنك استدعاء واجهة Twenty API باستخدام رمز وصول للتطبيق مقيّد بنطاق تطبيقك.
* يُسمح بدالة ما بعد التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
* يتم تعيين `universalIdentifier` للدالة تلقائيًا كـ `postInstallLogicFunctionUniversalIdentifier` في بيان التطبيق أثناء الإنشاء — لست بحاجة إلى الإشارة إليه في `defineApplication()`.
* تُرفَق خصائص الدالة `universalIdentifier` و`shouldRunOnVersionUpgrade` و`shouldRunSynchronously` تلقائيًا ببيان التطبيق ضمن الحقل `postInstallLogicFunction` أثناء عملية البناء — ولا تحتاج إلى الإشارة إليها في `defineApplication()`.
* تم تعيين مهلة افتراضية إلى 300 ثانية (5 دقائق) للسماح بمهام الإعداد الأطول مثل تهيئة البيانات.
* **لا يُنفَّذ في وضع التطوير**: عند تسجيل تطبيق محليًا (عبر `yarn twenty dev`)، يتجاوز الخادم تدفّق التثبيت بالكامل ويُزامن الملفات مباشرةً عبر مراقِب CLI — لذا لن يعمل ما بعد التثبيت في وضع التطوير مطلقًا، بغضّ النظر عن `shouldRunSynchronously`. استخدم `yarn twenty exec --postInstall` لتشغيله يدويًا على مساحة عمل قيد التشغيل.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="تعريف دالة منطقية لما قبل التثبيت (واحدة لكل تطبيق)">
دالة ما قبل التثبيت هي دالة منطقية تعمل تلقائيًا أثناء التثبيت، **قبل تطبيق ترحيل البيانات الوصفية لمساحة العمل**. تتشارك نفس بنية الحمولة مع ما بعد التثبيت (`InstallPayload`)، لكنها موضوعة أبكر في تدفّق التثبيت كي تجهّز حالة يعتمد عليها الترحيل القادم — ومن الاستخدامات الشائعة: نسخ البيانات احتياطيًا، التحقق من التوافق مع المخطط الجديد، أو أرشفة السجلات التي ستُعاد هيكلتها أو ستُحذف.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
يمكنك أيضًا تنفيذ دالة ما قبل التثبيت يدويًا في أي وقت باستخدام CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
النقاط الرئيسية:
* تستخدم دوال ما قبل التثبيت `definePreInstallLogicFunction()` — نفس الإعدادات المتخصصة كما في ما بعد التثبيت، لكنها مرتبطة بموضع مختلف ضمن دورة الحياة.
* يتلقّى كلٌّ من معالجي ما قبل التثبيت وما بعد التثبيت النوع نفسه `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. استورده مرة واحدة وأعد استخدامه لكلا الخطافين.
* **موعد تشغيل الخطاف**: موضوع مباشرةً قبل ترحيل البيانات الوصفية لمساحة العمل (`synchronizeFromManifest`). قبل التنفيذ، يُشغِّل الخادم مزامنة "pared-down sync" ذات طابع إضافي فقط تقوم بتسجيل دالة ما قبل التثبيت للإصدار **الجديد** في البيانات الوصفية لمساحة العمل — دون لمس أي شيء آخر — ثم يُنفّذها. لأن هذه المزامنة «إضافية فقط»، تبقى كائنات وحقول وبيانات الإصدار السابق سليمة عند تشغيل معالجك: يمكنك قراءة حالة ما قبل الترحيل ونسخها احتياطيًا بأمان.
* **نموذج التنفيذ**: يُنفَّذ ما قبل التثبيت **بشكل متزامن** و**يحجب عملية التثبيت**. إذا رمى المعالج استثناءً، تُلغى عملية التثبيت قبل تطبيق أي تغييرات على المخطط — وتبقى مساحة العمل على الإصدار السابق بحالة متّسقة. هذا مقصود: ما قبل التثبيت هو فرصتك الأخيرة لرفض ترقية تنطوي على مخاطر.
* كما هو الحال مع ما بعد التثبيت، يُسمح بدالة ما قبل التثبيت واحدة فقط لكل تطبيق. تُربَط تلقائيًا ببيان التطبيق تحت `preInstallLogicFunction` أثناء عملية البناء.
* **لا يُنفَّذ في وضع التطوير**: كما في ما بعد التثبيت — يتم تجاوز تدفّق التثبيت بالكامل للتطبيقات المسجّلة محليًا، لذا لن يعمل ما قبل التثبيت مطلقًا عند `yarn twenty dev`. استخدم `yarn twenty exec --preInstall` لتشغيله يدويًا.
</Accordion>
<Accordion title="ما قبل التثبيت مقابل ما بعد التثبيت: متى تستخدم أيّهما" description="اختيار خطاف التثبيت المناسب">
كلا الخطافين جزء من تدفّق التثبيت نفسه ويتلقّيان نفس `InstallPayload`. الاختلاف يكمن في **موعد** تشغيلهما نسبةً إلى ترحيل البيانات الوصفية لمساحة العمل، وهذا يغيّر البيانات التي يمكنهما التعامل معها بأمان.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
ما قبل التثبيت دائمًا **متزامن** (يحجب التثبيت ويمكنه إحباطه). ما بعد التثبيت **غير متزامن افتراضيًا** — يُدرج على عامل مع محاولات إعادة تلقائية — لكن يمكن التبديل إلى تنفيذ متزامن عبر `shouldRunSynchronously: true`. راجع الأكورديون `definePostInstallLogicFunction` أعلاه لمعرفة متى تستخدم كل وضع.
**استخدم `post-install` لأي شيء يتطلّب وجود المخطط الجديد.** وهذا هو السيناريو الشائع:
* بَذر بيانات افتراضية (إنشاء سجلات أولية وعروض افتراضية ومحتوى تجريبي) للكائنات والحقول المضافة حديثًا.
* تسجيل خطافات الويب مع خدمات أطراف ثالثة بعد أن حصل التطبيق على بيانات الاعتماد الخاصة به.
* استدعاء واجهة برمجة التطبيقات الخاصة بك لإكمال إعداد يعتمد على البيانات الوصفية المتزامنة.
* منطق idempotent لتحقيق "تأكّد من وجود هذا" والذي ينبغي مواءمة الحالة في كل ترقية — بالاقتران مع `shouldRunOnVersionUpgrade: true`.
مثال — بَذر سجل `PostCard` افتراضي بعد التثبيت:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**استخدم `pre-install` عندما قد يُتلف الترحيل أو يدمّر البيانات الحالية.** لأن ما قبل التثبيت يعمل مقابل المخطط *السابق* وفشله يُرجِع الترقية إلى الوراء، فهو المكان المناسب لأي شيء محفوف بالمخاطر:
* **نسخ البيانات احتياطيًا قبل حذفها أو إعادة هيكلتها** — مثل إزالة حقل في v2 وتحتاج إلى نسخ قيمه إلى حقل آخر أو تصديرها إلى التخزين قبل تشغيل الترحيل.
* **أرشفة السجلات التي سيبطلها قيد جديد** — مثل أن يصبح حقل ما `NOT NULL` وتحتاج أولًا إلى حذف الصفوف ذات القيم الفارغة أو إصلاحها.
* **التحقق من التوافق ورفض الترقية إذا تعذّر ترحيل البيانات الحالية بسلاسة** — ارمِ من داخل المعالج وسيُلغى التثبيت دون تطبيق أي تغييرات. هذا أكثر أمانًا من اكتشاف عدم التوافق في منتصف الترحيل.
* **إعادة تسمية البيانات أو إعادة تعيين مفاتيحها** قبل تغيير في المخطط قد يؤدي إلى فقدان الارتباط.
مثال — أرشف السجلات قبل ترحيل هدّام:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**قاعدة عامة:**
| ترغب في… | استخدام |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| بذر بيانات افتراضية، تهيئة مساحة العمل، تسجيل موارد خارجية | `post-install` |
| تشغيل بذر طويل الأمد أو استدعاءات أطراف ثالثة لا ينبغي أن تحجب استجابة التثبيت | `post-install` (الإعداد الافتراضي — `shouldRunSynchronously: false`، مع محاولات إعادة من العامل) |
| تشغيل إعداد سريع سيعتمد عليه المستدعي مباشرةً بعد عودة نداء التثبيت | `post-install` مع `shouldRunSynchronously: true` |
| قراءة البيانات أو نسخها احتياطيًا والتي قد يفقدها الترحيل القادم | `pre-install` |
| رفض ترقية قد تُفسد البيانات الحالية | `pre-install` (ارمِ من المعالج) |
| تنفيذ مواءمة في كل ترقية | `post-install` مع `shouldRunOnVersionUpgrade: true` |
| تنفيذ إعداد لمرة واحدة في التثبيت الأول فقط | `post-install` مع `shouldRunOnVersionUpgrade: false` (الإعداد الافتراضي) |
<Note>
إذا ساورك الشك، فاجعل الافتراضي هو **post-install**. الجأ إلى ما قبل التثبيت فقط عندما يكون الترحيل نفسه هدّامًا وتحتاج إلى التقاط الحالة السابقة قبل أن تزول.
</Note>
</Accordion>
<Accordion title="defineFrontComponent" description="عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة">
@@ -755,7 +881,7 @@ export default defineFrontComponent({
});
```
بعد المزامنة باستخدام `yarn twenty dev`، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
بعد المزامنة باستخدام `yarn twenty dev` (أو تشغيل الأمر لمرة واحدة `yarn twenty dev --once`)، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="زر إجراء سريع في الزاوية العلوية اليمنى" />
@@ -1401,7 +1527,7 @@ export default defineFrontComponent({
### كيف يعمل التجميع
تستخدم خطوة البناء (`yarn twenty dev` أو `yarn twenty build`) أداة esbuild لإنتاج ملف واحد مستقل لكل دالة منطقية وكل مكوّن أمامي. تُضمَّن جميع الحزم المستوردة داخل الحزمة.
تستخدم خطوة البناء أداة esbuild لإنتاج ملف واحد مستقل لكل دالة منطقية ولكل مكوّن أمامي. تُضمَّن جميع الحزم المستوردة داخل الحزمة.
**الدوال المنطقية** تعمل في بيئة Node.js. الوحدات المدمجة في Node (`fs` و`path` و`crypto` و`http` وغيرها) متاحة ولا تحتاج إلى تثبيت.
@@ -1816,8 +1942,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute pre-install or post-install functions
yarn twenty exec --preInstall
# Execute the post-install function
yarn twenty exec --postInstall
```
@@ -98,6 +98,21 @@ yarn twenty dev --verbose
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="مخرجات الطرفية في وضع التطوير" />
</div>
#### مزامنة لمرة واحدة باستخدام `yarn twenty dev --once`
إذا كنت لا تريد تشغيل مراقب في الخلفية (مثلًا في خط أنابيب CI، أو خطاف Git، أو سير عمل مُؤتمت عبر سكربت)، فمرِّر الخيار `--once`. يُشغِّل خط الأنابيب نفسه مثل `yarn twenty dev` — إنشاء بيان البناء، تجميع الملفات، الرفع، المزامنة، إعادة توليد عميل API مضبوط الأنواع — ولكنه **ينهي التنفيذ فور اكتمال المزامنة**:
```bash filename="Terminal"
yarn twenty dev --once
```
| أمر | السلوك | متى يُستخدم |
| ------------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `yarn twenty dev` | يراقب ملفات المصدر ويعيد المزامنة عند كل تغيير. يستمر بالتشغيل حتى توقفه. | تطوير محلي تفاعلي — تريد لوحة حالة مباشرة وحلقة تغذية راجعة فورية. |
| `yarn twenty dev --once` | يجري عملية بناء واحدة + مزامنة واحدة، ثم ينهي التنفيذ برمز `0` عند النجاح أو `1` عند الفشل. | البرامج النصية، وCI، وخطافات ما قبل الالتزام، ووكلاء الذكاء الاصطناعي، وأي سير عمل غير تفاعلي. |
كلا الوضعين يتطلبان خادم Twenty يعمل في وضع التطوير وجهة بعيدة موثَّقة — تنطبق المتطلبات المسبقة نفسها.
### اعرض تطبيقك في Twenty
افتح [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) في متصفحك. انتقل إلى **Settings > Apps** واختر علامة التبويب **Developer**. يُفترض أن ترى تطبيقك مُدرجًا تحت **Your Apps**:
@@ -66,12 +66,18 @@ yarn twenty deploy
### إدارة الإصدارات
عند تحديث تطبيق tarball منشور مسبقًا، يشترط الخادم أن تكون قيمة `version` في `package.json` **أعلى قطعًا** (وفق ترتيب [الإصدار الدلالي](https://semver.org)) من الإصدار المنشور حاليًا. إعادة نشر الإصدار نفسه، أو دفع إصدار أدنى، يُرفَض قبل تخزين ملف tarball — سترى خطأ `VERSION_ALREADY_EXISTS` من CLI.
لطرح تحديث:
1. ارفع قيمة الحقل `version` في ملف `package.json`
1. قم بزيادة الحقل `version` في ملف `package.json` (مثلًا: `1.2.3` → `1.2.4`، `1.3.0`، أو `2.0.0`)
2. شغّل `yarn twenty deploy` (أو `yarn twenty deploy --remote production`)
3. سترى مساحات العمل التي ثبّتت التطبيق الترقية متاحة في إعداداتها
<Note>
علامات ما قبل الإصدار تعمل كما هو متوقع: زيادة `1.0.0-rc.1` → `1.0.0-rc.2` مسموح بها، ويُعترَف بالإصدار النهائي مثل `1.0.0` على أنه أعلى من `1.0.0-rc.5`. يجب أن يكون الإصدار في `package.json` بنفسه سلسلة semver صالحة.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
## النشر على npm
@@ -81,7 +87,7 @@ yarn twenty deploy
### المتطلبات
* حساب على [npm](https://www.npmjs.com)
* الكلمة المفتاحية `twenty-app` في مصفوفة `keywords` في `package.json` (موجودة مسبقًا عند تهيئة المشروع باستخدام `create-twenty-app`)
* الكلمة المفتاحية `twenty-app` في مصفوفة `keywords` في `package.json` (أضفها يدويًا — فهي غير مضمنة افتراضيًا في قالب `create-twenty-app`)
```json filename="package.json"
{
@@ -189,3 +195,12 @@ jobs:
```bash filename="Terminal"
yarn twenty install
```
<Note>
يفرض الخادم اعتماد إصدارات semver عند التثبيت، بما يعكس القواعد المطبّقة عند النشر:
* تثبيت الإصدار نفسه المثبّت بالفعل في مساحة عملك يُرفَض بخطأ `APP_ALREADY_INSTALLED`.
* تثبيت إصدار أدنى من الإصدار المثبّت حاليًا يُرفَض بخطأ `CANNOT_DOWNGRADE_APPLICATION`.
لتثبيت إصدار أحدث، انشره (deploy) أو انشره إلى السجل (publish) أولًا، ثم أعد تشغيل `yarn twenty install`.
</Note>
@@ -140,12 +140,12 @@ description: دليل شامل لاستكشاف أخطاء استيراد CSV و
#### تاريخ
**المشكلة:** تنسيق تاريخ غير معروف
**الحل:** استخدم تنسيقًا متّسقًا في جميع أنحاء الملف
**الحل:** استخدم تنسيق `YYYY-MM-DD` بشكل متّسق في جميع أنحاء الملف
```
✓ 2024-03-15 (YYYY-MM-DD - recommended)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
✓ 2024-03-15 (YYYY-MM-DD)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
```
#### هاتف
@@ -103,8 +103,6 @@ description: دليل كامل خطوة بخطوة لتنسيق بياناتك
استخدم تنسيقًا موحدًا في كامل ملفك:
* `YYYY-MM-DD` (مُوصى به): `2024-03-15`
* `MM/DD/YYYY`: `03/15/2024`
* `DD/MM/YYYY`: `15/03/2024`
* ISO 8601: `2024-03-15T10:30:00Z`
### حقول الأرقام
@@ -47,7 +47,7 @@ npx nx run twenty-server:database:reset
#### Pro objekty ve schématech Core/Metadata (TypeORM)
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate
```
## Technologický stack
@@ -476,7 +476,7 @@ export default defineLogicFunction({
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
@@ -644,49 +644,15 @@ export default defineLogicFunction({
**Napište kvalitní `description`.** Agenti AI se spoléhají na pole funkce `description` při rozhodování, kdy nástroj použít. Buďte konkrétní ohledně toho, co nástroj dělá a kdy se má volat.
</Note>
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Definujte předinstalační logickou funkci (jedna na aplikaci)">
Předinstalační funkce je logická funkce, která se automaticky spouští před instalací vaší aplikace v pracovním prostoru. To je užitečné pro validační úlohy, kontrolu předpokladů nebo přípravu stavu pracovního prostoru před zahájením hlavní instalace.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Předinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Hlavní body:
* Předinstalační funkce používají `definePreInstallLogicFunction()` — specializovanou variantu, která vynechává nastavení spouštěčů (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Obslužná funkce (handler) obdrží `InstallLogicFunctionPayload` s `{ previousVersion: string }` — verzi aplikace, která byla dříve nainstalována (nebo prázdný řetězec při čisté instalaci).
* Na jednu aplikaci je povolena pouze jedna předinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna.
* Identifikátor `universalIdentifier` funkce se během sestavení automaticky nastaví v manifestu aplikace jako `preInstallLogicFunctionUniversalIdentifier` — není potřeba jej uvádět v `defineApplication()`.
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší přípravné úlohy.
</Accordion>
<Accordion title="definePostInstallLogicFunction" description="Definujte postinstalační logickou funkci (jedna na aplikaci)">
Postinstalační funkce je logická funkce, která se automaticky spouští po instalaci vaší aplikace do pracovního prostoru. To je užitečné pro jednorázové úlohy nastavení, jako je naplnění výchozími daty, vytvoření počátečních záznamů nebo konfigurace nastavení pracovního prostoru.
Postinstalační funkce je logická funkce, která se spustí automaticky, jakmile je instalace vaší aplikace v pracovním prostoru dokončena. Server ji provede **poté**, co byla synchronizována metadata aplikace a vygenerován klient SDK, takže je pracovní prostor plně připraven k použití a nové schéma je zavedeno. Mezi typické případy použití patří naplnění výchozími daty, vytvoření počátečních záznamů, konfigurace nastavení pracovního prostoru nebo zřizování prostředků ve službách třetích stran.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
@@ -695,6 +661,8 @@ export default definePostInstallLogicFunction({
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
@@ -707,10 +675,168 @@ yarn twenty exec --postInstall
Hlavní body:
* Postinstalační funkce používají `definePostInstallLogicFunction()` — specializovanou variantu, která vynechává nastavení spouštěčů (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Obslužná funkce (handler) obdrží `InstallLogicFunctionPayload` s `{ previousVersion: string }` — verzi aplikace, která byla dříve nainstalována (nebo prázdný řetězec při čisté instalaci).
* Obslužná funkce obdrží `InstallPayload` s `{ previousVersion?: string; newVersion: string }` — `newVersion` je verze, která se instaluje, a `previousVersion` je verze, která byla nainstalována dříve (nebo `undefined` při čisté instalaci). Tyto hodnoty použijte k rozlišení čistých instalací od aktualizací a ke spuštění migrační logiky specifické pro verzi.
* **Kdy se hook spouští**: ve výchozím nastavení pouze při čistých instalacích. Předejte `shouldRunOnVersionUpgrade: true`, pokud chcete, aby se spouštěl i při aktualizaci aplikace z předchozí verze. Pokud je vynechán, příznak má výchozí hodnotu `false` a při aktualizacích se hook přeskočí.
* **Model provádění — ve výchozím nastavení asynchronní, synchronní volitelně**: příznak `shouldRunSynchronously` určuje *jak* se spouští post-install.
* `shouldRunSynchronously: false` *(výchozí)* — hook je **zařazen do fronty zpráv** s `retryLimit: 3` a běží asynchronně ve workeru. Odezva instalace se vrátí hned po zařazení úlohy do fronty, takže pomalá nebo chybující obslužná funkce neblokuje volajícího. Worker se pokusí o opakování až třikrát. **Použijte pro dlouho běžící úlohy** — plnění velkých datových sad, volání pomalých externích API, zřizování externích prostředků, cokoli, co by mohlo přesáhnout rozumné časové okno HTTP odezvy.
* `shouldRunSynchronously: true` — hook se provádí **inline během instalačního procesu** (stejný vykonavatel jako pre-install). Instalační požadavek blokuje, dokud obslužná funkce nedokončí, a pokud vyvolá výjimku, volající instalace obdrží `POST_INSTALL_ERROR`. Žádné automatické opakování. **Použijte pro rychlé úlohy, které se musí dokončit před odpovědí** — například vrácení validační chyby uživateli nebo rychlé nastavení, na kterém bude klient záviset ihned po návratu volání instalace. Mějte na paměti, že v době, kdy se spustí post-install, už byla migrace metadat aplikována, takže selhání v synchronním režimu změny schématu **ne**vrací zpět — pouze odhalí chybu.
* Ujistěte se, že vaše obslužná funkce je idempotentní. V asynchronním režimu se může fronta pokusit až třikrát; v obou režimech se může hook znovu spustit při aktualizacích, pokud je `shouldRunOnVersionUpgrade: true`.
* Proměnné prostředí `APPLICATION_ID`, `APP_ACCESS_TOKEN` a `API_URL` jsou dostupné uvnitř obslužné funkce (stejně jako u jakékoli jiné logické funkce), takže můžete volat Twenty API s aplikačním přístupovým tokenem omezeným na vaši aplikaci.
* Na jednu aplikaci je povolena pouze jedna postinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna.
* Identifikátor `universalIdentifier` funkce se během sestavení automaticky nastaví v manifestu aplikace jako `postInstallLogicFunctionUniversalIdentifier` — není potřeba jej uvádět v `defineApplication()`.
* Atributy funkce `universalIdentifier`, `shouldRunOnVersionUpgrade` a `shouldRunSynchronously` jsou během buildu automaticky připojeny k manifestu aplikace do pole `postInstallLogicFunction` — není potřeba je uvádět v `defineApplication()`.
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší úlohy nastavení, jako je naplnění daty.
* **Nespouští se v režimu dev**: když je aplikace registrována lokálně (pomocí `yarn twenty dev`), server zcela přeskočí instalační tok a synchronizuje soubory přímo prostřednictvím sledovače CLI — takže se post-install v režimu dev nikdy nespustí bez ohledu na `shouldRunSynchronously`. Použijte `yarn twenty exec --postInstall` k ručnímu spuštění nad běžícím pracovním prostorem.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Definujte předinstalační logickou funkci (jedna na aplikaci)">
Funkce pre-install je logická funkce, která se během instalace spouští automaticky, **před aplikováním migrace metadat pracovního prostoru**. Má stejný tvar payloadu jako post-install (`InstallPayload`), ale je zařazena dříve v instalačním toku, aby mohla připravit stav, na němž nadcházející migrace závisí — typické použití zahrnuje zálohování dat, ověření kompatibility s novým schématem nebo archivaci záznamů, které se chystají přeuspořádat nebo odstranit.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
Předinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Hlavní body:
* Funkce pre-install používají `definePreInstallLogicFunction()` — stejné specializované nastavení jako u post-install, pouze připojené k jiné fázi životního cyklu.
* Obě obslužné funkce pre- i post-install přijímají stejný typ `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importujte jej jednou a znovu použijte pro oba hooky.
* **Kdy se hook spouští**: umístěn těsně před migrací metadat pracovního prostoru (`synchronizeFromManifest`). Před spuštěním server provede čistě aditivní "zjednodušenou synchronizaci", která v metadatech pracovního prostoru zaregistruje pre-install funkci **nové** verze — ničeho dalšího se nedotkne — a poté ji spustí. Protože tato synchronizace je pouze aditivní, objekty, pole a data předchozí verze zůstávají při spuštění vaší obslužné funkce zachována: můžete bezpečně číst a zálohovat stav před migrací.
* **Model provádění**: pre-install se provádí **synchronně** a **blokuje instalaci**. Pokud obslužná funkce vyvolá výjimku, instalace se přeruší ještě před aplikováním jakýchkoli změn schématu — pracovní prostor zůstane na předchozí verzi v konzistentním stavu. Je to záměrné: pre-install je vaše poslední šance odmítnout rizikovou aktualizaci.
* Stejně jako u post-install je na jednu aplikaci povolena pouze jedna funkce pre-install. Během buildu je automaticky připojena k manifestu aplikace pod `preInstallLogicFunction`.
* **Nespouští se v režimu dev**: stejně jako u post-install — u lokálně registrovaných aplikací je instalační tok zcela přeskočen, takže se pre-install pod `yarn twenty dev` nikdy nespustí. Použijte `yarn twenty exec --preInstall` k ručnímu spuštění.
</Accordion>
<Accordion title="Pre-install vs post-install: kdy použít který" description="Výběr správného instalačního hooku">
Oba hooky jsou součástí téhož instalačního toku a přijímají stejný `InstallPayload`. Rozdíl je v tom, **kdy** se spouštějí vzhledem k migraci metadat pracovního prostoru, a to určuje, jakých dat se mohou bezpečně dotýkat.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
Pre-install je vždy **synchronní** (blokuje instalaci a může ji přerušit). Post-install je **ve výchozím nastavení asynchronní** — zařazen do workeru s automatickými pokusy o opakování — ale může přejít na synchronní provádění pomocí `shouldRunSynchronously: true`. Viz accordion `definePostInstallLogicFunction` výše, kdy použít jednotlivé režimy.
**Použijte `post-install` pro cokoli, co vyžaduje existenci nového schématu.** To je běžný případ:
* Plnění výchozími daty (vytváření počátečních záznamů, výchozích pohledů, demo obsahu) vůči nově přidaným objektům a polím.
* Registrace webhooků u služeb třetích stran poté, co má aplikace své přihlašovací údaje.
* Volání vlastního API k dokončení nastavení, které závisí na synchronizovaných metadatech.
* Idempotentní logika "zajisti, že to existuje", která má při každé aktualizaci uvést stav do souladu — kombinujte s `shouldRunOnVersionUpgrade: true`.
Příklad — po instalaci naplňte výchozí záznam `PostCard`:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**Použijte `pre-install`, pokud by migrace jinak zničila nebo poškodila existující data.** Protože pre-install běží proti *předchozímu* schématu a jeho selhání vrací aktualizaci zpět, je to správné místo pro cokoli rizikového:
* **Zálohování dat, která se chystají odstranit nebo přeuspořádat** — např. odstraňujete pole ve verzi v2 a potřebujete jeho hodnoty zkopírovat do jiného pole nebo je před spuštěním migrace exportovat do úložiště.
* **Archivace záznamů, které by nové omezení zneplatnilo** — např. pole se stává `NOT NULL` a je třeba nejprve smazat nebo opravit řádky s hodnotami null.
* **Ověření kompatibility a odmítnutí aktualizace, pokud nelze aktuální data čistě migrovat** — vyhoďte výjimku z obslužné funkce a instalace se ukončí bez provedených změn. Je to bezpečnější, než zjistit nekompatibilitu uprostřed migrace.
* **Přejmenování nebo změna klíčů dat** před změnou schématu, která by ztratila vazby.
Příklad — archivujte záznamy před destruktivní migrací:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**Zlaté pravidlo:**
| Chcete… | Použít |
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Naplňte výchozí data, nakonfigurujte pracovní prostor, zaregistrujte externí prostředky | `post-install` |
| Spusťte dlouho běžící plnění nebo volání třetích stran, která by neměla blokovat odezvu instalace | `post-install` (výchozí — `shouldRunSynchronously: false`, s opakovanými pokusy workeru) |
| Spusťte rychlé nastavení, na které bude volající spoléhat ihned po návratu volání instalace | `post-install` s `shouldRunSynchronously: true` |
| Čtěte nebo zálohujte data, která by nadcházející migrace ztratila | `pre-install` |
| Odmítněte aktualizaci, která by poškodila existující data | `pre-install` (vyhoďte výjimku z obslužné funkce) |
| Spouštějte srovnání stavu při každé aktualizaci | `post-install` s `shouldRunOnVersionUpgrade: true` |
| Proveďte jednorázové nastavení pouze při první instalaci | `post-install` s `shouldRunOnVersionUpgrade: false` (výchozí) |
<Note>
Pokud si nejste jisti, výchozí volbou je **post-install**. Po pre-install sáhněte pouze tehdy, když je samotná migrace destruktivní a potřebujete zachytit předchozí stav, než zmizí.
</Note>
</Accordion>
<Accordion title="defineFrontComponent" description="Definujte frontendové komponenty pro vlastní uživatelské rozhraní">
@@ -756,7 +882,7 @@ export default defineFrontComponent({
});
```
Po synchronizaci pomocí `yarn twenty dev` se rychlá akce zobrazí v pravém horním rohu stránky:
Po synchronizaci pomocí `yarn twenty dev` (nebo po jednorázovém spuštění `yarn twenty dev --once`) se rychlá akce zobrazí v pravém horním rohu stránky:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Tlačítko rychlé akce v pravém horním rohu" />
@@ -1402,7 +1528,7 @@ export default defineFrontComponent({
### Jak funguje bundlování
Krok sestavení (`yarn twenty dev` nebo `yarn twenty build`) používá esbuild k vytvoření jediného samostatného souboru pro každou logickou funkci a každou frontendovou komponentu. Všechny importované balíčky jsou vloženy přímo do bundlu.
Krok sestavení používá esbuild k vytvoření jediného samostatného souboru pro každou logickou funkci a každou frontendovou komponentu. Všechny importované balíčky jsou vloženy přímo do bundlu.
**Logické funkce** běží v prostředí Node.js. Vestavěné moduly Node (`fs`, `path`, `crypto`, `http` atd.) jsou k dispozici a není je třeba instalovat.
@@ -1817,8 +1943,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute pre-install or post-install functions
yarn twenty exec --preInstall
# Execute the post-install function
yarn twenty exec --postInstall
```
@@ -98,6 +98,21 @@ Vývojový režim je k dispozici pouze na instancích Twenty běžících v rež
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Výstup terminálu ve vývojovém režimu" />
</div>
#### Jednorázová synchronizace pomocí `yarn twenty dev --once`
Pokud nechcete, aby na pozadí běžel watcher (například v CI pipeline, git hooku nebo skriptovaném workflow), použijte příznak `--once`. Spouští stejnou pipeline jako `yarn twenty dev` — sestaví manifest, zabalí soubory, nahraje, synchronizuje, znovu vygeneruje typovaného klienta API — ale **ukončí se, jakmile se synchronizace dokončí**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Příkaz | Chování | Kdy použít |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Sleduje vaše zdrojové soubory a při každé změně znovu spustí synchronizaci. Zůstává spuštěný, dokud jej nezastavíte. | Interaktivní lokální vývoj — chcete živý panel stavu a okamžitou zpětnovazebnou smyčku. |
| `yarn twenty dev --once` | Provede jedno sestavení + synchronizaci, poté ukončí běh s kódem `0` při úspěchu nebo `1` při neúspěchu. | Skripty, CI, pre-commit hooky, AI agenti a jakýkoli neinteraktivní pracovní postup. |
Oba režimy vyžadují server Twenty běžící v režimu vývoje a autentizovaný remote — platí stejné požadavky.
### Zobrazte svou aplikaci v Twenty
Otevřete ve svém prohlížeči [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Přejděte do **Settings > Apps** a vyberte kartu **Developer**. Vaše aplikace by měla být uvedena v části **Your Apps**:
@@ -66,12 +66,18 @@ Odkaz ke sdílení používá základní adresu URL serveru (bez jakékoli subdo
### Správa verzí
Při aktualizaci již nasazené tarballové aplikace server vyžaduje, aby hodnota `version` v `package.json` byla **přísně vyšší** (podle řazení [semver](https://semver.org)) než aktuálně nasazená verze. Opětovné nasazení stejné verze nebo odeslání nižší verze je odmítnuto ještě před uložením tarballu — v CLI uvidíte chybu `VERSION_ALREADY_EXISTS`.
Chcete-li vydat aktualizaci:
1. Zvyšte hodnotu pole `version` v souboru `package.json`
1. Zvyšte hodnotu pole `version` v souboru `package.json` (např. `1.2.3` → `1.2.4`, `1.3.0` nebo `2.0.0`)
2. Spusťte `yarn twenty deploy` (nebo `yarn twenty deploy --remote production`)
3. Pracovní prostory, které mají aplikaci nainstalovanou, uvidí dostupnou aktualizaci ve svém nastavení
<Note>
Předběžné tagy fungují podle očekávání: zvýšení z `1.0.0-rc.1` → `1.0.0-rc.2` je povoleno a finální vydání jako `1.0.0` je správně rozpoznáno jako vyšší než `1.0.0-rc.5`. Verze v `package.json` musí být platným řetězcem semver.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
## Publikování na npm
@@ -81,7 +87,7 @@ Publikování na npm zajistí, že bude vaše aplikace dohledatelná v Marketpla
### Požadavky
* Účet na [npm](https://www.npmjs.com)
* Klíčové slovo `twenty-app` ve vašem poli `keywords` v souboru `package.json` (již je zahrnuto, když založíte projekt pomocí `create-twenty-app`)
* Klíčové slovo `twenty-app` ve vašem poli `keywords` v souboru `package.json` (přidejte je ručně — ve výchozím nastavení není zahrnuto v šabloně `create-twenty-app`)
```json filename="package.json"
{
@@ -189,3 +195,12 @@ Aplikace můžete nainstalovat také z příkazového řádku:
```bash filename="Terminal"
yarn twenty install
```
<Note>
Server při instalaci vynucuje verzování semver a zrcadlí pravidla pro nasazení:
* Instalace stejné verze, která je již nainstalována ve vašem pracovním prostoru, je odmítnuta s chybou `APP_ALREADY_INSTALLED`.
* Instalace nižší verze, než je aktuálně nainstalovaná, je odmítnuta s chybou `CANNOT_DOWNGRADE_APPLICATION`.
K instalaci novější verze ji nejprve nasaďte nebo publikujte, poté znovu spusťte `yarn twenty install`.
</Note>
@@ -140,12 +140,12 @@ Data neodpovídají očekávanému formátu pro tento typ pole.
#### Datum
**Problém:** Nerozpoznaný formát data
**Řešení:** Používejte jednotný formát v celém souboru
**Řešení:** Používejte formát `YYYY-MM-DD` jednotně v celém souboru
```
✓ 2024-03-15 (YYYY-MM-DD - recommended)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
✓ 2024-03-15 (YYYY-MM-DD)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
```
#### Telefon
@@ -103,8 +103,6 @@ Adresa je **vnořené pole** s více sloupci (některé mohou zůstat prázdné)
Používejte jednotný formát v celém souboru:
* `YYYY-MM-DD` (doporučeno): `2024-03-15`
* `MM/DD/YYYY`: `03/15/2024`
* `DD/MM/YYYY`: `15/03/2024`
* ISO 8601: `2024-03-15T10:30:00Z`
### Číselná pole
@@ -47,7 +47,7 @@ npx nx run twenty-server:database:reset
#### Für Objekte in Core/Metadata-Schemas (TypeORM)
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate
```
## Technologie-Stack
@@ -476,7 +476,7 @@ export default defineLogicFunction({
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
@@ -643,49 +643,15 @@ export default defineLogicFunction({
**Schreiben Sie eine gute `description`.** KI-Agenten verlassen sich auf das `description`-Feld der Funktion, um zu entscheiden, wann das Tool verwendet werden soll. Seien Sie konkret darin, was das Tool tut und wann es aufgerufen werden soll.
</Note>
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Eine Pre-Installations-Logikfunktion definieren (eine pro App)">
Eine Pre-Installationsfunktion ist eine Logikfunktion, die automatisch ausgeführt wird, bevor Ihre App in einem Arbeitsbereich installiert wird. Dies ist nützlich für Validierungsaufgaben, Überprüfungen von Voraussetzungen oder die Vorbereitung des Status des Arbeitsbereichs, bevor die Hauptinstallation fortgesetzt wird.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Sie können die Pre-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Hauptpunkte:
* Pre-Installationsfunktionen verwenden `definePreInstallLogicFunction()` — eine spezialisierte Variante, die Trigger-Einstellungen (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`) weglässt.
* Der Handler erhält ein `InstallLogicFunctionPayload` mit `{ previousVersion: string }` — die Version der App, die zuvor installiert war (oder eine leere Zeichenkette bei Neuinstallationen).
* Pro Anwendung ist nur eine Pre-Installationsfunktion zulässig. Der Manifest-Build schlägt fehl, wenn mehr als eine erkannt wird.
* Der `universalIdentifier` der Funktion wird während des Builds im Anwendungsmanifest automatisch als `preInstallLogicFunctionUniversalIdentifier` gesetzt — Sie müssen ihn nicht in `defineApplication()` referenzieren.
* Das standardmäßige Timeout ist auf 300 Sekunden (5 Minuten) festgelegt, um längere Vorbereitungsvorgänge zu ermöglichen.
</Accordion>
<Accordion title="definePostInstallLogicFunction" description="Eine Post-Installations-Logikfunktion definieren (eine pro App)">
Eine Post-Installationsfunktion ist eine Logikfunktion, die automatisch ausgeführt wird, nachdem Ihre App in einem Arbeitsbereich installiert wurde. Dies ist nützlich für einmalige Einrichtungsvorgänge wie das Befüllen mit Standarddaten, das Erstellen erster Datensätze oder das Konfigurieren von Arbeitsbereichseinstellungen.
Eine Post-Installationsfunktion ist eine Logikfunktion, die automatisch ausgeführt wird, nachdem Ihre App in einem Arbeitsbereich installiert wurde. Der Server führt sie **nach** der Synchronisierung der Metadaten der App und der Generierung des SDK-Clients aus, sodass der Arbeitsbereich vollständig einsatzbereit ist und das neue Schema bereitsteht. Typische Anwendungsfälle umfassen das Befüllen von Standarddaten, das Erstellen anfänglicher Datensätze, das Konfigurieren von Arbeitsbereichseinstellungen oder das Bereitstellen von Ressourcen bei Diensten von Drittanbietern.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
@@ -694,6 +660,8 @@ export default definePostInstallLogicFunction({
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
@@ -706,10 +674,168 @@ yarn twenty exec --postInstall
Hauptpunkte:
* Post-Installationsfunktionen verwenden `definePostInstallLogicFunction()` — eine spezialisierte Variante, die Trigger-Einstellungen (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`) weglässt.
* Der Handler erhält ein `InstallLogicFunctionPayload` mit `{ previousVersion: string }` — die Version der App, die zuvor installiert war (oder eine leere Zeichenkette bei Neuinstallationen).
* Der Handler erhält ein `InstallPayload` mit `{ previousVersion?: string; newVersion: string }` — `newVersion` ist die zu installierende Version, und `previousVersion` ist die zuvor installierte Version (oder `undefined` bei einer Neuinstallation). Verwenden Sie diese Werte, um Neuinstallationen von Upgrades zu unterscheiden und versionsspezifische Migrationslogik auszuführen.
* **Wann der Hook ausgeführt wird**: standardmäßig nur bei Neuinstallationen. Übergeben Sie `shouldRunOnVersionUpgrade: true`, wenn er auch beim Upgrade der App von einer vorherigen Version ausgeführt werden soll. Wenn weggelassen, ist das Flag standardmäßig `false` und Upgrades überspringen den Hook.
* **Ausführungsmodell — standardmäßig asynchron, synchron optional**: Das Flag `shouldRunSynchronously` steuert, *wie* Post-Install ausgeführt wird.
* `shouldRunSynchronously: false` *(Standard)* — der Hook wird **in die Nachrichtenwarteschlange eingereiht** mit `retryLimit: 3` und läuft asynchron in einem Worker. Die Installationsantwort kommt zurück, sobald der Job eingereiht ist, sodass ein langsamer oder fehlschlagender Handler den Aufrufer nicht blockiert. Der Worker versucht es bis zu dreimal erneut. **Verwenden Sie dies für lang laufende Jobs** — das Befüllen großer Datensätze, Aufrufe langsamer Drittanbieter-APIs, Bereitstellung externer Ressourcen, alles, was ein vernünftiges HTTP-Antwortfenster überschreiten könnte.
* `shouldRunSynchronously: true` — der Hook wird **inline während des Installationsablaufs** ausgeführt (gleicher Executor wie bei Pre-Install). Die Installationsanforderung blockiert, bis der Handler fertig ist, und wenn er einen Fehler wirft, erhält der Installationsaufrufer einen `POST_INSTALL_ERROR`. Keine automatischen Wiederholungen. **Verwenden Sie dies für schnelle Aufgaben, die vor der Antwort abgeschlossen sein müssen** — z. B. um dem Benutzer einen Validierungsfehler auszugeben oder für eine schnelle Einrichtung, auf die der Client unmittelbar nach der Rückkehr des Installationsaufrufs angewiesen ist. Beachten Sie, dass die Metadatenmigration bereits angewendet wurde, wenn Post-Install läuft, sodass ein Fehler im Synchronmodus die Schemaänderungen **nicht** rückgängig macht — er zeigt lediglich den Fehler an.
* Stellen Sie sicher, dass Ihr Handler idempotent ist. Im asynchronen Modus kann die Warteschlange bis zu dreimal erneut versuchen; in beiden Modi kann der Hook bei Upgrades erneut laufen, wenn `shouldRunOnVersionUpgrade: true`.
* Die Umgebungsvariablen `APPLICATION_ID`, `APP_ACCESS_TOKEN` und `API_URL` sind im Handler verfügbar (wie bei jeder anderen Logikfunktion), sodass Sie die Twenty API mit einem auf Ihre App beschränkten Anwendungszugriffstoken aufrufen können.
* Pro Anwendung ist nur eine Post-Installationsfunktion zulässig. Der Manifest-Build schlägt fehl, wenn mehr als eine erkannt wird.
* Der `universalIdentifier` der Funktion wird während des Builds im Anwendungsmanifest automatisch als `postInstallLogicFunctionUniversalIdentifier` gesetzt — Sie müssen ihn nicht in `defineApplication()` referenzieren.
* Die `universalIdentifier`, `shouldRunOnVersionUpgrade` und `shouldRunSynchronously` der Funktion werden während des Builds automatisch dem Anwendungsmanifest unter dem Feld `postInstallLogicFunction` hinzugefügt — Sie müssen sie in `defineApplication()` nicht referenzieren.
* Das standardmäßige Timeout ist auf 300 Sekunden (5 Minuten) festgelegt, um längere Einrichtungsvorgänge wie Daten-Seeding zu ermöglichen.
* **Nicht im Dev-Modus ausgeführt**: Wenn eine App lokal registriert ist (über `yarn twenty dev`), überspringt der Server den Installationsablauf vollständig und synchronisiert Dateien direkt über den CLI-Watcher — daher läuft Post-Install im Dev-Modus nie, unabhängig von `shouldRunSynchronously`. Verwenden Sie `yarn twenty exec --postInstall`, um es manuell gegen einen laufenden Workspace auszulösen.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Eine Pre-Installations-Logikfunktion definieren (eine pro App)">
Eine Pre-Install-Funktion ist eine Logikfunktion, die automatisch während der Installation ausgeführt wird, **bevor die Metadatenmigration des Workspaces angewendet wird**. Sie hat die gleiche Payload-Struktur wie Post-Install (`InstallPayload`), ist aber früher im Installationsablauf positioniert, sodass sie Zustände vorbereiten kann, von denen die bevorstehende Migration abhängt — typische Anwendungsfälle sind das Sichern von Daten, die Validierung der Kompatibilität mit dem neuen Schema oder das Archivieren von Datensätzen, die umstrukturiert oder entfernt werden sollen.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
Sie können die Pre-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Hauptpunkte:
* Pre-Install-Funktionen verwenden `definePreInstallLogicFunction()` — dieselbe spezialisierte Konfiguration wie bei Post-Install, nur an einen anderen Lifecycle-Slot gebunden.
* Sowohl Pre- als auch Post-Install-Handler erhalten denselben `InstallPayload`-Typ: `{ previousVersion?: string; newVersion: string }`. Importieren Sie ihn einmal und verwenden Sie ihn für beide Hooks wieder.
* **Wann der Hook ausgeführt wird**: positioniert direkt vor der Metadatenmigration des Workspaces (`synchronizeFromManifest`). Vor der Ausführung führt der Server einen rein additiven "pared-down sync" durch, der die Pre-Install-Funktion der **neuen** Version in den Workspace-Metadaten registriert — sonst wird nichts angefasst — und führt sie dann aus. Da dieser Sync nur additiv ist, sind die Objekte, Felder und Daten der vorherigen Version noch intakt, wenn Ihr Handler läuft: Sie können den Zustand vor der Migration gefahrlos lesen und sichern.
* **Ausführungsmodell**: Pre-Install wird **synchron** ausgeführt und **blockiert die Installation**. Wenn der Handler einen Fehler wirft, wird die Installation abgebrochen, bevor Schemaänderungen angewendet werden — der Workspace verbleibt in der vorherigen Version in einem konsistenten Zustand. Das ist beabsichtigt: Pre-Install ist Ihre letzte Chance, ein riskantes Upgrade abzulehnen.
* Wie bei Post-Install ist pro Anwendung nur eine Pre-Installationsfunktion zulässig. Sie wird während des Builds automatisch dem Anwendungsmanifest unter `preInstallLogicFunction` hinzugefügt.
* **Nicht im Dev-Modus ausgeführt**: wie bei Post-Install — der Installationsablauf wird für lokal registrierte Apps vollständig übersprungen, daher läuft Pre-Install unter `yarn twenty dev` nie. Verwenden Sie `yarn twenty exec --preInstall`, um es manuell auszulösen.
</Accordion>
<Accordion title="Pre-Install vs. Post-Install: wann was verwenden" description="Den richtigen Installations-Hook wählen">
Beide Hooks sind Teil desselben Installationsablaufs und erhalten dasselbe `InstallPayload`. Der Unterschied besteht darin, **wann** sie relativ zur Metadatenmigration des Workspaces ausgeführt werden, und das ändert, auf welche Daten sie gefahrlos zugreifen können.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
Pre-Install ist immer **synchron** (blockiert die Installation und kann sie abbrechen). Post-Install ist **standardmäßig asynchron** — in einen Worker eingereiht mit automatischen Wiederholungen — kann aber per `shouldRunSynchronously: true` in die synchrone Ausführung wechseln. Siehe das Akkordeon zu `definePostInstallLogicFunction` oben, wann welcher Modus zu verwenden ist.
**Verwenden Sie `post-install` für alles, wofür das neue Schema existieren muss.** Dies ist der Regelfall:
* Standarddaten befüllen (Anlegen anfänglicher Datensätze, Standardansichten, Demo-Inhalte) für neu hinzugefügte Objekte und Felder.
* Registrieren von Webhooks bei Drittanbieter-Diensten, jetzt, da die App ihre Anmeldedaten hat.
* Aufrufen Ihrer eigenen API, um eine Einrichtung abzuschließen, die von den synchronisierten Metadaten abhängt.
* Idempotente "Stelle sicher, dass dies existiert"-Logik, die bei jedem Upgrade den Zustand abgleichen soll — kombinieren Sie dies mit `shouldRunOnVersionUpgrade: true`.
Beispiel — nach der Installation einen Standard-`PostCard`-Datensatz anlegen:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**Verwenden Sie `pre-install`, wenn eine Migration ansonsten vorhandene Daten löschen oder beschädigen würde.** Da Pre-Install gegen das vorherige Schema läuft und ein Fehlschlag das Upgrade zurückrollt, ist es der richtige Ort für alles Riskante:
* **Sichern von Daten, die gleich gelöscht oder umstrukturiert werden** — z. B. Sie entfernen in v2 ein Feld und müssen dessen Werte vor der Migration in ein anderes Feld kopieren oder in einen Speicher exportieren.
* **Archivieren von Datensätzen, die eine neue Einschränkung ungültig machen würde** — z. B. ein Feld wird `NOT NULL` und Sie müssen zuerst Zeilen mit Null-Werten löschen oder korrigieren.
* **Kompatibilität validieren und das Upgrade ablehnen, wenn die aktuellen Daten nicht sauber migriert werden können** — werfen Sie im Handler einen Fehler, und die Installation wird ohne Änderungen abgebrochen. Das ist sicherer, als die Inkompatibilität mitten in der Migration zu entdecken.
* **Daten umbenennen oder Schlüssel neu zuweisen** vor einer Schemaänderung, bei der sonst die Zuordnung verloren ginge.
Beispiel — Datensätze vor einer destruktiven Migration archivieren:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**Faustregel:**
| Sie möchten … | Verwenden |
| ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Standarddaten befüllen, den Workspace konfigurieren, externe Ressourcen registrieren | `post-install` |
| Lang laufendes Seeding oder Drittanbieteraufrufe ausführen, die die Installationsantwort nicht blockieren sollten | `post-install` (Standard — `shouldRunSynchronously: false`, mit Worker-Wiederholungen) |
| Schnelle Einrichtung ausführen, auf die sich der Aufrufer unmittelbar nach der Rückkehr des Installationsaufrufs verlassen wird | `post-install` mit `shouldRunSynchronously: true` |
| Daten lesen oder sichern, die bei der bevorstehenden Migration verloren gingen | `pre-install` |
| Ein Upgrade ablehnen, das vorhandene Daten beschädigen würde | `pre-install` (`throw` im Handler) |
| Bei jedem Upgrade einen Abgleich ausführen | `post-install` mit `shouldRunOnVersionUpgrade: true` |
| Einmalige Einrichtung nur bei der ersten Installation durchführen | `post-install` mit `shouldRunOnVersionUpgrade: false` (Standard) |
<Note>
Im Zweifel auf **Post-Install** setzen. Greifen Sie nur zu Pre-Install, wenn die Migration selbst destruktiv ist und Sie den vorherigen Zustand abfangen müssen, bevor er verloren geht.
</Note>
</Accordion>
<Accordion title="defineFrontComponent" description="Frontend-Komponenten für benutzerdefinierte UI definieren">
@@ -755,7 +881,7 @@ export default defineFrontComponent({
});
```
Nach dem Synchronisieren mit `yarn twenty dev` erscheint die Schnellaktion oben rechts auf der Seite:
Nach dem Synchronisieren mit `yarn twenty dev` (oder durch einmaliges Ausführen von `yarn twenty dev --once`) erscheint die Schnellaktion oben rechts auf der Seite:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Schnellaktionsschaltfläche oben rechts" />
@@ -1401,7 +1527,7 @@ export default defineFrontComponent({
### Wie das Bundling funktioniert
Der Build-Schritt (`yarn twenty dev` oder `yarn twenty build`) verwendet esbuild, um pro Logikfunktion und pro Frontend-Komponente eine einzelne, in sich geschlossene Datei zu erzeugen. Alle importierten Pakete werden in das Bundle eingebettet.
Der Build-Schritt verwendet esbuild, um pro Logikfunktion und pro Frontend-Komponente eine einzelne, in sich geschlossene Datei zu erzeugen. Alle importierten Pakete werden in das Bundle eingebettet.
**Logikfunktionen** laufen in einer Node.js-Umgebung. Eingebaute Node.js-Module (`fs`, `path`, `crypto`, `http` usw.) stehen zur Verfügung und müssen nicht installiert werden.
@@ -1816,8 +1942,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute pre-install or post-install functions
yarn twenty exec --preInstall
# Execute the post-install function
yarn twenty exec --postInstall
```
@@ -98,6 +98,21 @@ Der Dev-Modus ist nur auf Twenty-Instanzen verfügbar, die im Entwicklungsmodus
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Terminalausgabe im Dev-Modus" />
</div>
#### Einmalige Synchronisierung mit `yarn twenty dev --once`
Wenn Sie keinen Watcher im Hintergrund ausführen lassen möchten (zum Beispiel in einer CI-Pipeline, einem Git-Hook oder einem skriptgesteuerten Workflow), geben Sie das Flag `--once` an. Es führt dieselbe Pipeline aus wie `yarn twenty dev` — Build-Manifest erstellen, Dateien bündeln, hochladen, synchronisieren, den typisierten API-Client neu generieren — beendet sich jedoch, **sobald die Synchronisierung abgeschlossen ist**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Befehl | Verhalten | Wann verwenden |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Überwacht Ihre Quelldateien und synchronisiert bei jeder Änderung erneut. Läuft weiter, bis Sie es stoppen. | Interaktive lokale Entwicklung — Sie möchten das Live-Status-Panel und eine sofortige Feedback-Schleife. |
| `yarn twenty dev --once` | Führt einen einzelnen Build + Sync aus und beendet sich anschließend mit Code `0` bei Erfolg oder `1` bei einem Fehler. | Skripte, CI, Pre-Commit-Hooks, KI-Agenten und jeder nicht-interaktive Workflow. |
Beide Modi erfordern einen Twenty-Server, der im Entwicklungsmodus läuft, und ein authentifiziertes Remote — es gelten dieselben Voraussetzungen.
### Sehen Sie sich Ihre App in Twenty an
Öffnen Sie [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) in Ihrem Browser. Navigieren Sie zu **Settings > Apps** und wählen Sie die Registerkarte **Developer**. Unter **Your Apps** sollte Ihre App aufgeführt sein:
@@ -66,12 +66,18 @@ Der Freigabelink verwendet die Basis-URL des Servers (ohne Workspace-Subdomain),
### Versionsverwaltung
Beim Aktualisieren einer bereits bereitgestellten Tarball-App verlangt der Server, dass die `version` in `package.json` **strikt höher** (gemäß der [semver](https://semver.org)-Reihenfolge) ist als die derzeit bereitgestellte Version. Das erneute Bereitstellen derselben Version oder das Pushen einer niedrigeren Version wird abgelehnt, bevor das Tarball gespeichert wird — in der CLI wird ein `VERSION_ALREADY_EXISTS`-Fehler angezeigt.
So veröffentlichen Sie ein Update:
1. Erhöhen Sie das Feld `version` in Ihrer `package.json`
1. Erhöhen Sie das Feld `version` in Ihrer `package.json` (z. B. `1.2.3` → `1.2.4`, `1.3.0` oder `2.0.0`).
2. Führen Sie `yarn twenty deploy` aus (oder `yarn twenty deploy --remote production`)
3. Arbeitsbereiche, die die App installiert haben, sehen in ihren Einstellungen, dass ein Upgrade verfügbar ist.
<Note>
Pre-Release-Tags funktionieren wie erwartet: Das Erhöhen von `1.0.0-rc.1` → `1.0.0-rc.2` ist zulässig, und eine finale Version wie `1.0.0` wird korrekt als höher als `1.0.0-rc.5` erkannt. Die Version in `package.json` muss selbst eine gültige semver-Zeichenfolge sein.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
## Auf npm veröffentlichen
@@ -81,7 +87,7 @@ Die Veröffentlichung auf npm macht Ihre App im Twenty-Marktplatz auffindbar. Je
### Anforderungen
* Ein [npm](https://www.npmjs.com)-Konto
* Das Schlüsselwort `twenty-app` in Ihrem `package.json`-Array `keywords` (bereits enthalten, wenn Sie mit `create-twenty-app` ein Gerüst erstellen)
* Das Schlüsselwort `twenty-app` in Ihrem `package.json`-Array `keywords` (manuell hinzufügen — es ist in der `create-twenty-app`-Vorlage standardmäßig nicht enthalten)
```json filename="package.json"
{
@@ -189,3 +195,12 @@ Sie können Apps auch über die Befehlszeile installieren:
```bash filename="Terminal"
yarn twenty install
```
<Note>
Der Server erzwingt bei der Installation semver-Versionierung und spiegelt damit die Regeln beim Bereitstellen wider:
* Die Installation derselben Version, die in Ihrem Arbeitsbereich bereits installiert ist, wird mit einem `APP_ALREADY_INSTALLED`-Fehler abgelehnt.
* Die Installation einer niedrigeren Version als die aktuell installierte wird mit einem `CANNOT_DOWNGRADE_APPLICATION`-Fehler abgelehnt.
Um eine neuere Version zu installieren, stellen Sie sie zuerst bereit oder veröffentlichen Sie sie und führen Sie dann `yarn twenty install` erneut aus.
</Note>
@@ -140,12 +140,12 @@ Die Daten entsprechen nicht dem erwarteten Format für diesen Feldtyp.
#### Datum
**Problem:** Nicht erkanntes Datumsformat
**Lösung:** Verwenden Sie in der gesamten Datei ein einheitliches Format
**Lösung:** Verwenden Sie in der gesamten Datei durchgängig das Format `YYYY-MM-DD`
```
✓ 2024-03-15 (YYYY-MM-DD - empfohlen)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
✓ 2024-03-15 (YYYY-MM-DD)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
```
#### Telefon
@@ -103,8 +103,6 @@ Adresse ist ein **verschachteltes Feld** mit mehreren Spalten (einige können le
Verwenden Sie in Ihrer Datei ein einheitliches Format:
* `YYYY-MM-DD` (empfohlen): `2024-03-15`
* `MM/DD/YYYY`: `03/15/2024`
* `DD/MM/YYYY`: `15/03/2024`
* ISO 8601: `2024-03-15T10:30:00Z`
### Zahlenfelder
@@ -47,7 +47,7 @@ npx nx run twenty-server:database:reset
#### Para objetos en esquemas Core/Metadata (TypeORM)
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate
```
## Stack Tecnológico
@@ -47,7 +47,7 @@ npx nx run twenty-server:database:reset
#### Per oggetti negli schemi Core/Metadata (TypeORM)
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate
```
## Tech Stack
@@ -476,7 +476,7 @@ export default defineLogicFunction({
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
@@ -643,49 +643,15 @@ export default defineLogicFunction({
**Scrivi una buona `description`.** Gli agenti IA fanno affidamento sul campo `description` della funzione per decidere quando usare lo strumento. Sii specifico su cosa fa lo strumento e quando dovrebbe essere invocato.
</Note>
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Definisci una funzione logica di pre-installazione (una per app)">
Una funzione di pre-installazione è una funzione logica che viene eseguita automaticamente prima che la tua app venga installata in uno spazio di lavoro. È utile per attività di convalida, controlli dei prerequisiti o per preparare lo stato dello spazio di lavoro prima che proceda l'installazione principale.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Puoi anche eseguire manualmente la funzione di pre-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Punti chiave:
* Le funzioni di pre-installazione utilizzano `definePreInstallLogicFunction()` — una variante specializzata che omette le impostazioni dei trigger (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* L'handler riceve un `InstallLogicFunctionPayload` con `{ previousVersion: string }` — la versione dell'app precedentemente installata (oppure una stringa vuota per nuove installazioni).
* È consentita una sola funzione di pre-installazione per applicazione. La build del manifesto genererà un errore se ne viene rilevata più di una.
* L'`universalIdentifier` della funzione viene impostato automaticamente come `preInstallLogicFunctionUniversalIdentifier` nel manifesto dell'applicazione durante la build — non è necessario farvi riferimento in `defineApplication()`.
* Il timeout predefinito è impostato a 300 secondi (5 minuti) per consentire attività di preparazione più lunghe.
</Accordion>
<Accordion title="definePostInstallLogicFunction" description="Definisci una funzione logica di post-installazione (una per app)">
Una funzione post-installazione è una funzione logica che viene eseguita automaticamente dopo che la tua app è stata installata in uno spazio di lavoro. Questo è utile per attività di configurazione una tantum come il popolamento di dati predefiniti, la creazione di record iniziali o la configurazione delle impostazioni dello spazio di lavoro.
Una funzione post-installazione è una funzione logica che viene eseguita automaticamente dopo che la tua app è stata installata in uno spazio di lavoro. Il server la esegue **dopo** che i metadati dell'app sono stati sincronizzati e il client SDK è stato generato, così lo spazio di lavoro è completamente pronto per l'uso e il nuovo schema è attivo. I casi d'uso tipici includono il popolamento di dati predefiniti, la creazione di record iniziali, la configurazione delle impostazioni dello spazio di lavoro o il provisioning di risorse su servizi di terze parti.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
@@ -694,6 +660,8 @@ export default definePostInstallLogicFunction({
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
@@ -706,10 +674,168 @@ yarn twenty exec --postInstall
Punti chiave:
* Le funzioni di post-installazione utilizzano `definePostInstallLogicFunction()` — una variante specializzata che omette le impostazioni dei trigger (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* L'handler riceve un `InstallLogicFunctionPayload` con `{ previousVersion: string }` — la versione dell'app precedentemente installata (oppure una stringa vuota per nuove installazioni).
* L'handler riceve un `InstallPayload` con `{ previousVersion?: string; newVersion: string }` — `newVersion` è la versione in fase di installazione e `previousVersion` è la versione installata in precedenza (oppure `undefined` in caso di nuova installazione). Usa questi valori per distinguere le nuove installazioni dagli aggiornamenti e per eseguire logiche di migrazione specifiche per versione.
* **Quando viene eseguito l'hook**: solo sulle nuove installazioni, per impostazione predefinita. Passa `shouldRunOnVersionUpgrade: true` se vuoi che venga eseguito anche quando l'app viene aggiornata da una versione precedente. Se omesso, il flag è `false` per impostazione predefinita e gli aggiornamenti saltano l'hook.
* **Modello di esecuzione — asincrono per impostazione predefinita, sincrono su richiesta**: il flag `shouldRunSynchronously` controlla *come* viene eseguito il post-install.
* `shouldRunSynchronously: false` *(default)* — l'hook viene **messo in coda nella coda dei messaggi** con `retryLimit: 3` ed eseguito in modo asincrono in un worker. La risposta di installazione viene restituita non appena il job è messo in coda, quindi un handler lento o in errore non blocca il chiamante. Il worker riproverà fino a tre volte. **Usalo per job di lunga durata** — popolamento di dataset di grandi dimensioni, chiamate a API di terze parti lente, provisioning di risorse esterne, qualsiasi cosa che possa superare una finestra di risposta HTTP ragionevole.
* `shouldRunSynchronously: true` — l'hook viene eseguito **inline durante il flusso di installazione** (stesso executor del pre-install). La richiesta di installazione rimane bloccata finché l'handler non termina e, se genera un'eccezione, il chiamante dell'installazione riceve un `POST_INSTALL_ERROR`. Nessun tentativo automatico. **Usalo per attività rapide che devono completarsi prima della risposta** — ad esempio, emettere un errore di validazione all'utente, oppure un setup rapido di cui il client avrà bisogno immediatamente dopo il ritorno della chiamata di installazione. Tieni presente che la migrazione dei metadati è già stata applicata quando viene eseguito il post-install, quindi un errore in modalità sincrona **non** annulla le modifiche allo schema — si limita a far emergere l'errore.
* Assicurati che il tuo handler sia idempotente. In modalità asincrona la coda può riprovare fino a tre volte; in entrambe le modalità l'hook può essere eseguito di nuovo durante gli aggiornamenti quando `shouldRunOnVersionUpgrade: true`.
* Le variabili d'ambiente `APPLICATION_ID`, `APP_ACCESS_TOKEN` e `API_URL` sono disponibili all'interno dell'handler (come in qualsiasi altra funzione logica), quindi puoi chiamare le API di Twenty con un token di accesso applicativo con ambito sulla tua app.
* È consentita una sola funzione di post-installazione per applicazione. La build del manifesto genererà un errore se ne viene rilevata più di una.
* L'`universalIdentifier` della funzione viene impostato automaticamente come `postInstallLogicFunctionUniversalIdentifier` nel manifesto dell'applicazione durante la build — non è necessario farvi riferimento in `defineApplication()`.
* I campi `universalIdentifier`, `shouldRunOnVersionUpgrade` e `shouldRunSynchronously` della funzione vengono associati automaticamente al manifest dell'applicazione nel campo `postInstallLogicFunction` durante la build — non è necessario referenziarli in `defineApplication()`.
* Il timeout predefinito è impostato a 300 secondi (5 minuti) per consentire attività di configurazione più lunghe, come il popolamento dei dati.
* **Non eseguito in modalità dev**: quando un'app è registrata in locale (tramite `yarn twenty dev`), il server salta completamente il flusso di installazione e sincronizza i file direttamente tramite il watcher della CLI — quindi il post-install non viene mai eseguito in modalità dev, indipendentemente da `shouldRunSynchronously`. Usa `yarn twenty exec --postInstall` per attivarlo manualmente su un workspace in esecuzione.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Definisci una funzione logica di pre-installazione (una per app)">
Una funzione di pre-install è una funzione logica che viene eseguita automaticamente durante l'installazione, **prima che venga applicata la migrazione dei metadati del workspace**. Condivide la stessa struttura di payload del post-install (`InstallPayload`), ma è posizionata prima nel flusso di installazione così da poter preparare lo stato da cui dipenderà la migrazione imminente — usi tipici includono il backup dei dati, la validazione della compatibilità con il nuovo schema o l'archiviazione di record che stanno per essere ristrutturati o eliminati.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
Puoi anche eseguire manualmente la funzione di pre-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Punti chiave:
* Le funzioni di pre-install usano `definePreInstallLogicFunction()` — stessa configurazione specialistica del post-install, solo agganciata a uno slot di ciclo di vita diverso.
* Sia gli handler di pre- sia quelli di post-install ricevono lo stesso tipo `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importalo una volta e riutilizzalo per entrambi gli hook.
* **Quando viene eseguito l'hook**: posizionato appena prima della migrazione dei metadati del workspace (`synchronizeFromManifest`). Prima dell'esecuzione, il server esegue una "sincronizzazione ridotta" puramente additiva che registra nei metadati del workspace la funzione di pre-install della versione **nuova** — nient'altro viene toccato — e poi la esegue. Poiché questa sincronizzazione è solo additiva, gli oggetti, i campi e i dati della versione precedente restano intatti quando il tuo handler viene eseguito: puoi leggere ed eseguire in sicurezza il backup dello stato pre-migrazione.
* **Modello di esecuzione**: il pre-install è eseguito **in modo sincrono** e **blocca l'installazione**. Se l'handler genera un'eccezione, l'installazione viene interrotta prima che vengano applicate modifiche allo schema — il workspace rimane sulla versione precedente in uno stato coerente. Questo è intenzionale: il pre-install è la tua ultima possibilità per rifiutare un aggiornamento rischioso.
* Come per il post-install, è consentita una sola funzione di pre-installazione per applicazione. Viene collegata automaticamente al manifest dell'applicazione nel campo `preInstallLogicFunction` durante la build.
* **Non eseguito in modalità dev**: come per il post-install — il flusso di installazione viene completamente saltato per le app registrate localmente, quindi il pre-install non viene mai eseguito con `yarn twenty dev`. Usa `yarn twenty exec --preInstall` per attivarlo manualmente.
</Accordion>
<Accordion title="Pre-install vs post-install: quando usare l'uno o l'altro" description="Scegliere l'hook di installazione giusto">
Entrambi gli hook fanno parte dello stesso flusso di installazione e ricevono lo stesso `InstallPayload`. La differenza è **quando** vengono eseguiti rispetto alla migrazione dei metadati del workspace, e questo modifica quali dati possono gestire in sicurezza.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
Il pre-install è sempre **sincrono** (blocca l'installazione e può interromperla). Il post-install è **asincrono per impostazione predefinita** — messo in coda su un worker con retry automatici — ma può optare per l'esecuzione sincrona con `shouldRunSynchronously: true`. Vedi l'accordion `definePostInstallLogicFunction` sopra per quando usare ciascuna modalità.
**Usa `post-install` per tutto ciò che richiede l'esistenza del nuovo schema.** Questo è il caso più comune:
* Popolamento di dati predefiniti (creazione di record iniziali, viste predefinite, contenuti demo) su oggetti e campi appena aggiunti.
* Registrazione di webhook con servizi di terze parti ora che l'app ha le proprie credenziali.
* Chiamare la tua API per completare il setup che dipende dai metadati sincronizzati.
* Logica idempotente di "ensure this exists" che dovrebbe riconciliare lo stato a ogni aggiornamento — da combinare con `shouldRunOnVersionUpgrade: true`.
Esempio — eseguire il seeding di un record `PostCard` predefinito dopo l'installazione:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**Usa `pre-install` quando una migrazione altrimenti distruggerebbe o corromperebbe i dati esistenti.** Poiché il pre-install viene eseguito contro lo schema *precedente* e un suo fallimento annulla l'aggiornamento, è il posto giusto per qualsiasi operazione rischiosa:
* **Eseguire il backup dei dati che stanno per essere eliminati o ristrutturati** — ad esempio, stai rimuovendo un campo nella v2 e devi copiarne i valori in un altro campo o esportarli su uno storage prima che venga eseguita la migrazione.
* **Archiviare i record che un nuovo vincolo renderebbe non validi** — ad esempio, un campo sta diventando `NOT NULL` e devi prima eliminare o correggere le righe con valori nulli.
* **Validare la compatibilità e rifiutare l'aggiornamento se i dati attuali non possono essere migrati correttamente** — genera un'eccezione dall'handler e l'installazione si interrompe senza applicare modifiche. Questo è più sicuro che scoprire l'incompatibilità a migrazione in corso.
* **Rinominare o rigenerare le chiavi dei dati** prima di una modifica dello schema che farebbe perdere l'associazione.
Esempio — archiviare i record prima di una migrazione distruttiva:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**Regola generale:**
| Vuoi… | Usa |
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Popolare dati predefiniti, configurare il workspace, registrare risorse esterne | `post-install` |
| Eseguire seeding di lunga durata o chiamate a terze parti che non dovrebbero bloccare la risposta dell'installazione | `post-install` (predefinito — `shouldRunSynchronously: false`, con retry del worker) |
| Eseguire un setup rapido di cui il chiamante farà affidamento immediatamente dopo il ritorno della chiamata di installazione | `post-install` con `shouldRunSynchronously: true` |
| Leggere o eseguire il backup dei dati che la prossima migrazione perderebbe | `pre-install` |
| Rifiutare un aggiornamento che corromperebbe i dati esistenti | `pre-install` (genera un'eccezione dall'handler) |
| Eseguire la riconciliazione a ogni aggiornamento | `post-install` con `shouldRunOnVersionUpgrade: true` |
| Eseguire un setup una tantum solo alla prima installazione | `post-install` con `shouldRunOnVersionUpgrade: false` (predefinito) |
<Note>
In caso di dubbio, usa **post-install**. Ricorri al pre-install solo quando la migrazione stessa è distruttiva e devi intercettare lo stato precedente prima che vada perso.
</Note>
</Accordion>
<Accordion title="defineFrontComponent" description="Definisci componenti front-end per un'interfaccia utente personalizzata">
@@ -755,7 +881,7 @@ export default defineFrontComponent({
});
```
Dopo la sincronizzazione con `yarn twenty dev`, l'azione rapida appare nell'angolo in alto a destra della pagina:
Dopo la sincronizzazione con `yarn twenty dev` (o eseguendo una volta sola `yarn twenty dev --once`), l'azione rapida appare nell'angolo in alto a destra della pagina:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Pulsante di azione rapida nell'angolo in alto a destra" />
@@ -1401,7 +1527,7 @@ export default defineFrontComponent({
### Come funziona il bundling
La fase di build (`yarn twenty dev` o `yarn twenty build`) usa esbuild per produrre un singolo file autonomo per ogni funzione logica e per ogni componente front-end. Tutti i pacchetti importati sono incorporati nel bundle.
La fase di build usa esbuild per produrre un singolo file autonomo per ogni funzione logica e per ogni componente front-end. Tutti i pacchetti importati sono incorporati nel bundle.
**Le funzioni logiche** vengono eseguite in un ambiente Node.js. I moduli integrati di Node (`fs`, `path`, `crypto`, `http`, ecc.) sono disponibili e non necessitano di essere installati.
@@ -1816,8 +1942,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute pre-install or post-install functions
yarn twenty exec --preInstall
# Execute the post-install function
yarn twenty exec --postInstall
```
@@ -98,6 +98,21 @@ La modalità di sviluppo è disponibile solo sulle istanze di Twenty in esecuzio
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Output del terminale in modalità sviluppo" />
</div>
#### Sincronizzazione una tantum con `yarn twenty dev --once`
Se non vuoi un watcher in esecuzione in background (ad esempio in una pipeline CI, un hook di Git o un flusso di lavoro scriptato), passa il flag `--once`. Esegue la stessa pipeline di `yarn twenty dev` — genera il manifest, effettua il bundling dei file, carica, sincronizza, rigenera il client API tipizzato — ma **termina non appena la sincronizzazione è completata**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Comando | Comportamento | Quando usarlo |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Monitora i file sorgente e risincronizza a ogni modifica. Rimane in esecuzione finché non lo interrompi. | Sviluppo locale interattivo — vuoi il pannello di stato in tempo reale e un ciclo di feedback immediato. |
| `yarn twenty dev --once` | Esegue una singola build + sincronizzazione, quindi termina con codice `0` in caso di successo o `1` in caso di errore. | Script, CI, hook pre-commit, agenti IA e qualsiasi flusso di lavoro non interattivo. |
Entrambe le modalità richiedono un server Twenty in esecuzione in modalità di sviluppo e un remote autenticato — si applicano gli stessi prerequisiti.
### Visualizza la tua app in Twenty
Apri [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) nel browser. Vai su **Settings > Apps** e seleziona la scheda **Developer**. Dovresti vedere la tua app elencata in **Your Apps**:
@@ -66,12 +66,18 @@ Il link di condivisione utilizza l'URL di base del server (senza alcun sottodomi
### Gestione delle versioni
Quando si aggiorna un'app in formato tarball già distribuita, il server richiede che la `version` in `package.json` sia **strettamente superiore** (per l'ordinamento [semver](https://semver.org)) rispetto alla versione attualmente distribuita. Eseguire nuovamente il deploy della stessa versione, o pubblicarne una inferiore, viene rifiutato prima che il tarball venga archiviato — vedrai un errore `VERSION_ALREADY_EXISTS` nella CLI.
Per rilasciare un aggiornamento:
1. Incrementa il campo `version` nel tuo `package.json`
1. Incrementa il campo `version` nel tuo `package.json` (ad es. `1.2.3` → `1.2.4`, `1.3.0` o `2.0.0`).
2. Esegui `yarn twenty deploy` (oppure `yarn twenty deploy --remote production`)
3. Gli spazi di lavoro che hanno l'app installata vedranno l'aggiornamento disponibile nelle proprie impostazioni
<Note>
I tag di pre-release funzionano come previsto: incrementare `1.0.0-rc.1` → `1.0.0-rc.2` è consentito e una release finale come `1.0.0` viene correttamente riconosciuta come superiore a `1.0.0-rc.5`. La versione in `package.json` deve essere essa stessa una stringa semver valida.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
## Pubblicazione su npm
@@ -81,7 +87,7 @@ La pubblicazione su npm rende la tua app scopribile nel marketplace di Twenty. Q
### Requisiti
* Un account [npm](https://www.npmjs.com)
* La parola chiave `twenty-app` nell'array `keywords` del tuo `package.json` (già inclusa quando inizializzi con `create-twenty-app`)
* La parola chiave `twenty-app` nell'array `keywords` del tuo `package.json` (aggiungila manualmente — non è inclusa per impostazione predefinita nel template `create-twenty-app`)
```json filename="package.json"
{
@@ -189,3 +195,12 @@ Puoi anche installare le app dalla riga di comando:
```bash filename="Terminal"
yarn twenty install
```
<Note>
Il server applica il versioning semver durante linstallazione, rispecchiando le regole del deploy:
* Linstallazione della stessa versione già installata nel tuo spazio di lavoro viene rifiutata con un errore `APP_ALREADY_INSTALLED`.
* Linstallazione di una versione inferiore rispetto a quella attualmente installata viene rifiutata con un errore `CANNOT_DOWNGRADE_APPLICATION`.
Per installare una versione più recente, effettua prima il deploy o la pubblicazione, quindi riesegui `yarn twenty install`.
</Note>
@@ -140,12 +140,12 @@ I dati non corrispondono al formato previsto per quel tipo di campo.
#### Data
**Problema:** formato data non riconosciuto
**Soluzione:** usa un formato coerente in tutto il file
**Soluzione:** Usa il formato `YYYY-MM-DD` in modo coerente in tutto il file
```
✓ 2024-03-15 (YYYY-MM-DD - recommended)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
✓ 2024-03-15 (YYYY-MM-DD)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
```
#### Telefono
@@ -103,8 +103,6 @@ Indirizzo è un **campo annidato** con più colonne (alcune possono essere lasci
Usa un formato coerente in tutto il file:
* `YYYY-MM-DD` (consigliato): `2024-03-15`
* `MM/DD/YYYY`: `03/15/2024`
* `DD/MM/YYYY`: `15/03/2024`
* ISO 8601: `2024-03-15T10:30:00Z`
### Campi numerici
@@ -47,7 +47,7 @@ npx nx run twenty-server:database:reset
#### Para objetos nos esquemas Core/Metadata (TypeORM)
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate
```
## Pilha de Tecnologias
@@ -476,7 +476,7 @@ export default defineLogicFunction({
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
@@ -643,49 +643,15 @@ export default defineLogicFunction({
**Escreva uma boa `description`.** Os agentes de IA dependem do campo `description` da função para decidir quando usar a ferramenta. Seja específico sobre o que a ferramenta faz e quando ela deve ser chamada.
</Note>
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Defina uma função de lógica de pré-instalação (uma por aplicativo)">
Uma função de pré-instalação é uma função de lógica que é executada automaticamente antes de o seu aplicativo ser instalado em um espaço de trabalho. Isso é útil para tarefas de validação, verificações de pré-requisitos ou para preparar o estado do espaço de trabalho antes que a instalação principal prossiga.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Você também pode executar manualmente a função de pré-instalação a qualquer momento usando a CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Pontos-chave:
* As funções de pré-instalação usam `definePreInstallLogicFunction()` — uma variante especializada que omite as configurações de gatilho (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* O manipulador recebe um `InstallLogicFunctionPayload` com `{ previousVersion: string }` — a versão do app que foi instalada anteriormente (ou uma string vazia para instalações novas).
* É permitida apenas uma função de pré-instalação por app. A geração do manifesto apresentará erro se mais de uma for detectada.
* O `universalIdentifier` da função é definido automaticamente como `preInstallLogicFunctionUniversalIdentifier` no manifesto do aplicativo durante a geração — você não precisa referenciá-lo em `defineApplication()`.
* O tempo limite padrão é definido como 300 segundos (5 minutos) para permitir tarefas de preparação mais longas.
</Accordion>
<Accordion title="definePostInstallLogicFunction" description="Defina uma função de lógica de pós-instalação (uma por aplicativo)">
Uma função de pós-instalação é uma função de lógica que é executada automaticamente após o seu aplicativo ser instalado em um espaço de trabalho. Isso é útil para tarefas de configurão únicas, como preencher dados padrão, criar registros iniciais ou configurar as configurações do espaço de trabalho.
Uma função de pós-instalação é uma função lógica que é executada automaticamente assim que seu aplicativo terminar de ser instalado em um espaço de trabalho. O servidor a executa **depois** que os metadados do aplicativo forem sincronizados e o cliente do SDK for gerado, para que o espaço de trabalho esteja totalmente pronto para uso e o novo esquema esteja disponível. Casos de uso típicos incluem popular dados padrão, criar registros iniciais, configurar as definições do espaço de trabalho ou provisionar recursos em serviços de terceiros.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
@@ -694,6 +660,8 @@ export default definePostInstallLogicFunction({
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
@@ -706,10 +674,168 @@ yarn twenty exec --postInstall
Pontos-chave:
* As funções de pós-instalação usam `definePostInstallLogicFunction()` — uma variante especializada que omite as configurações de gatilho (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* O manipulador recebe um `InstallLogicFunctionPayload` com `{ previousVersion: string }` — a versão do app que foi instalada anteriormente (ou uma string vazia para instalações novas).
* O manipulador recebe um `InstallPayload` com `{ previousVersion?: string; newVersion: string }` — `newVersion` é a versão que está sendo instalada, e `previousVersion` é a versão que foi instalada anteriormente (ou `undefined` em uma instalação nova). Use esses valores para distinguir instalações novas de atualizações e para executar lógica de migração específica da versão.
* **Quando o hook é executado**: apenas em instalações novas, por padrão. Passe `shouldRunOnVersionUpgrade: true` se você também quiser que ele seja executado quando o app for atualizado a partir de uma versão anterior. Quando omitida, a flag tem valor padrão `false` e as atualizações ignoram o hook.
* **Modelo de execução — assíncrono por padrão, síncrono opcional**: a flag `shouldRunSynchronously` controla *como* a pós-instalação é executada.
* `shouldRunSynchronously: false` *(padrão)* — o hook é **enfileirado na fila de mensagens** com `retryLimit: 3` e é executado de forma assíncrona em um worker. A resposta da instalação retorna assim que o job é enfileirado, então um manipulador lento ou com falha não bloqueia quem chamou. O worker tentará novamente até três vezes. **Use isto para jobs de longa duração** — popular grandes conjuntos de dados, chamar APIs de terceiros lentas, provisionar recursos externos, qualquer coisa que possa exceder uma janela razoável de resposta HTTP.
* `shouldRunSynchronously: true` — o hook é executado **inline durante o fluxo de instalação** (mesmo executor da pré-instalação). A requisição de instalação bloqueia até o manipulador terminar e, se ele lançar uma exceção, quem chamou a instalação recebe um `POST_INSTALL_ERROR`. Sem novas tentativas automáticas. **Use isto para trabalhos rápidos que precisam ser concluídos antes da resposta** — por exemplo, emitir um erro de validação para o usuário ou fazer uma configuração rápida da qual o cliente dependerá imediatamente após a chamada de instalação retornar. Tenha em mente que a migração de metadados já foi aplicada quando a pós-instalação é executada, então uma falha no modo síncrono **não** reverte as alterações de esquema — ela apenas expõe o erro.
* Garanta que seu manipulador seja idempotente. No modo assíncrono, a fila pode tentar novamente até três vezes; em qualquer modo, o hook pode ser executado novamente em atualizações quando `shouldRunOnVersionUpgrade: true`.
* As variáveis de ambiente `APPLICATION_ID`, `APP_ACCESS_TOKEN` e `API_URL` estão disponíveis dentro do manipulador (assim como em qualquer outra função de lógica), então você pode chamar a API da Twenty com um token de acesso de aplicativo com escopo para o seu app.
* É permitida apenas uma função de pós-instalação por app. A geração do manifesto apresentará erro se mais de uma for detectada.
* O `universalIdentifier` da função é definido automaticamente como `postInstallLogicFunctionUniversalIdentifier` no manifesto do aplicativo durante a geração — você não precisa referenciá-lo em `defineApplication()`.
* O `universalIdentifier`, `shouldRunOnVersionUpgrade` e `shouldRunSynchronously` da função são anexados automaticamente ao manifesto do aplicativo no campo `postInstallLogicFunction` durante o build — você não precisa referenciá-los em `defineApplication()`.
* O tempo limite padrão é definido como 300 segundos (5 minutos) para permitir tarefas de configuração mais longas, como o pré-carregamento de dados.
* **Não executado no modo de desenvolvimento**: quando um app é registrado localmente (via `yarn twenty dev`), o servidor pula completamente o fluxo de instalação e sincroniza arquivos diretamente pelo watcher da CLI — portanto, a pós-instalação nunca é executada no modo de desenvolvimento, independentemente de `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` para acioná-lo manualmente em um workspace em execução.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Defina uma função de lógica de pré-instalação (uma por aplicativo)">
Uma função de pré-instalação é uma função de lógica que é executada automaticamente durante a instalação, **antes que a migração de metadados do workspace seja aplicada**. Ela compartilha o mesmo formato de payload que a pós-instalação (`InstallPayload`), mas está posicionada mais cedo no fluxo de instalação para poder preparar o estado do qual a próxima migração depende — usos típicos incluem fazer backup de dados, validar a compatibilidade com o novo esquema ou arquivar registros que estão prestes a ser reestruturados ou removidos.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
Você também pode executar manualmente a função de pré-instalação a qualquer momento usando a CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Pontos-chave:
* Funções de pré-instalação usam `definePreInstallLogicFunction()` — a mesma configuração especializada da pós-instalação, apenas anexada a um ponto diferente do ciclo de vida.
* Os manipuladores de pré e pós-instalação recebem o mesmo tipo `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importe-o uma vez e reutilize-o para ambos os hooks.
* **Quando o hook é executado**: posicionado imediatamente antes da migração de metadados do workspace (`synchronizeFromManifest`). Antes de executar, o servidor realiza uma "sincronização simplificada" puramente aditiva que registra a função de pré-instalação da **nova** versão nos metadados do workspace — nada mais é alterado — e então a executa. Como essa sincronização é apenas aditiva, os objetos, campos e dados da versão anterior ainda estão intactos quando seu manipulador é executado: você pode ler e fazer backup com segurança do estado pré-migração.
* **Modelo de execução**: a pré-instalação é executada **de forma síncrona** e **bloqueia a instalação**. Se o manipulador lançar uma exceção, a instalação é abortada antes que quaisquer alterações de esquema sejam aplicadas — o workspace permanece na versão anterior em um estado consistente. Isto é intencional: a pré-instalação é sua última chance de recusar uma atualização arriscada.
* Assim como na pós-instalação, é permitida apenas uma função de pré-instalação por app. Ela é anexada ao manifesto do aplicativo sob `preInstallLogicFunction` automaticamente durante o build.
* **Não é executada no modo de desenvolvimento**: igual à pós-instalação — o fluxo de instalação é totalmente ignorado para apps registrados localmente, portanto a pré-instalação nunca é executada com `yarn twenty dev`. Use `yarn twenty exec --preInstall` para acioná-lo manualmente.
</Accordion>
<Accordion title="Pré-instalação vs pós-instalação: quando usar cada um" description="Escolhendo o hook de instalação correto">
Ambos os hooks fazem parte do mesmo fluxo de instalação e recebem o mesmo `InstallPayload`. A diferença é **quando** eles são executados em relação à migração de metadados do workspace, e isso muda quais dados eles podem manipular com segurança.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
A pré-instalação é sempre **síncrona** (ela bloqueia a instalação e pode abortá-la). A pós-instalação é **assíncrona por padrão** — enfileirada em um worker com novas tentativas automáticas — mas pode optar por execução síncrona com `shouldRunSynchronously: true`. Veja o acordeão `definePostInstallLogicFunction` acima para saber quando usar cada modo.
**Use `post-install` para qualquer coisa que precise que o novo esquema exista.** Este é o caso mais comum:
* Popular dados padrão (criando registros iniciais, visualizações padrão, conteúdo de demonstração) em objetos e campos recém-adicionados.
* Registrar webhooks com serviços de terceiros agora que o app tem suas credenciais.
* Chamar sua própria API para finalizar a configuração que depende dos metadados sincronizados.
* Lógica idempotente de "garantir que isso exista" que deve reconciliar o estado em cada atualização — combine com `shouldRunOnVersionUpgrade: true`.
Exemplo — popular um registro `PostCard` padrão após a instalação:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**Use `pre-install` quando uma migração, de outra forma, destruiria ou corromperia dados existentes.** Como a pré-instalação roda contra o esquema *anterior* e sua falha reverte a atualização, é o lugar certo para qualquer coisa arriscada:
* **Fazer backup de dados que estão prestes a ser removidos ou reestruturados** — por exemplo, você está removendo um campo na v2 e precisa copiar seus valores para outro campo ou exportá-los para um armazenamento antes que a migração seja executada.
* **Arquivar registros que uma nova restrição invalidaria** — por exemplo, um campo está se tornando `NOT NULL` e você precisa excluir ou corrigir linhas com valores nulos primeiro.
* **Validar a compatibilidade e recusar a atualização se os dados atuais não puderem ser migrados de forma limpa** — lance uma exceção no manipulador e a instalação é abortada sem alterações aplicadas. Isto é mais seguro do que descobrir a incompatibilidade no meio da migração.
* **Renomear ou reatribuir chaves de dados** antes de uma alteração de esquema que perderia a associação.
Exemplo — arquivar registros antes de uma migração destrutiva:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**Regra geral:**
| Você quer… | Usar |
| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Popular dados padrão, configurar o workspace, registrar recursos externos | `post-install` |
| Executar processos longos de popular dados ou chamadas a terceiros que não devem bloquear a resposta da instalação | `post-install` (padrão — `shouldRunSynchronously: false`, com novas tentativas do worker) |
| Executar uma configuração rápida da qual o chamador dependerá imediatamente após o retorno da chamada de instalação | `post-install` com `shouldRunSynchronously: true` |
| Ler ou fazer backup de dados que a próxima migração perderia | `pre-install` |
| Rejeitar uma atualização que corromperia dados existentes | `pre-install` (lançar uma exceção no manipulador) |
| Executar reconciliação em cada atualização | `post-install` com `shouldRunOnVersionUpgrade: true` |
| Fazer uma configuração única apenas na primeira instalação | `post-install` com `shouldRunOnVersionUpgrade: false` (padrão) |
<Note>
Em caso de dúvida, use **post-install** como padrão. Recurra à pré-instalação somente quando a própria migração for destrutiva e você precisar interceptar o estado anterior antes que ele desapareça.
</Note>
</Accordion>
<Accordion title="defineFrontComponent" description="Definir componentes de front-end para UI personalizada">
@@ -755,7 +881,7 @@ export default defineFrontComponent({
});
```
Após sincronizar com `yarn twenty dev`, a ação rápida aparece no canto superior direito da página:
Após sincronizar com `yarn twenty dev` (ou executando uma única vez o `yarn twenty dev --once`), a ação rápida aparece no canto superior direito da página:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Botão de ação rápida no canto superior direito" />
@@ -1401,7 +1527,7 @@ export default defineFrontComponent({
### Como o empacotamento funciona
A etapa de build (`yarn twenty dev` ou `yarn twenty build`) usa o esbuild para produzir um único arquivo independente por função lógica e por componente de front-end. Todos os pacotes importados são incorporados ao bundle.
A etapa de build usa o esbuild para produzir um único arquivo independente por função lógica e por componente de front-end. Todos os pacotes importados são incorporados ao bundle.
**Funções lógicas** são executadas em um ambiente Node.js. Módulos nativos do Node (`fs`, `path`, `crypto`, `http`, etc.) estão disponíveis e não precisam ser instalados.
@@ -1816,8 +1942,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute pre-install or post-install functions
yarn twenty exec --preInstall
# Execute the post-install function
yarn twenty exec --postInstall
```
@@ -98,6 +98,21 @@ O modo de desenvolvimento só está disponível em instâncias do Twenty em modo
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Saída do terminal no modo de desenvolvimento" />
</div>
#### Sincronização única com `yarn twenty dev --once`
Se você não quiser um monitor em execução em segundo plano (por exemplo, em um pipeline de CI, um hook do git ou um fluxo de trabalho com script), passe a flag `--once`. Ele executa o mesmo pipeline que `yarn twenty dev` — gerar o manifesto, empacotar arquivos, fazer upload, sincronizar, regenerar o cliente de API tipado — mas **encerra assim que a sincronização for concluída**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Comando | Comportamento | Quando usar |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Monitora seus arquivos-fonte e ressincroniza a cada alteração. Continua em execução até você interrompê-lo. | Desenvolvimento local interativo — você quer o painel de status em tempo real e um ciclo de feedback instantâneo. |
| `yarn twenty dev --once` | Executa uma única compilação + sincronização e, em seguida, encerra com o código `0` em caso de sucesso ou `1` em caso de falha. | Scripts, CI, hooks de pre-commit, agentes de IA e qualquer fluxo de trabalho não interativo. |
Ambos os modos exigem um servidor Twenty em execução no modo de desenvolvimento e um remoto autenticado — aplicam-se os mesmos pré-requisitos.
### Veja seu aplicativo no Twenty
Abra [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) no seu navegador. Navegue até **Settings > Apps** e selecione a aba **Developer**. Você deverá ver seu aplicativo listado em **Your Apps**:
@@ -66,12 +66,18 @@ O link de compartilhamento usa a URL base do servidor (sem qualquer subdomínio
### Gerenciamento de versões
Ao atualizar um aplicativo empacotado como tarball já implantado, o servidor exige que o `version` no `package.json` seja **estritamente maior** (de acordo com a ordenação do [semver](https://semver.org)) do que a versão atualmente implantada. Reimplantar a mesma versão, ou enviar uma inferior, é rejeitado antes que o tarball seja armazenado — você verá um erro `VERSION_ALREADY_EXISTS` na CLI.
Para lançar uma atualização:
1. Atualize o campo `version` no seu `package.json`
1. Atualize o campo `version` no seu `package.json` (por exemplo, `1.2.3` → `1.2.4`, `1.3.0` ou `2.0.0`)
2. Execute `yarn twenty deploy` (ou `yarn twenty deploy --remote production`)
3. Os espaços de trabalho que têm o aplicativo instalado verão a atualização disponível em suas configurações
<Note>
Tags de pré-lançamento funcionam como esperado: incrementar `1.0.0-rc.1` → `1.0.0-rc.2` é permitido, e uma versão final como `1.0.0` é corretamente reconhecida como superior a `1.0.0-rc.5`. A versão em `package.json` deve ser, ela própria, uma string semver válida.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
## Publicação no npm
@@ -81,7 +87,7 @@ Publicar no npm torna seu aplicativo descobrível no Marketplace da Twenty. Qual
### Requisitos
* Uma conta no [npm](https://www.npmjs.com)
* A palavra-chave `twenty-app` no array `keywords` do seu `package.json` (já incluída quando você cria o projeto com `create-twenty-app`)
* A palavra-chave `twenty-app` no array `keywords` do seu `package.json` (adicione-a manualmente — não é incluída por padrão no template `create-twenty-app`)
```json filename="package.json"
{
@@ -189,3 +195,12 @@ Você também pode instalar apps pela linha de comando:
```bash filename="Terminal"
yarn twenty install
```
<Note>
O servidor impõe o versionamento semver na instalação, espelhando as regras da implantação:
* Instalar a mesma versão que já está instalada no seu espaço de trabalho é rejeitado com um erro `APP_ALREADY_INSTALLED`.
* Instalar uma versão inferior à atualmente instalada é rejeitado com um erro `CANNOT_DOWNGRADE_APPLICATION`.
Para instalar uma versão mais recente, implante ou publique-a primeiro e, em seguida, execute novamente `yarn twenty install`.
</Note>
@@ -140,12 +140,12 @@ Os dados não correspondem ao formato esperado para esse tipo de campo.
#### Data
**Problema:** Formato de data não reconhecido
**Solução:** Use um formato consistente em todo o ficheiro
**Solução:** Use o formato `YYYY-MM-DD` de forma consistente em todo o ficheiro
```
✓ 2024-03-15 (YYYY-MM-DD - recomendado)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
✓ 2024-03-15 (YYYY-MM-DD)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
```
#### Telefone
@@ -103,8 +103,6 @@ Endereço é um **campo aninhado** com várias colunas (algumas podem ficar em b
Use formatação consistente em todo o seu arquivo:
* `YYYY-MM-DD` (recomendado): `2024-03-15`
* `MM/DD/YYYY`: `03/15/2024`
* `DD/MM/YYYY`: `15/03/2024`
* ISO 8601: `2024-03-15T10:30:00Z`
### Campos Numéricos
@@ -47,7 +47,7 @@ npx nx run twenty-server:database:reset
#### Pentru obiectele din schematizările de Bază/Metadate (TypeORM)
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate
```
## Tehnologii Utilizate
@@ -756,7 +756,7 @@ export default defineFrontComponent({
});
```
După sincronizarea cu `yarn twenty dev`, acțiunea rapidă apare în colțul din dreapta sus al paginii:
După sincronizarea cu `yarn twenty dev` (sau prin rularea o singură dată a comenzii `yarn twenty dev --once`), acțiunea rapidă apare în colțul din dreapta sus al paginii:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Buton de acțiune rapidă în colțul din dreapta sus" />
@@ -1402,7 +1402,7 @@ export default defineFrontComponent({
### Cum funcționează împachetarea
Pasul de build (`yarn twenty dev` sau `yarn twenty build`) folosește esbuild pentru a produce un singur fișier autonom per funcție logică și per componentă frontend. Toate pachetele importate sunt integrate în bundle.
Pasul de build folosește esbuild pentru a produce un singur fișier autonom pentru fiecare funcție logică și pentru fiecare componentă frontend. Toate pachetele importate sunt integrate în bundle.
**Funcțiile logice** rulează într-un mediu Node.js. Modulele built-in Node (`fs`, `path`, `crypto`, `http` etc.) sunt disponibile și nu trebuie instalate.
@@ -98,6 +98,21 @@ Modul de dezvoltare este disponibil doar pe instanțele Twenty care rulează în
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Ieșirea terminalului în modul de dezvoltare" />
</div>
#### One-shot sync with `yarn twenty dev --once`
If you do not want a watcher running in the background (for example in a CI pipeline, a git hook, or a scripted workflow), pass the `--once` flag. It runs the same pipeline as `yarn twenty dev` — build manifest, bundle files, upload, sync, regenerate the typed API client — but **exits as soon as the sync completes**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Comandă | Comportament | When to use |
| ------------------------ | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Watches your source files and re-syncs on every change. Keeps running until you stop it. | Interactive local development — you want the live status panel and instant feedback loop. |
| `yarn twenty dev --once` | Performs a single build + sync, then exits with code `0` on success or `1` on failure. | Scripts, CI, pre-commit hooks, AI agents, and any non-interactive workflow. |
Both modes require a Twenty server running in development mode and an authenticated remote — the same prerequisites apply.
### Vedeți aplicația în Twenty
Deschideți [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) în browser. Navigați la **Settings > Apps** și selectați fila **Developer**. Ar trebui să vedeți aplicația listată la **Your Apps**:
@@ -47,7 +47,7 @@ npx nx run twenty-server:database:reset
#### Для объектов в схемах Core/Metadata (TypeORM)
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate
```
## Технологический стек
@@ -476,7 +476,7 @@ export default defineLogicFunction({
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
@@ -643,49 +643,15 @@ export default defineLogicFunction({
**Напишите хорошее описание в поле `description`.** Агенты ИИ опираются на поле `description` функции, чтобы решить, когда использовать инструмент. Чётко опишите, что делает инструмент и когда его следует вызывать.
</Note>
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Определяет предустановочную логическую функцию (по одной на приложение)">
Предустановочная функция — это логическая функция, которая автоматически выполняется до установки вашего приложения в рабочем пространстве. Это полезно для задач валидации, проверки предварительных условий или подготовки состояния рабочего пространства перед основной установкой.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Вы также можете вручную выполнить предустановочную функцию в любое время с помощью CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Основные моменты:
* Предустановочные функции используют `definePreInstallLogicFunction()` — специализированный вариант, который опускает настройки триггеров (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Обработчик получает `InstallLogicFunctionPayload` с `{ previousVersion: string }` — версией приложения, которая была установлена ранее (или пустой строкой для новых установок).
* Для каждого приложения допускается только одна предустановочная функция. Сборка манифеста завершится ошибкой, если будет обнаружено более одной такой функции.
* Параметр `universalIdentifier` функции автоматически устанавливается как `preInstallLogicFunctionUniversalIdentifier` в манифесте приложения во время сборки — вам не нужно ссылаться на него в `defineApplication()`.
* Тайм-аут по умолчанию установлен на 300 секунд (5 минут), чтобы обеспечить выполнение более длительных задач подготовки.
</Accordion>
<Accordion title="definePostInstallLogicFunction" description="Определяет послеустановочную логическую функцию (по одной на приложение)">
Послеустановочная функция — это функция логики, которая автоматически выполняется после установки вашего приложения в рабочем пространстве. Это полезно для одноразовых задач настройки, таких как инициализация данных по умолчанию, создание начальных записей или настройка параметров рабочего пространства.
Послеустановочная функция — это функция логики, которая автоматически выполняется после завершения установки вашего приложения в рабочем пространстве. Сервер выполняет её **после** того, как метаданные приложения синхронизированы и клиент SDK сгенерирован, так что рабочее пространство полностью готово к использованию, а новая схема уже применена. Типичные сценарии использования включают предзаполнение данных по умолчанию, создание начальных записей, настройку параметров рабочего пространства или выделение ресурсов в сторонних сервисах.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
@@ -694,6 +660,8 @@ export default definePostInstallLogicFunction({
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
@@ -706,10 +674,168 @@ yarn twenty exec --postInstall
Основные моменты:
* Послеустановочные функции используют `definePostInstallLogicFunction()` — специализированный вариант, который опускает настройки триггеров (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Обработчик получает `InstallLogicFunctionPayload` с `{ previousVersion: string }` — версией приложения, которая была установлена ранее (или пустой строкой для новых установок).
* Обработчик получает `InstallPayload` с `{ previousVersion?: string; newVersion: string }` — `newVersion` — это устанавливаемая версия, а `previousVersion` — версия, установленная ранее (или `undefined` при чистой установке). Используйте эти значения, чтобы отличать чистые установки от обновлений и запускать логику миграции, зависящую от версии.
* **Когда запускается хук**: по умолчанию только при чистой установке. Передайте `shouldRunOnVersionUpgrade: true`, если хотите, чтобы он также выполнялся при обновлении приложения с предыдущей версии. Если флаг опущен, по умолчанию он равен `false`, и при обновлении хук пропускается.
* **Модель выполнения — по умолчанию асинхронно, синхронный режим по выбору**: флаг `shouldRunSynchronously` определяет, *как* выполняется post-install.
* `shouldRunSynchronously: false` *(по умолчанию)* — хук **помещается в очередь сообщений** с `retryLimit: 3` и выполняется асинхронно в воркере. Ответ на установку возвращается сразу после постановки задания в очередь, поэтому медленный или дающий сбой обработчик не блокирует вызывающую сторону. Воркер выполнит до трёх повторных попыток. **Используйте это для длительных задач** — наполнение большими наборами данных, вызовы медленных сторонних API, подготовка внешних ресурсов — всего, что может выйти за разумное окно ответа HTTP.
* `shouldRunSynchronously: true` — хук выполняется **непосредственно в процессе установки** (тот же исполнитель, что и для pre-install). Запрос установки блокируется, пока обработчик не завершится, и если он генерирует исключение, вызывающая сторона установки получает `POST_INSTALL_ERROR`. Автоматических повторов нет. **Используйте это для быстрых задач, которые должны завершиться до отправки ответа** — например, выдача ошибки валидации пользователю или быстрая настройка, на которую клиент будет полагаться сразу после возврата вызова установки. Имейте в виду, что к моменту запуска post-install миграция метаданных уже применена, поэтому сбой в синхронном режиме **не** откатывает изменения схемы — он лишь выявляет ошибку.
* Убедитесь, что ваш обработчик идемпотентен. В асинхронном режиме очередь может выполнить до трёх повторных попыток; в любом режиме хук может запускаться снова при обновлениях, когда `shouldRunOnVersionUpgrade: true`.
* Переменные окружения `APPLICATION_ID`, `APP_ACCESS_TOKEN` и `API_URL` доступны внутри обработчика (как и в любой другой логической функции), поэтому вы можете вызывать API Twenty с токеном доступа приложения, ограниченным вашим приложением.
* Для каждого приложения допускается только одна послеустановочная функция. Сборка манифеста завершится ошибкой, если будет обнаружено более одной такой функции.
* Параметр `universalIdentifier` функции автоматически устанавливается как `postInstallLogicFunctionUniversalIdentifier` в манифесте приложения во время сборки — вам не нужно ссылаться на него в `defineApplication()`.
* Параметры функции `universalIdentifier`, `shouldRunOnVersionUpgrade` и `shouldRunSynchronously` автоматически добавляются в манифест приложения в поле `postInstallLogicFunction` во время сборки — вам не нужно указывать их в `defineApplication()`.
* Тайм-аут по умолчанию установлен на 300 секунд (5 минут), чтобы позволить выполнять более длительные задачи настройки, такие как инициализация данных.
* **Не выполняется в режиме разработки**: когда приложение зарегистрировано локально (через `yarn twenty dev`), сервер полностью пропускает процесс установки и синхронизирует файлы напрямую через наблюдатель CLI — поэтому post-install никогда не запускается в режиме разработки, независимо от `shouldRunSynchronously`. Используйте `yarn twenty exec --postInstall`, чтобы запустить это вручную для запущенного рабочего пространства.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Определяет предустановочную логическую функцию (по одной на приложение)">
Функция pre-install — это логическая функция, которая автоматически выполняется во время установки, **до применения миграции метаданных рабочего пространства**. Она использует ту же структуру полезной нагрузки, что и post-install (`InstallPayload`), но находится раньше в процессе установки, чтобы подготовить состояние, от которого зависит предстоящая миграция, — типичные сценарии включают резервное копирование данных, проверку совместимости с новой схемой или архивирование записей, которые будут реструктурированы или удалены.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
Вы также можете вручную выполнить предустановочную функцию в любое время с помощью CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Основные моменты:
* Функции pre-install используют `definePreInstallLogicFunction()` — та же специализированная конфигурация, что и у post-install, только привязанная к другому этапу жизненного цикла.
* И обработчики pre-, и post-install получают один и тот же тип `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Импортируйте его один раз и используйте повторно в обоих хуках.
* **Когда запускается хук**: выполняется непосредственно перед миграцией метаданных рабочего пространства (`synchronizeFromManifest`). Перед выполнением сервер запускает чисто добавочную «урезанную синхронизацию», которая регистрирует в метаданных рабочего пространства pre-install функцию **новой** версии — ничего больше не затрагивается — а затем выполняет её. Поскольку эта синхронизация только добавляет, объекты, поля и данные предыдущей версии остаются нетронутыми к моменту запуска вашего обработчика: вы можете безопасно читать и сохранять состояние до миграции.
* **Модель выполнения**: pre-install выполняется **синхронно** и **блокирует установку**. Если обработчик генерирует исключение, установка прерывается до применения каких-либо изменений схемы — рабочее пространство остаётся на предыдущей версии в согласованном состоянии. Это сделано намеренно: pre-install — ваш последний шанс отказать в рискованном обновлении.
* Как и в случае с post-install, для каждого приложения допускается только одна предустановочная функция. Она автоматически добавляется в манифест приложения в поле `preInstallLogicFunction` во время сборки.
* **Не выполняется в режиме разработки**: как и post-install, процесс установки полностью пропускается для локально зарегистрированных приложений, поэтому pre-install никогда не запускается при `yarn twenty dev`. Используйте `yarn twenty exec --preInstall`, чтобы запустить это вручную.
</Accordion>
<Accordion title="Pre-install и post-install: когда что использовать" description="Выбор подходящего хука установки">
Оба хука являются частью одного и того же процесса установки и получают один и тот же `InstallPayload`. Разница в том, **когда** они запускаются относительно миграции метаданных рабочего пространства, и это определяет, к каким данным можно безопасно обращаться.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
Pre-install всегда **синхронный** (он блокирует установку и может её прервать). Post-install **по умолчанию асинхронный** — ставится в очередь воркера с автоматическими повторами — но может перейти к синхронному выполнению с `shouldRunSynchronously: true`. См. аккордеон `definePostInstallLogicFunction` выше о том, когда использовать каждый режим.
**Используйте `post-install` для всего, что требует наличия новой схемы.** Это распространённый случай:
* Наполнение данными по умолчанию (создание начальных записей, стандартных представлений, демонстрационного контента) для недавно добавленных объектов и полей.
* Регистрация вебхуков в сторонних сервисах теперь, когда у приложения уже есть учётные данные.
* Вызов вашего собственного API для завершения настройки, зависящей от синхронизированных метаданных.
* Идемпотентная логика «убедиться, что это существует», которая должна приводить состояние в соответствие при каждом обновлении — совместите с `shouldRunOnVersionUpgrade: true`.
Пример — создать запись `PostCard` по умолчанию после установки:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**Используйте `pre-install`, когда миграция в противном случае уничтожит или повредит существующие данные.** Поскольку pre-install работает с *предыдущей* схемой и при сбое откатывает обновление, это правильное место для всего рискованного:
* **Резервное копирование данных, которые будут удалены или реструктурированы** — например, вы удаляете поле в v2 и вам нужно скопировать его значения в другое поле или экспортировать их в хранилище до запуска миграции.
* **Архивирование записей, которые новое ограничение сделает недопустимыми** — например, поле становится `NOT NULL`, и вам сначала нужно удалить или исправить строки со значениями null.
* **Проверка совместимости и отказ от обновления, если текущие данные нельзя корректно мигрировать** — выбросьте исключение из обработчика, и установка прервётся без внесения изменений. Это безопаснее, чем обнаружить несовместимость в середине миграции.
* **Переименование или изменение ключей данных** перед изменением схемы, которое привело бы к потере связи.
Пример — архивировать записи перед разрушительной миграцией:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**Общее правило:**
| Вы хотите… | Использовать |
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Наполнить данными по умолчанию, настроить рабочее пространство, зарегистрировать внешние ресурсы | `post-install` |
| Выполнить длительное наполнение или сторонние вызовы, которые не должны блокировать ответ установки | `post-install` (по умолчанию — `shouldRunSynchronously: false`, с повторами воркера) |
| Выполнить быструю настройку, на которую вызывающая сторона будет полагаться сразу после возврата вызова установки | `post-install` с `shouldRunSynchronously: true` |
| Прочитать или сохранить данные, которые предстоящая миграция может потерять | `pre-install` |
| Отклонить обновление, которое повредит существующие данные | `pre-install` (бросьте исключение из обработчика) |
| Выполнять согласование при каждом обновлении | `post-install` с `shouldRunOnVersionUpgrade: true` |
| Сделать одноразовую настройку только при первой установке | `post-install` с `shouldRunOnVersionUpgrade: false` (по умолчанию) |
<Note>
Если сомневаетесь, выбирайте по умолчанию **post-install**. Обращайтесь к pre-install только тогда, когда сама миграция разрушительна и вам нужно перехватить предыдущее состояние, прежде чем оно исчезнет.
</Note>
</Accordion>
<Accordion title="defineFrontComponent" description="Определение фронт-компонентов для настраиваемого интерфейса">
@@ -755,7 +881,7 @@ export default defineFrontComponent({
});
```
После синхронизации с помощью `yarn twenty dev` быстрое действие появится в правом верхнем углу страницы:
После синхронизации с помощью `yarn twenty dev` (или однократного запуска `yarn twenty dev --once`) быстрое действие появится в правом верхнем углу страницы:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Кнопка быстрого действия в правом верхнем углу" />
@@ -1401,7 +1527,7 @@ export default defineFrontComponent({
### Как работает бандлинг
Этап сборки (`yarn twenty dev` или `yarn twenty build`) использует esbuild для создания одного самодостаточного файла на каждую логическую функцию и на каждый компонент фронтенда. Все импортированные пакеты встроены в бандл.
Этап сборки использует esbuild для создания одного самодостаточного файла на каждую логическую функцию и на каждый компонент фронтенда. Все импортированные пакеты встроены в бандл.
**Логические функции** выполняются в среде Node.js. Встроенные модули Node (`fs`, `path`, `crypto`, `http` и т. д.) доступны и не требуют установки.
@@ -1816,8 +1942,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute pre-install or post-install functions
yarn twenty exec --preInstall
# Execute the post-install function
yarn twenty exec --postInstall
```
@@ -98,6 +98,21 @@ yarn twenty dev --verbose
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Вывод терминала в режиме разработки" />
</div>
#### Разовая синхронизация с `yarn twenty dev --once`
Если вы не хотите, чтобы в фоновом режиме работал наблюдатель (например, в конвейере CI, хуке Git или скриптовом рабочем процессе), передайте флаг `--once`. Он запускает тот же конвейер, что и `yarn twenty dev` — собирает манифест, упаковывает файлы, загружает, синхронизирует, повторно генерирует типизированный клиент API — но **выходит сразу после завершения синхронизации**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Команда | Поведение | Когда использовать |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Отслеживает ваши исходные файлы и повторно синхронизирует при каждом изменении. Продолжает работать, пока вы его не остановите. | Интерактивная локальная разработка — вам нужна панель статуса в реальном времени и мгновенная обратная связь. |
| `yarn twenty dev --once` | Выполняет одну сборку и синхронизацию, затем завершает работу с кодом `0` при успехе или `1` при ошибке. | Скрипты, CI, хуки pre-commit, AI-агенты и любые неинтерактивные рабочие процессы. |
Оба режима требуют сервер Twenty, запущенный в режиме разработки, и аутентифицированный удалённый ресурс — действуют те же предварительные требования.
### Посмотрите своё приложение в Twenty
Откройте [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) в браузере. Перейдите в **Settings > Apps** и выберите вкладку **Developer**. Вы должны увидеть своё приложение в разделе **Your Apps**:
@@ -66,12 +66,18 @@ yarn twenty deploy
### Управление версиями
When updating an already deployed tarball app, the server requires the `version` in `package.json` to be **strictly higher** (per [semver](https://semver.org) ordering) than the currently deployed version. Повторное развёртывание той же версии или публикация более низкой версии отклоняются до сохранения tarball — в CLI вы увидите ошибку `VERSION_ALREADY_EXISTS`.
Чтобы выпустить обновление:
1. Обновите значение поля `version` в файле `package.json`
1. Увеличьте значение поля `version` в вашем `package.json` (например: `1.2.3` → `1.2.4`, `1.3.0` или `2.0.0`).
2. Выполните `yarn twenty deploy` (или `yarn twenty deploy --remote production`)
3. Рабочие пространства, в которых установлено приложение, увидят доступное обновление в своих настройках
<Note>
Пререлизные теги работают как ожидается: повышение версии `1.0.0-rc.1` → `1.0.0-rc.2` допускается, а финальный релиз вроде `1.0.0` корректно распознаётся как более высокий, чем `1.0.0-rc.5`. Версия в `package.json` должна сама по себе быть корректной строкой semver.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
## Публикация в npm
@@ -81,7 +87,7 @@ yarn twenty deploy
### Требования
* Учётная запись [npm](https://www.npmjs.com)
* Ключевое слово `twenty-app` в массиве `keywords` вашего `package.json` (уже добавлено при создании проекта с помощью `create-twenty-app`)
* Ключевое слово `twenty-app` в массиве `keywords` вашего `package.json` (добавьте его вручную — по умолчанию оно не включено в шаблон `create-twenty-app`)
```json filename="package.json"
{
@@ -189,3 +195,12 @@ jobs:
```bash filename="Terminal"
yarn twenty install
```
<Note>
Сервер при установке применяет версионирование semver, аналогичное правилам при развёртывании:
* Установка той же версии, которая уже установлена в вашем рабочем пространстве, отклоняется с ошибкой `APP_ALREADY_INSTALLED`.
* Установка версии ниже текущей отклоняется с ошибкой `CANNOT_DOWNGRADE_APPLICATION`.
Чтобы установить более новую версию, сначала разверните или опубликуйте её, затем снова выполните `yarn twenty install`.
</Note>
@@ -140,12 +140,12 @@ description: Полное руководство по устранению ош
#### Дата
**Проблема:** Нераспознанный формат даты
**Решение:** Используйте единый формат по всему файлу
**Решение:** Используйте формат `YYYY-MM-DD` последовательно по всему файлу
```
✓ 2024-03-15 (YYYY-MM-DD - recommended)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
✓ 2024-03-15 (YYYY-MM-DD)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
```
#### Телефон
@@ -103,8 +103,6 @@ Twenty требует уникальности для некоторых пол
Используйте единый формат по всему файлу:
* `YYYY-MM-DD` (рекомендуется): `2024-03-15`
* `MM/DD/YYYY`: `03/15/2024`
* `DD/MM/YYYY`: `15/03/2024`
* ISO 8601: `2024-03-15T10:30:00Z`
### Числовые поля
@@ -47,7 +47,7 @@ npx nx run twenty-server:database:reset
#### Core/Metadata şemalarında (TypeORM) nesneler için
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:database:migrate:generate
```
## Teknoloji Yığını

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