Compare commits

...
Author SHA1 Message Date
Sonarly Claude CodeandClaude Opus 4.6 bc45b0e7e8 Fix missing throttleRetryAfter field metadata crash during message import
For workspaces that haven't run the v1.18 migration yet, the
throttleRetryAfter field doesn't exist in the workspace metadata.
Update calls that include this field cause formatData to throw,
crashing the messaging import job and breaking error recovery.

Wrap each update call that includes throttleRetryAfter in a try-catch
that retries without the field when the field metadata is missing,
allowing message import to work for both migrated and unmigrated
workspaces.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 00:38:30 +00:00
Charles BochetandGitHub 36c2b0e23b Migrate dropdown to jotai (#18063)
Here we go again
2026-02-19 00:58:26 +01:00
66deb8be63 i18n - docs translations (#18062)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 23:34:46 +01:00
Charles BochetandGitHub 01d2269bd0 Fix website build (#18061)
As per title
2026-02-18 23:34:36 +01:00
04502e5abb Messages Message Folder Association (#17398)
This PR adds Message folder association for message channel messages,
Currently under testing phase, not ready yet.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:13:47 +01:00
Charles BochetandGitHub 549c7a613b Fix website build (#18057) 2026-02-18 22:40:54 +01:00
743b733e70 i18n - translations (#18056)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 22:40:10 +01:00
Thomas des FrancsGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Charles BochetCharles Bochet
612f7c37a5 Improve security settings card grouping and description overflow (#17928)
# After

- Added a separtor between the two audit logs cards
- Rename the audit log card to avoid repetition
- Grouped "Invite by link" and "2 factor auth" in one group
- Changed the card component description to always be one line max with
truncation & tooltips

<img width="777" height="1278" alt="CleanShot 2026-02-13 at 17 02 36"
src="https://github.com/user-attachments/assets/685c792a-c85b-4521-8c1b-bd9adedc75d9"
/>

<img width="976" height="690"
alt="b49f2eb043b6712d013618bb0a4ef7f011cf2316e1163fbdee4c293bed036ac9"
src="https://github.com/user-attachments/assets/6e17aa11-ecdb-4f98-ba50-5cd9b9c5def6"
/>

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-18 22:33:37 +01:00
42108e0611 i18n - translations (#18053)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 22:20:32 +01:00
Charles BochetandGitHub 9a2dc45eb7 Fix website build (#18052) 2026-02-18 22:15:30 +01:00
neo773andGitHub 7de565f70c Prevent SSRF via IMAP/SMTP/CalDAV (#17973)
Prevents leaking of internal services by filtering out private IPs, same
way we do for webhooks
2026-02-18 22:15:10 +01:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
67074a7581 Harden server-side input validation and auth defaults (#18018)
## Summary

- **File storage (LocalDriver):** Add realpath resolution and symlink
rejection to `writeFile`, `downloadFile`, and `downloadFolder` — brings
them in line with the existing `readFile` protections. Includes unit
tests.
- **JWT:** Pin signing/verification to HS256 explicitly.
- **Auth:** Revoke active refresh tokens when a user changes their
password.
- **Logic functions:** Validate `handlerName` as a safe JS identifier at
both DTO and runtime level, preventing injection into the generated
runner script.
- **User entity:** Remove `passwordHash` from the GraphQL schema
(`@Field` decorator removed, column stays).
- **Query params:** Use `crypto.randomBytes` instead of `Math.random`
for SQL parameter name generation.
- **Exception filter:** Mirror the request `Origin` header instead of
sending `Access-Control-Allow-Origin: *`.

## Test plan

- [x] `local.driver.spec.ts` — writeFile rejects symlinks, downloadFile
rejects paths outside storage
- [ ] Verify JWT auth flow still works (login, token refresh)
- [ ] Verify password change invalidates existing sessions
- [ ] Verify logic function creation with valid/invalid handler names
- [ ] Verify file upload/download in dev environment


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-18 21:41:01 +01:00
Charles BochetandGitHub 330737aa2e Fix impossible scroll in sdk app:dev (#18051)
Issue is that we are refreshing the terminal because of icons animations
2026-02-18 21:39:21 +01:00
4cb64c6aa5 Restore old favorite design (#18049)
Issue : With IS_NAVIGATION_MENU_ITEM_ENABLED:true +
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED:false, nav menu design is
changed after 1.18.0 release : views expansion removed, system object
displayed, position re-ordered

We prefer keeping the same "old" favorite behaviour and design state

- After 1.18.0 all workspaces have up-to-date navigation menu items
(migrated)
- IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED becomes the FF for nav menu
new design

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-18 21:35:17 +01:00
6b3ef404b0 i18n - docs translations (#18050)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 21:31:54 +01:00
6aaafb76b6 fix: show "Not shared" for junction relation fields when target or intermediate object is not readable (#18025)
When a user's role lacks read permission on the target object (e.g.,
Company) or the intermediate junction object (e.g., EmploymentHistory),
junction relation fields like "Previous Companies" displayed as blank
instead of showing "Not shared."

- In RecordFieldList, junction fields now check the junction object's
read permission and set isForbidden on the field context so FieldDisplay
renders "Not shared" instead of an empty field.
- In RelationFromManyFieldDisplay, if junction records exist but all
nested target records are null (permission-denied by the API), the
component renders "Not shared" instead of an empty list.

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2026-02-18 21:05:45 +01:00
Thomas des FrancsandGitHub 7ec4508d5f Add currency hover tooltip in CurrencyDisplay (#18045)
https://github.com/user-attachments/assets/af768ecb-b5e9-4e8d-a9f9-fee1a08ea9a0

Fixes https://github.com/twentyhq/twenty/issues/17756
2026-02-18 20:25:34 +01:00
Thomas des FrancsandGitHub a05a9c8f79 refactor workflow action messaging with callout (#18038)
## Summary
- remove the dedicated `WorkflowMessage` component and story
- update workflow action editor components to use the shared callout
patterns
- adjust `Callout` and its stories to support the new usage in workflow
actions

## Before/After

<img width="525" height="548" alt="image"
src="https://github.com/user-attachments/assets/ce57a84f-f070-4149-85ef-a4d162b2d878"
/>


<img width="518" height="593" alt="image"
src="https://github.com/user-attachments/assets/f7249cd0-221f-496d-9deb-d9966ee43382"
/>
2026-02-18 20:23:38 +01:00
Charles BochetandGitHub ce1ffa8550 Refactor page layout types (#18042)
## Refactor page layout widget types into shared package and expose from
SDK

### Why

Widget configuration types were defined only on the server, forcing SDK
consumer apps to import from deep internal `twenty-shared/dist` paths —
fragile and breaks on structural changes. Server DTOs also had no
compile-time guarantee they matched the canonical types.

### What changed

- **`twenty-shared`**: Migrated `ChartFilter`, `GridPosition`,
`RatioAggregateConfig` and all 20 widget configuration variants into
`twenty-shared/types`. `PageLayoutWidgetConfiguration` (base, with
`SerializedRelation`) and `PageLayoutWidgetUniversalConfiguration`
(derived via `FormatRecordSerializedRelationProperties`) are now the
single source of truth.
- **`twenty-sdk`**: Re-exported `AggregateOperations`,
`ObjectRecordGroupByDateGranularity`, `PageLayoutTabLayoutMode`, and
`PageLayoutWidgetUniversalConfiguration` so consumer apps import from
`twenty-sdk` directly.
- **`twenty-server`**: All widget DTOs now `implements` their shared
type for compile-time enforcement. Added helpers to convert nested
`fieldMetadataId` ↔ `fieldMetadataUniversalIdentifier` inside chart
filters. Removed redundant local type re-exports.
2026-02-18 20:17:40 +01:00
martmullandGitHub 2cc3c75c7e Add example in create-twenty-app (#18043)
- add interactive mode to create-twenty-app
- by default create an example for each entities

<img width="1181" height="168" alt="image"
src="https://github.com/user-attachments/assets/a2490d8f-66a1-4cd5-bf41-57166cc20a1e"
/>
2026-02-18 18:04:19 +00:00
Thomas TrompetteandGitHub 9733ff1b8e Add missing objects to rich app integration tests (#18039)
Updating rich app so it also create:
- a many to many relation
- views
- navigation items

The app was built successfully.

Will still be missing front component examples

<img width="1498" height="660" alt="Capture d’écran 2026-02-18 à 17 47
25"
src="https://github.com/user-attachments/assets/acd5193f-3a36-4eb7-8276-3154e4e60f5e"
/>
2026-02-18 18:01:14 +00:00
e4075caa65 i18n - docs translations (#18048)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 19:42:34 +01:00
Charles BochetandGitHub c3781e87cc Sync page Layout (#18034)
## Sync page layouts, tabs, and widgets

Adds the ability for SDK applications to synchronize `pageLayout`,
`pageLayoutTab`, and `pageLayoutWidget` entities, following the same
pattern established in #18003 for views and navigation menu items.

### Changes

**`twenty-shared`**
- New `PageLayoutManifest`, `PageLayoutTabManifest`, and
`PageLayoutWidgetManifest` types with a hierarchical structure (page
layout → tabs → widgets)
- Added `pageLayouts: PageLayoutManifest[]` to the `Manifest` type

**`twenty-sdk`**
- New `definePageLayout()` SDK function with validation for
universalIdentifier, name, and nested tabs/widgets
- Wired into the manifest extraction and build pipeline
(`DefinePageLayout` target function, `PageLayouts` entity key)
- Exported from the SDK entry point

**`twenty-server`**
- Added `pageLayout`, `pageLayoutTab`, `pageLayoutWidget` to
`APPLICATION_MANIFEST_METADATA_NAMES`
- New conversion utilities: manifest → universal flat entity for all
three entity types
- Updated `computeApplicationManifestAllUniversalFlatEntity
2026-02-18 17:55:04 +01:00
e9b5cb830c i18n - docs translations (#18040)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 17:54:40 +01:00
EtienneandGitHub e40c758aa6 Nav Menu Item Migration command fix (#18041) 2026-02-18 17:45:21 +01:00
martmullandGitHub 53c314d0fa 2094 extensibility define postinstall orand preinstall function to run in application (#18037)
- add a new optional key `postInstallLogicFunctionUniversalIdentifier`
in applicationConfig
- seed postInstall function in create-twenty-app
- update execute:function options
- update doc
2026-02-18 15:38:22 +00:00
618df704e6 [Chore] : Generate migration for DATE_TIME to DATE for DATE_TIME + IS operand filters (#17564)
migration command in response to the fix :
https://github.com/twentyhq/twenty/pull/17529 for the issue
https://github.com/twentyhq/core-team-issues/issues/2027

---------

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-02-18 15:15:38 +00:00
EtienneandGitHub 058489b5cc Fixes - Workspace logo migration (#18035)
- Update migration command to handle case where workspace logo is
originated from workspace email and point to twenty-icons.com
- Update same logic for new workspaces
- Add feature-flag for all newly created workspaces
2026-02-18 16:35:48 +01:00
3bd431e95d Use proper PostgreSQL identifier/literal escaping in workspace DDL (#18024)
## Summary

- Replace the character-stripping approach (`removeSqlDDLInjection`)
with standard PostgreSQL `escapeIdentifier` and `escapeLiteral`
functions across all workspace schema manager services
- Add missing identifier escaping to `createForeignKey` (was the only
method in the FK manager without it)
- Add allowlist validation for index WHERE clauses and FK action types
- Harden tsvector expression builder with proper identifier quoting

## Context

The workspace schema managers build DDL dynamically from metadata (table
names, column names, enum values, etc.). The previous approach stripped
all non-alphanumeric characters — safe but lossy (silently corrupts
values with legitimate special characters). The new approach uses
PostgreSQL's standard escaping:

- **Identifiers**: double internal `"` and wrap → `"my""table"` (same
algorithm as `pg` driver's `escapeIdentifier`)
- **Literals**: double internal `'` and wrap → `'it''s a value'` (same
algorithm as `pg` driver's `escapeLiteral`)

`removeSqlDDLInjection` is kept only for name generation (e.g.,
`computePostgresEnumName`) where stripping to `[a-zA-Z0-9_]` is the
correct behavior.

## Files changed

| File | What |
|------|------|
| `remove-sql-injection.util.ts` | Added `escapeIdentifier` +
`escapeLiteral` |
| `validate-index-where-clause.util.ts` | New — allowlist for partial
index WHERE clauses |
| 5 schema manager services | Replaced strip+manual-quote with
`escapeIdentifier`/`escapeLiteral` |
| `build-sql-column-definition.util.ts` | `escapeIdentifier` for column
names, validated `generatedType` |
| `sanitize-default-value.util.ts` | `escapeLiteral` instead of
stripping |
| `serialize-default-value.util.ts` | `escapeLiteral` for values,
`escapeIdentifier` for enum casts |
| `get-ts-vector-column-expression.util.ts` | `escapeIdentifier` for
field names in expressions |
| `sanitize-default-value.util.spec.ts` | Updated tests for escape
behavior |

## Test plan

- [x] All 64 existing tests pass across 6 test suites
- [x] `lint:diff-with-main` passes
- [x] TypeScript typecheck — no new errors
- [ ] Verify workspace sync-metadata still works end-to-end
- [ ] Verify custom object/field creation works
- [ ] Verify enum field option changes work


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 14:24:10 +00:00
f4a61f26c0 Replace generic "Unknown error" messages with descriptive error details (#18019)
## Summary

- Replace all generic `"Unknown error"` fallback messages across the
server codebase with messages that include the actual error details
- The most impactful change is in `guard-redirect.service.ts`, which
handles OAuth redirect errors — non-`AuthException` errors (e.g.,
passport state verification failures) now show `"Authentication error:
<actual message>"` instead of the opaque `"Unknown error"`
- Gmail/Google error handler services now include the error message in
the thrown exception instead of discarding it
- Other catch blocks (workflow delay resume, migration runner rollback,
code interpreter, marketplace) now use `String(error)` for non-Error
objects instead of a static fallback

Fixes the class of issues reported in
https://github.com/twentyhq/twenty/issues/17812, where a user saw
"Unknown error" during Google OAuth and had no way to diagnose the root
cause (which turned out to be a session cookie / SSL configuration
issue).

## Test plan

- [ ] Verify OAuth error flows (e.g., Google Auth with misconfigured
callback URL) now display the actual error message on the `/verify` page
instead of "Unknown error"
- [ ] Verify Gmail sync error handling still correctly classifies and
re-throws errors with descriptive messages
- [ ] Verify workflow delay resume failures include the error details in
the workflow run status


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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 14:12:47 +00:00
8e6b267ff3 Allow DATE_TIME IS operand to filter on a whole day (#17529)
Fixes:  https://github.com/twentyhq/core-team-issues/issues/2027

We've replaced the DATE_TIME picker with DATE picker, and changed the
logic to filter for complete day period.



https://github.com/user-attachments/assets/ba7e1078-bab3-4c62-a803-d6a851f14b7d

---------

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-02-18 13:44:05 +00:00
neo773andGitHub 88146c2170 Fix Gmail thread awareness for custom labels (#18031)
This fixes two edge cases for Gmail

- When policy was set to `SELECTED_FOLDERS` excluding root INBOX, it
missed label changes, so messages with newly applied labels weren't
imported until a full resync.

- Gmail thread replies by default by default do no inherit parent
message's label properties so thread context was also lost because only
individually labeled messages were returned, dropping earlier parts of
the conversation.

Fixed by subscribing to `labelAdded`/`labelRemoved` history events and
fetching full thread context when at least one message in a thread
carries a synced label. `ALL_FOLDERS` path is untouched.
2026-02-18 14:10:38 +01:00
EtienneandGitHub 3706da9bcb v1.18 - Fix command (#18032)
Same as here https://github.com/twentyhq/twenty/pull/18016
2026-02-18 13:52:13 +01:00
9bac8f15d4 i18n - translations (#18029)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 13:26:54 +01:00
Charles BochetandGitHub 7332379d26 Improve API Client usage and add Typescript check (#18023)
## Summary


https://github.com/user-attachments/assets/1e75cc9d-d9d2-4ef2-99f9-34450f5d8de7



Add background incremental type checking (`tsc --watch`) to the SDK dev
mode, so type regressions are caught when the generated API client
changes — without requiring a full rebuild of source files.

Previously, removing a field from the data model would regenerate the
API client, but existing front components/logic functions referencing
the removed field wouldn't surface type errors (since their source
didn't change, esbuild wouldn't rebuild them).

## What changed

- **Background `tsc --watch`**: a long-lived TypeScript watcher runs
alongside esbuild watchers, incrementally re-checking all files when the
generated client changes. Only logs on state transitions (errors appear
/ errors clear) to stay quiet.
- **Atomic client generation**: API client is now generated into a temp
directory and swapped in atomically, avoiding a race condition where
`tsc --watch` could see an empty `generated/` directory
mid-regeneration.
- **Step decoupling**: orchestrator steps no longer receive
`uploadFilesStep` directly. Instead, they use callbacks (`onFileBuilt`,
`onApiClientGenerated`), and each step manages its own `builtFileInfos`
state.
- **`apiClientChecksum` omitted from `ApplicationConfig`**: it's a
build-time computed value, same as `packageJsonChecksum`.
<img width="327" height="177" alt="image"
src="https://github.com/user-attachments/assets/02bd25bb-fa41-42b0-8d96-01c51bd4580c"
/>

<img width="529" height="452" alt="image"
src="https://github.com/user-attachments/assets/61f6e968-365b-4a5b-8f2b-a8419d6b1bd3"
/>
2026-02-18 13:26:30 +01:00
Raphaël BosiandGitHub 2455c859b4 Add SSE for metadata and plug front components (#17998)
Create the necessary tooling to listen to metadata events and plug it to
the front components. Now we have a hot reload like experience when we
edit a component in an app.

## Backend

- Split `EventWithQueryIds` into `ObjectRecordEventWithQueryIds` and
`MetadataEventWithQueryIds`
- Publish metadata event batches to active SSE streams in
`MetadataEventsToDbListener`

## Frontend

- Create a metadata event dispatching pipeline: SSE metadata events are
grouped by metadata name, transformed into
`MetadataOperationBrowserEventDetail` objects, and dispatched as browser
`CustomEvents`
- Add `useListenToMetadataOperationBrowserEvent` hook for consuming
metadata operation events filtered by metadata name and operation type
- Rename `useListenToObjectRecordEventsForQuery` to
`useListenToEventsForQuery`, now accepting both
`RecordGqlOperationSignature` and `MetadataGqlOperationSignature`
- Implement `useOnFrontComponentUpdated` which subscribes to front
component metadata events and updates the Apollo cache when the
component is modified
- Add `builtComponentChecksum` to the front component query and appends
it to the component URL for browser cache invalidation
2026-02-18 11:26:20 +00:00
martmullandGitHub e3753bf822 App feedbacks (#18028)
as title
2026-02-18 11:04:54 +00:00
WeikoandGitHub f3faa11dd2 New field creates fields widget field (#18022)
## Context
Introducing "NewFieldDefaultConfiguration" to FIELDS widget
configurations
```typescript
{
  isVisible: boolean;
  viewFieldGroupId: string | null;
}
```

This configuration will define where a new field should be added (which
section) and its default visibility inside FIELDS widget views.
The new field position should always be at the end (meaning the last
position for the view fields OR the last position of a viewFieldGroup)

See "New fields" on this screenshot
<img width="401" height="724" alt="Layout V1"
src="https://github.com/user-attachments/assets/4969bcaa-f244-4504-8947-778a02c24c47"
/>
2026-02-18 11:03:27 +00:00
nitinandGitHub 477fbc0865 fixes: loosen up front validation, add resolveEntityRelationUniversalIdentifiers to update and restore (#18015)
closes https://github.com/twentyhq/private-issues/issues/419
2026-02-18 10:35:08 +00:00
EtienneGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
08a3d983cb Date & DateTime validation fixes / improvements (#18009)
Fixes https://github.com/twentyhq/twenty/issues/17138

- Backend should have strict date/dateTime format validation
- FE in import csv is more permissive

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-02-18 10:13:18 +00:00
EtienneandGitHub ee15e034b5 Files command - fixes (#18016)
- Fixed "property entity not found" error when updating/creating a new
field and querying the same object repository just after
- Downgraded log type for unnecessary migration
2026-02-18 09:09:52 +00:00
b7274da8fa i18n - docs translations (#18020)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 08:12:20 +01:00
4a485aecb0 i18n - docs translations (#18017)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 01:06:14 +01:00
BOHEUSGitHubneo773neo773greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
5ae1d94f23 Fix connected account permissions (#17598)
Fixes #17411

---------

Co-authored-by: neo773 <neo773@protonmail.com>
Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-18 01:05:56 +01:00
Abdul RahmanGitHubCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>DevessierFélix Malfait
015ccbf0a7 Navbar customization improvements (#17863)
- Add folder icon editing support
- And other navbar customization fixes/improvements.

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-02-17 19:53:52 +00:00
3d362e6e01 i18n - docs translations (#18014)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-17 19:33:21 +01:00
Charles BochetGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
d7f025157b Migrate more to Jotai (#17968)
As per title

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-17 19:24:36 +01:00
98482f3a01 i18n - translations (#18013)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-17 19:18:53 +01:00
BOHEUSandGitHub 171efe2a19 New onboarding plan (#17776)
Fixes github.com/twentyhq/core-team-issues/issues/637
2026-02-17 19:11:47 +01:00
Baptiste DevessierandGitHub 7512b9f9bb Load view field groups (#18010) 2026-02-17 17:22:48 +00:00
Charles BochetandGitHub c0cc0689d6 Add Client Api generation (#17961)
## Add API client generation to SDK dev mode and refactor orchestrator
into step-based pipeline

### Why

The SDK dev mode lacked typed API client generation, forcing developers
to work without auto-generated GraphQL types when building applications.
Additionally, the orchestrator was a monolithic class that mixed watcher
management, token handling, and sync logic — making it difficult to
extend with new steps like client generation.

### How

- **Refactored the orchestrator** into a step-based pipeline with
dedicated classes: `CheckServer`, `EnsureValidTokens`,
`ResolveApplication`, `BuildManifest`, `UploadFiles`,
`GenerateApiClient`, `SyncApplication`, and `StartWatchers`. Each step
has typed input/output/status, managed by a new `OrchestratorState`
class.
- **Added `GenerateApiClientOrchestratorStep`** that detects
object/field schema changes and regenerates a typed GraphQL client (via
`@genql/cli`) into `node_modules/twenty-sdk/generated` for seamless
imports.
- **Replaced `checkApplicationExist`** with `findOneApplication` on both
server resolver and SDK API service, returning the entity data instead
of a boolean.
- **Added application token pair mutations**
(`generateApplicationToken`, `renewApplicationToken`) to the API
service, with the server now returning `ApplicationTokenPairDTO`
containing both access and refresh tokens.
- **Restructured the dev UI** into `dev/ui/components/` with dedicated
panel, section, and event log components.
- **Simplified `AppDevCommand`** from ~180 lines of watcher management
down to ~40 lines that delegate entirely to the orchestrator.
2026-02-17 18:45:52 +01:00
0891886aa0 i18n - translations (#18012)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-17 18:35:35 +01:00
Thomas TrompetteandGitHub f768bbe512 Replace country code by calling code in workflows (#18008)
Long overdue PR: replacing deprecated country code in workflows.
Will allow to use variables.

We keep storing the country code since it allows to display the right
flag in picker when there are multiple countries for one calling code.
But we do not store country for variables.

<img width="477" height="254" alt="Capture d’écran 2026-02-17 à 17 25
23"
src="https://github.com/user-attachments/assets/dc67c41c-33cf-4021-b7bb-490827b2aa3c"
/>
2026-02-17 17:06:09 +00:00
610c0ebc9d Move secure HTTP client IP validation to connection level (#18006)
## Summary
- Refactors SSRF protection from a request-level adapter to
connection-level agents, validating resolved IPs in `createConnection` +
socket `lookup` events
- Sets both `httpAgent` and `httpsAgent` so validation applies
regardless of protocol switches during redirects
- Caps `maxRedirects` to 10 as defense in depth

## Test plan
- [x] All 59 existing + new unit tests pass (agent util, isPrivateIp,
service)
- [x] No linter errors
- [ ] Verify webhook delivery still works with URLs that redirect
- [ ] Verify image upload from external URLs still works (relies on
redirect following)


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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes core outbound HTTP security behavior and redirect handling,
which could impact webhook/image-fetch flows and connection semantics
despite improved SSRF coverage.
> 
> **Overview**
> Refactors outbound SSRF protection from a custom axios `adapter` to
connection-level `httpAgent`/`httpsAgent` created by new
`createSsrfSafeAgent`, which blocks private IP literals up front and
validates DNS-resolved IPs via the socket `lookup` event.
> 
> When safe mode is enabled, `SecureHttpClientService.getHttpClient` now
always installs both agents and enforces a capped `maxRedirects`
(default `5`), and the old `getSecureAxiosAdapter`
implementation/tests/types are removed. `isPrivateIp` is
tightened/expanded to treat `0.0.0.0/8` as private and avoid
misclassifying bare IPv4 decimals as IPv6.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8261da4ff0. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 16:59:38 +00:00
Lucas BordeauandGitHub e4aad7751f Fixed group by query order by inside group (#18005)
Fixes https://github.com/twentyhq/core-team-issues/issues/2229

This PR fixes a bug on board, that we thought was due to dev seeds, but
that was in fact a conflict of `ORDER BY` clauses at the SQL level in
group by queries.

The problem was that an ORDER BY was applied on top of a sub-query ORDER
BY, thus breaking the initial ordering of each group.

# Before

<img width="1117" height="1033" alt="Capture d’écran 2026-02-17 à 16
32 56"
src="https://github.com/user-attachments/assets/764183ae-4058-498b-9fe0-919e9511e67d"
/>

# After 

<img width="1101" height="1007" alt="Capture d’écran 2026-02-17 à 16
32 27"
src="https://github.com/user-attachments/assets/b3100b30-25b5-4568-a9cf-0760f42ceb88"
/>
2026-02-17 16:18:23 +00:00
7c5a13852b i18n - translations (#18007)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-17 17:27:50 +01:00
EtienneandGitHub 163c1175cb File - Migrate core pictures (workspace and member logo) + workflow attachments (#17924)
- Create a common file-by-id download controller
- Create core picture module with resolver and logic to handle
workspaceLogo and workspaceMemberProfilePicture update
- Create workflow file module (same)
- Data migration
2026-02-17 15:42:50 +00:00
Baptiste DevessierandGitHub 963f2de864 Translate page layout tab title (#17975)
> [!NOTE]
> The improvement will only fully work once all tabs get translated. 

## When using the front-end mocks


https://github.com/user-attachments/assets/cf7dc0fb-9438-4e18-841e-47558ff71474

## When loading page layout information from database


https://github.com/user-attachments/assets/d2c9d98b-97e2-4629-aa6c-53f3f3713733

## When editing dashboard


https://github.com/user-attachments/assets/c6ae3a7a-f05f-48ea-8b33-8f689e3f71f7

Closes
https://github.com/twentyhq/twenty/issues/17950#issuecomment-3902782952
2026-02-17 15:26:28 +00:00
martmullandGitHub e3fcff00b0 Fix initial code step functionInput (#18002)
At code step creation

## before
<img width="623" height="816" alt="image"
src="https://github.com/user-attachments/assets/0aed5ed8-8d56-4988-9d31-fe80942191bb"
/>

## after
<img width="627" height="528" alt="image"
src="https://github.com/user-attachments/assets/9e2a5cb9-7480-4734-8e41-25b45edcec07"
/>
2026-02-17 16:45:00 +01:00
Thomas TrompetteandGitHub b4e924b671 Sync views and navigation items (#18003)
Both objects are necessary to fully enjoy objects within applications

<img width="770" height="311" alt="Capture d’écran 2026-02-17 à 15 19
43"
src="https://github.com/user-attachments/assets/48c51fa4-63f4-45b2-a40a-df73f3aa79be"
/>
2026-02-17 16:32:41 +01:00
Lakshay ManchandaandGitHub 63a3f93a78 Emitting the correct event based on whether the record is being soft deleted or restored. (#17953)
fix for #17262 
This change ensures that when the record is restored, instead of
emitting a database event of "DELETED", a database event of "RESTORED"
will be emitted, as it is happening in the inner working of TypeORM.

This ensures that the DELETED event are not fired on RESTORED, for
example, triggering a workflow.

The screen recording shows that restoring the soft deleted records does
not trigger the workflow set to to run on "Record Deleted"


https://github.com/user-attachments/assets/90e0184f-2e08-466c-a40d-1592b60e64ff
2026-02-17 15:05:44 +00:00
Lucas BordeauandGitHub 8938dd637f Added relations to SSE events (#17683)
Fixes https://github.com/twentyhq/core-team-issues/issues/2192

This PR implements what is necessary to re-create the query that we
build on the frontend to obtain the returned object record from a
mutation, but on the backend, which was only partially implemented for
REST API.

Usually we want to have relations with only their id and label
identifier field to have lighter payloads.

In the event we only had depth 0 fields, with this PR we have all events
with depth 1 relations.

We have depth 2 for many-to-many cases, like updateOne or updateMany
result :
- Junction tables
- Activity target tables
2026-02-17 13:57:26 +00:00
WeikoandGitHub 20977428a1 Page layout various fixes (#17996)
## Context
- Add missing fields widget and FIELDS_WIDGET view for workflow run and
workflow version standard objects
- Fix FIELDS_WIDGET configuration fieldId universalIdentifier not being
converted to id when migration is executed.
2026-02-17 15:12:04 +01:00
Baptiste DevessierandGitHub 347298902d Fix dashboard new tab creation (#17971)
## Before


https://github.com/user-attachments/assets/cbc60013-8a5e-4af8-b02c-7dfbaf0c15e5


## After 


https://github.com/user-attachments/assets/1fd6f9f7-e774-4c63-aa80-50b33d2a4c59
2026-02-17 14:46:19 +01:00
5750c9be0c i18n - translations (#17997)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-17 14:36:47 +01:00
08feb6f651 i18n - translations (#17995)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-17 14:29:21 +01:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Charles Bochet
c9c3b2b691 fix: improve clipboard copy for non-HTTPS self-hosted deployments (#17989)
Closes #8305

The Clipboard API (`navigator.clipboard`) requires a secure context
(HTTPS or localhost). Self-hosted deployments on plain HTTP silently
fail when copying.

This PR:
- Adds a `document.execCommand('copy')` fallback for insecure contexts
- Shows a descriptive error message explaining HTTPS is required when
the fallback also fails
- Consolidates 3 components that were using `navigator.clipboard`
directly (without error handling) to use the centralized
`useCopyToClipboard` hook

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Scoped to frontend clipboard UX with a defensive fallback and clearer
errors; minimal impact outside copy flows.
> 
> **Overview**
> Improves copy-to-clipboard behavior in non-HTTPS/self-hosted
deployments by enhancing `useCopyToClipboard` to use
`navigator.clipboard` only in secure contexts and otherwise fall back to
`document.execCommand('copy')`.
> 
> Updates 2FA setup screens and the view visibility dropdown to use the
centralized `copyToClipboard` helper (with consistent snackbars), and
shows a more descriptive error (longer duration) when copying fails due
to an insecure context.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
30944e63eb. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-02-17 14:24:00 +01:00
09e1684300 feat: show auto-generated conversation title for AI chat. (#17922)
## Summary
Replaces the static "Ask AI" header in the command menu with the
conversation’s auto-generated title once it’s set after the first
message.

## Changes
- **Backend:** Title is generated after the first user message (existing
behavior).
- **Frontend:** After the first stream completes, we fetch the thread
title and sync it to:
- `currentAIChatThreadTitleState` (persists across command menu
close/reopen)
- Command menu page info and navigation stack (so the title survives
back navigation)
- **Entry points:** Opening Ask AI from the left nav or command center
uses the same title resolution (explicit `pageTitle` → current thread
title → "Ask AI" fallback).
- **Race fix:** Title sync only runs when the thread that finished
streaming is still the active thread, so switching threads mid-stream
doesn’t overwrite the current thread’s title.

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 12:46:35 +00:00
nitinandGitHub e80e9a6a25 fix: on charts newly added multi split values toggle wasnt being saved (#17990)
:)
2026-02-17 14:01:11 +01:00
c2fe18af53 i18n - translations (#17993)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-17 14:00:47 +01:00
EtienneandGitHub 2b702b2b45 Navigation Menu Item - Migration in v1.18 (#17991)
Add navigation menu item data migration command in 1.18
2026-02-17 14:00:24 +01:00
EtienneandGitHub dc167b2d3d File - Disable file upload in standalone rich text widget (#17934)
File's url are not signed, in standalone rich text. We choose to disable
file upload to prevent unauthorised file upload links.
2026-02-17 13:46:28 +01:00
1e6c5b57b2 fix: use pickMorphGroupSurvivor in relation loader to match field metadata deduplication (#17988)
## Summary

- Fixes "Target field metadata full object not found" error thrown
during optimistic effects (e.g., bulk delete) on workspaces with custom
objects
- The relation loader was using a simple sort-by-ID to pick the
representative morph field, while `filterMorphRelationDuplicateFields`
uses `pickMorphGroupSurvivor` which prefers active non-system fields.
When a custom object's auto-created morph field (`isSystem: true`)
happened to have the smallest UUID, the two loaders would disagree — the
relation DTO pointed to that system field's ID, but the field metadata
loader filtered it out in favor of a standard field, causing the
frontend lookup to fail.
- Now both code paths use `pickMorphGroupSurvivor` so they always agree
on which morph field represents the group.

## Test plan

- [ ] Create a custom object on a workspace that already has standard
objects with morph relations (e.g., noteTarget, taskTarget)
- [ ] Bulk-select and delete records (e.g., People) — should no longer
throw "Target field metadata full object not found"

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, localized change to morph-relation selection logic in a
dataloader; main risk is altered field choice for edge-case morph
groups, but behavior now matches existing deduplication.
> 
> **Overview**
> Ensures the relation dataloader picks the representative
morph-relation target field using `pickMorphGroupSurvivor` (preferring
active non-system fields) instead of the previous sort-by-id approach.
> 
> This aligns `createRelationLoader` with
`filterMorphRelationDuplicateFields`, preventing mismatches where
relation DTOs could reference a morph field that gets filtered out
elsewhere (e.g., triggering “Target field metadata full object not
found”).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c3a6d86126. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 13:46:05 +01:00
3516be2cf4 i18n - translations (#17987)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-17 13:43:13 +01:00
9477bb3677 Improve AI chat UX (#17974)
## Summary
- update AI chat message typography and list line-height for readability
- apply richer markdown-section styling for headings, spacing,
separators, and inline code
- keep links non-underlined by default with underline on hover, using
accent11 for link color
- preserve previous AI chat table design while keeping other markdown
improvements

## Validation
- yarn eslint
packages/twenty-front/src/modules/ai/components/LazyMarkdownRenderer.tsx
packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-02-17 11:24:33 +01:00
aac032e517 i18n - translations (#17986)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-17 11:14:14 +01:00
Paul RastoinandGitHub 5544b5dcfe Fix and refactor all metadata relation (#17978)
# Introduction
The initial motivation was that in the workspace migration create action
some universal foreign key aggregators weren't correctly deleted before
returned due to constant missconfiguration
<img width="2300" height="972" alt="image"
src="https://github.com/user-attachments/assets/9401eb02-2bb2-4e69-9c5f-9a354ff61079"
/>
It also meant that under the hood some optimistic behavior wasn't
correctly rendered for some aggregators

## Solution
Refactored the `ALL_METADATA_RELATIONS` as follows:

This way we can infer the FK and transpile it to a universalFK, also the
aggregators are one to one instead of one versus all available
Making the only manual configuration to be defined the `foreignKey` and
`inverseOneToManyProperty`

```
┌──────────────────────────────────────┐      ┌─────────────────────────────────────────────┐
│  ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY│      │      ALL_ONE_TO_MANY_METADATA_RELATIONS     │
│──────────────────────────────────────│      │─────────────────────────────────────────────│
│  Derived from: Entity types          │      │  Derived from: Entity types                 │
│                                      │      │                                             │
│  Provides:                           │      │  Provides:                                  │
│   • foreignKey                       │      │   • metadataName                            │
│                                      │      │   • flatEntityForeignKeyAggregator          │
│  Standalone low-level primitive      │      │   • universalFlatEntityForeignKeyAggregator │
└──────────────┬───────────────────────┘      └──────────────┬──────────────────────────────┘
               │                                             │
               │ foreignKey type +                           │ inverseOneToManyProperty
               │ universalForeignKey derivation              │ keys (type constraint)
               │                                             │
               ▼                                             ▼
       ┌───────────────────────────────────────────────────────────────┐
       │              ALL_MANY_TO_ONE_METADATA_RELATIONS              │
       │───────────────────────────────────────────────────────────────│
       │  Derived from:                                               │
       │   • Entity types (metadataName, isNullable)                  │
       │   • ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY (FK → universalFK)  │
       │   • ALL_ONE_TO_MANY_METADATA_RELATIONS (inverse keys)        │
       │                                                              │
       │  Provides:                                                   │
       │   • metadataName                                             │
       │   • foreignKey (replicated from FK constant)                 │
       │   • inverseOneToManyProperty                                 │
       │   • isNullable                                               │
       │   • universalForeignKey                                      │
       └──────────────────────────┬────────────────────────────────────┘
                                  │
               ┌──────────────────┼──────────────────┐
               │                  │                  │
               ▼                  ▼                  ▼
   ┌───────────────────┐ ┌────────────────┐ ┌──────────────────────┐
   │  Type consumers   │ │  Atomic utils  │ │  Optimistic utils    │
   │───────────────────│ │────────────────│ │──────────────────────│
   │ • JoinColumn      │ │ • resolve-*    │ │ • add/delete flat    │
   │ • RelatedNames    │ │ • get-*        │ │   entity maps        │
   │ • UniversalFlat   │ │                │ │ • add/delete         │
   │   EntityFrom      │ │                │ │   universal flat     │
   │                   │ │                │ │   entity maps        │
   └───────────────────┘ └────────────────┘ │                      │
                                            │  (bridge via         │
                                            │   inverseOneToMany   │
                                            │   Property →         │
                                            │   ONE_TO_MANY for    │
                                            │   aggregator lookup) │
                                            └──────────────────────┘
```

### Previously
```
┌─────────────────────────────────────────────────────────────────────┐
│                       ALL_METADATA_RELATIONS                       │
│─────────────────────────────────────────────────────────────────────│
│  Derived from: Entity types                                        │
│                                                                    │
│  Structure: { [metadataName]: { manyToOne: {...}, oneToMany: {...},│
│               serializedRelations?: {...} } }                      │
│                                                                    │
│  manyToOne provides:                                               │
│   • metadataName                                                   │
│   • foreignKey                                                     │
│   • flatEntityForeignKeyAggregator (nullable, often wrong/null)    │
│   • isNullable                                                     │
│                                                                    │
│  oneToMany provides:                                               │
│   • metadataName                                                   │
│                                                                    │
│  Monolithic single source of truth                                 │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
                           │ manyToOne entries transformed via
                           │ ToUniversalMetadataManyToOneRelationConfiguration
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────────┐
│                  ALL_UNIVERSAL_METADATA_RELATIONS                   │
│─────────────────────────────────────────────────────────────────────│
│  Derived from: ALL_METADATA_RELATIONS (type-level transform)       │
│                                                                    │
│  Structure: { [metadataName]: { manyToOne: {...}, oneToMany: {...} │
│  } }                                                               │
│                                                                    │
│  manyToOne provides:                                               │
│   • metadataName                                                   │
│   • foreignKey                                                     │
│   • universalForeignKey (derived: FK → replace Id → UniversalId)   │
│   • universalFlatEntityForeignKeyAggregator (derived from          │
│     flatEntityForeignKeyAggregator → replace Ids → UniversalIds)   │
│   • isNullable                                                     │
│                                                                    │
│  oneToMany: passthrough from ALL_METADATA_RELATIONS                │
│                                                                    │
│  Duplicated monolith with universal key transforms                 │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
        ┌──────────────────┼──────────────────────┐
        │                  │                      │
        ▼                  ▼                      ▼
┌───────────────┐ ┌────────────────────┐ ┌──────────────────────┐
│ Type consumers│ │   Atomic utils     │ │  Optimistic utils    │
│───────────────│ │────────────────────│ │──────────────────────│
│ • JoinColumn  │ │ • resolve-entity-  │ │ • add/delete flat    │
│ • RelatedNames│ │   relation-univ-id │ │   entity maps        │
│ • Universal   │ │   (ALL_METADATA_   │ │   (ALL_METADATA_     │
│   FlatEntity  │ │    RELATIONS       │ │    RELATIONS         │
│   From        │ │    .manyToOne)     │ │    .manyToOne)       │
│               │ │                    │ │                      │
│ Mixed usage   │ │ • resolve-univ-    │ │ • add/delete univ    │
│ of both       │ │   relation-ids     │ │   flat entity maps   │
│ constants     │ │   (ALL_UNIVERSAL_  │ │   (ALL_UNIVERSAL_    │
│               │ │    METADATA_REL    │ │    METADATA_REL      │
│               │ │    .manyToOne)     │ │    .manyToOne)       │
│               │ │                    │ │                      │
│               │ │ • resolve-univ-    │ │ universalFlatEntity  │
│               │ │   update-rel-ids   │ │ ForeignKeyAggregator │
│               │ │   (ALL_UNIVERSAL_  │ │ read directly from   │
│               │ │    METADATA_REL    │ │ the constant         │
│               │ │    .manyToOne)     │ │                      │
│               │ │                    │ │                      │
│               │ │ • regex hack:      │ │                      │
│               │ │   foreignKey       │ │                      │
│               │ │   .replace(/Id$/,  │ │                      │
│               │ │   'UniversalId')   │ │                      │
└───────────────┘ └────────────────────┘ └──────────────────────┘
```
2026-02-17 11:13:54 +01:00
8244610bdc i18n - translations (#17983)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-17 11:05:36 +01:00
e3db73ef46 Change condition for multi-workspace check (#17938)
When deploying with
```
IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=true
IS_MULTIWORKSPACE_ENABLED=true
```
The first workspace can be created successfully. However, any attempt to
create additional workspaces as admin fails with the error: `Workspace
creation is restricted to admins` because `canAccessFullAdminPanel` is
**false**

If these flags are set to false during the initial deployment and
restarting the Docker container, workspace creation works normally.

Problem is caused by `canAccessFullAdminPanel`

---------

Co-authored-by: ehconitin <nitinkoche03@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-02-17 09:29:59 +01:00
7523143f12 i18n - docs translations (#17981)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-17 09:29:37 +01:00
nitinandGitHub cfc4e0e343 [DASHBOARDS] Add split multi-value fields setting for charts (#17907)
closes https://github.com/twentyhq/twenty/issues/17890



https://github.com/user-attachments/assets/dbf2ebe6-c2da-44d0-846e-d7fa9fac5f43
2026-02-17 09:29:17 +01:00
martmullandGitHub c3d565f266 Add default fields on object manifest (#17977)
as title

<img width="830" height="227" alt="image"
src="https://github.com/user-attachments/assets/8cbf4a14-5d1d-496f-a146-f31079e6e602"
/>
2026-02-16 16:50:46 +01:00
Paul RastoinandGitHub 2b7b05de2e [OBJECT_MANIFEST_BREAKING_CHANGE] Sync returns workspace migration (#17918)
# Introduction
In this PR we start returning a workspace migration post sync so it can
committed and provided within the tarball

## Universal aggregators utils
Created two utils
### deleteUniversalFlatEntityForeignKeyAggregators
Used when building a universal create action, a newly created actions
should not contain any aggregated foreign key so they won't be codegen
in the workspace migration but also they are overriden at uninversal to
flat transpilation anw

### resetUniversalFlatEntityForeignKeyAggregators
Used before validating a new flat entity creation, some validator will
consume the fk aggregator in order to validate integrity, but of
optimstically provided it can result to errors. To avoid caller
responsability we override them here

## create-field-action refactor
Refactored the universal and flat field create action to be following
the base actions in order to ease typing
Also it was tailored to handle unlimited amount of flat field metadata
in the same actions whereas in the reality we were always only sending
at max 2 ( for relation fields )

Note: relation field has to be provided at the same as if not optimistic
would fail to retrieve circular universal identifiers

## ObjectManifest
Now always expect a `labelIdentifierFieldMetadataUniversalIdentifier`

## Integration test
Created an integration test that creates an app, sync a first manifest
and a second implying update workspace migration action generation
2026-02-16 15:00:24 +01:00
4b3c58e013 i18n - docs translations (#17972)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-16 13:51:12 +01:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
c775d65952 feat: configure standard views and migrate attachment seeds to FILES field (#17958)
## Summary

- Add default visible view fields for `timelineActivity`, `attachment`,
`noteTarget`, `taskTarget`, and `workspaceMember` objects so they
display useful columns out of the box
- Standardize morph relation field labels to "Target" with
`IconArrowUpRight` for consistency across all pivot/junction tables
- Mark deprecated fields (`fullPath`, `fileCategory`,
`linkedRecordCachedName`, `linkedRecordId`, `linkedObjectMetadataId`) as
`isSystem` to hide them from the UI column picker
- Fix morph field deduplication logic (`pickMorphGroupSurvivor`) to
prefer active, non-system fields over auto-generated system fields from
custom objects
- Migrate attachment seeds from legacy `fullPath`/`fileCategory` to the
new `FILES` field type, creating proper `FileEntity` records in
`core.file` via `fileStorageService.writeFile()`
- Restore `customDomain` in the user query fragment


<img width="825" height="754" alt="Screenshot 2026-02-15 at 15 44 27"
src="https://github.com/user-attachments/assets/9596a3dd-8d3a-43c0-925a-0adef9ee68a8"
/>
<img width="736" height="731" alt="Screenshot 2026-02-15 at 15 44 13"
src="https://github.com/user-attachments/assets/cd1a66c5-731d-43e6-bbc3-703cbeda1652"
/>
<img width="722" height="757" alt="Screenshot 2026-02-15 at 15 44 03"
src="https://github.com/user-attachments/assets/b5210546-6a40-4940-8e4f-874818a614fb"
/>
<img width="907" height="757" alt="Screenshot 2026-02-15 at 15 43 52"
src="https://github.com/user-attachments/assets/ead5b9a8-1989-4d68-9640-583da6233711"
/>
<img width="1002" height="731" alt="Screenshot 2026-02-15 at 15 43 38"
src="https://github.com/user-attachments/assets/38accb8c-f5d5-4bfc-b245-06389849810b"
/>




<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches migration/upgrade commands that write to core metadata tables
and adjust field/view definitions, plus changes dev seeding to create
`core.file` records; mistakes could affect UI visibility or seed
integrity across workspaces.
> 
> **Overview**
> Adds a new `upgrade:1-18:backfill-standard-views-and-field-metadata`
command that, per workspace, marks specific fields as `isSystem`,
normalizes morph-relation field `label`/`icon` to
`Target`/`IconArrowUpRight`, and backfills missing standard
`view`/`viewField` rows for `attachment`, `noteTarget`, `taskTarget`,
`timelineActivity`, and `workspaceMember`, followed by cache
invalidation + metadata version bump.
> 
> Refactors morph-relation deduplication to pick a single survivor per
`morphId` using a new `pickMorphGroupSurvivor` rule (prefer active +
non-system, then smallest id), with new unit tests.
> 
> Updates standard metadata generators and snapshots to reflect the new
system flags and default view fields, and rewrites attachment dev
seeding to populate the new `file` (FILES field) JSON and create
corresponding `core.file` entries via `FileStorageService.writeFile`
with workspace-scoped file IDs.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
b1939bbf6f. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-16 13:34:56 +01:00
b80146c890 i18n - docs translations (#17967)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-16 12:11:24 +01:00
5604cdbb4b i18n - translations (#17966)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-16 11:07:14 +01:00
martmullandGitHub da064d5e88 Support define is tool logic function (#17926)
- supports isTool and timeout settings in defineLogicFunction in apps
and in setting tabs definition
- compute for all toolInputSchema for logic funciton, in settings and in
code steps

<img width="991" height="802" alt="image"
src="https://github.com/user-attachments/assets/05dc1221-cac9-45a3-87b0-3b13161446fd"
/>
2026-02-16 10:43:29 +01:00
f694bb99b3 fix: restore customDomain field in getCurrentUser query fragment (#17949)
## Summary
- The `customDomain` field was accidentally removed from the
`currentWorkspace` GraphQL query fragment in #16016 (Nov 2025), when
`workspaceCustomApplication { id }` was added in its place rather than
alongside it.
- This caused the custom domain settings page to never display the
configured domain value, the reload/delete buttons, or the DNS records —
since `currentWorkspace.customDomain` was always `undefined`.
- Restores the missing field in the query fragment.

## Test plan
- [ ] Navigate to Settings > Domains on a workspace with a custom domain
configured
- [ ] Verify the custom domain value appears in the input field
- [ ] Verify the Reload and Delete buttons are visible
- [ ] Verify DNS records are displayed


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 06:58:22 +01:00
8190cd5fbf [FRONT COMPONENTS] Allow style librairies in remote dom (#17936)
https://github.com/user-attachments/assets/ce1b2b06-872f-41d0-844a-61db91696624

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-15 23:50:57 +01:00
b901bdec40 i18n - translations (#17945)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-15 23:50:35 +01:00
Abdullah.andGitHub b1d3ec665a fix: qs arrayLimit bypass in comma parsing allows denial of service (#17947)
Resolves [Dependabot Alert
454](https://github.com/twentyhq/twenty/security/dependabot/454) and
[Dependabot Alert
455](https://github.com/twentyhq/twenty/security/dependabot/455).

`zapier-platform-cli` leaves in one entry of qs locked at version
`6.5.x`, so the alert might not close automatically. However, the PR
fixes any occurrences in the server itself.
2026-02-15 19:52:23 +00:00
Abdullah.andGitHub ac3ac5cd4d fix: markdown-it is has a regular expression denial of service (#17946)
Resolves [Dependabot Alert
456](https://github.com/twentyhq/twenty/security/dependabot/456).
2026-02-15 19:52:21 +00:00
6f251a6f8e Add @mention support in AI Chat input (#17943)
## Summary
- Add `@mention` support to the AI Chat text input by replacing the
plain textarea with a minimal Tiptap editor and building a shared
`mention` module with reusable Tiptap extensions (`MentionTag`,
`MentionSuggestion`), search hook (`useMentionSearch`), and suggestion
menu — all shared with the existing BlockNote-based Notes mentions to
avoid code duplication
- Mentions are serialized as
`[[record:objectName:recordId:displayName]]` markdown (the format
already understood by the backend and rendered in chat messages), and
displayed using the existing `RecordLink` chip component for visual
consistency
- Fix images in chat messages overflowing their container by
constraining to `max-width: 100%`
- Fix web_search tool display showing literal `{query}` instead of the
actual query (ICU single-quote escaping issue in Lingui `t` tagged
templates)

## Test plan
- [ ] Open AI Chat, type `@` and verify the suggestion menu appears with
searchable records
- [ ] Select a mention from the dropdown (via click or keyboard
Enter/ArrowUp/Down) and verify the record chip renders inline
- [ ] Send a message containing a mention and verify it appears
correctly in the conversation as a clickable `RecordLink`
- [ ] Verify Enter sends the message when the suggestion menu is closed,
and selects a mention when the menu is open
- [ ] Verify images in AI chat responses are constrained to the
container width
- [ ] Verify the web_search tool step shows the actual search query
(e.g. "Searched the web for Salesforce") instead of `{query}`
- [ ] Verify Notes @mentions still work as before


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 14:37:33 +01:00
0876197c8d i18n - translations (#17935)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-14 10:54:52 +01:00
Abdullah.andGitHub 4f903fa0ba fix: add explicit error hint for coverage threshold failures in frontend test step (#17937) 2026-02-14 09:16:49 +00:00
2072d5f720 i18n - docs translations (#17939)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-14 00:03:59 +01:00
neo773andGitHub d54b713264 Respect Gmail retry-after in messaging throttle (#17850)
Gmail 429/403 rate-limit responses include an explicit retry-after
timestamp, usually ~15 minutes out.

The exponential backoff starts at 1 minute, so the channel burns through
all 5 retry attempts before the window actually closes and gets marked
as permanently failed.

Adds throttleRetryAfter to the message channel and uses max(backoff,
retryAfter) in isThrottled().
2026-02-13 18:20:37 +01:00
neo773GitHubClaude Opus 4.6cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
84afbb4d2c feat: add draft email workflow action (#17793)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-02-13 18:20:20 +01:00
ebfaff0a53 i18n - translations (#17932)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-13 18:19:38 +01:00
martmullandGitHub c3d8404112 Update cli tool versions (#17933)
as title
2026-02-13 18:03:13 +01:00
a95a286c59 Revert export twenty UI (#17929)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 17:55:04 +01:00
WeikoandGitHub c173f01601 Create record page layout after custom object creation (#17923)
## Context
Creating a new object should now also create its record page layout,
tabs and widgets, including fields widget with its associated views/view
fields.
Custom objects record page layout fields widgets don't have section per
default

Note: I had to enable some widget creation through the custom API but we
should now implement proper validation (which should be minimal since
there is usually only the configuration type in the configuration
(except for FIELDS widget which contains a viewId)

Next step: Create view field should also create a viewField for the
FIELDS_WIDGET view (we should also add in the FIELDS widget
configuration a newFieldDefaults which will contain default visibility
and position to apply to the new view field)

The feature is still gated behind an env variable (this was necessary
for workspace creation, not so much here in this case but I prefer to
keep the same path for consistence)
2026-02-13 16:16:47 +00:00
kpdevGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
cda70a70ca fix: replace react-tooltip with AppTooltip and refactor MenuItemAvatar (#17846)
This PR addresses TODO comments and improves code quality:

### 1. PullRequestItem.tsx
- Replaced `react-tooltip` with `twenty-ui` `AppTooltip` component
- Removed TODO comment
- Uses internal component library for consistency

### 2. MenuItemAvatar.tsx  
- Refactored to use `MenuItem` internally, eliminating code duplication
- Removed about 63 lines of duplicate code
- Removed TODO comment as the merge is now complete

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-02-13 16:12:16 +00:00
a48b69d1e5 i18n - docs translations (#17930)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-13 17:33:48 +01:00
Charles BochetandGitHub 4817c86bf0 Enhance stories for front component in sdk (#17925)
<img width="3838" height="2014" alt="image"
src="https://github.com/user-attachments/assets/87f4d4ba-7b39-4c44-b863-d8bfa6c4fd8a"
/>
2026-02-13 17:16:03 +01:00
martmullandGitHub 0befb021d0 Add scripts to publish cli tools (#17914)
- moves workspace:* dependencies to dev-dependencies to avoid spreading
them in npm releases
- remove fix on rollup.external
- remove prepublishOnly and postpublish scripts
- set bundle packages to private
- add release-dump-version that update package.json version before
releasing to npm
- add release-verify-build that check no externalized twenty package
exists in `dist` before releasing to npm
- works with new release github action here ->
https://github.com/twentyhq/twenty-infra/pull/397
2026-02-13 15:43:32 +00:00
e7b3f65c0c i18n - translations (#17920)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-13 16:53:53 +01:00
Thomas TrompetteandGitHub 5184491c59 Fix event stream infinite loops (#17919)
Event stream resolver was now configured for metadata scope but client
was still created on core.
So endpoints could not be found.
2026-02-13 16:53:39 +01:00
Charles BochetandGitHub f17cc4d190 Improve building of twenty-sdk (#17913)
## Split twenty-sdk build into separate Node and browser targets

The SDK was bundling Node.js code (CLI, SDK API) and browser code (UI
components, front-component renderer) through a single Vite config. This
caused incorrect externalization — Node builtins leaked into browser
bundles and browser-specific chunking logic applied to CLI output.

This PR splits the build into `vite.config.node.ts` and
`vite.config.browser.ts` so each target gets the right externals and
output format.

Also includes a few housekeeping renames:
- `front-component` export path → `front-component-renderer` (matches
what it actually is)
- `front-component-common` merged into `front-component-api` (was a
needless extra module)
2026-02-13 15:58:19 +01:00
nitinandGitHub 463ce43442 [FRONT COMPONENT] Add Front component token generation (#17855)
closes https://github.com/twentyhq/core-team-issues/issues/2180


https://github.com/user-attachments/assets/a898455d-eb1c-4d22-b585-785e98fc38a7
2026-02-13 14:18:09 +00:00
5cc0c03262 i18n - translations (#17915)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-13 15:01:26 +01:00
e2b9bc935f Add mentions feature to objects in notes (#16373)
Issue #15755 

### Context
As shown in the issue, users should be able to @ objects and have it be
linked to their associated page.

### Changes Made
- Updated the BlockNote schema to include the newly created
MentionInlineContent that displays an object using a RecordChip
- Added the UI for the menu that appears to select the requested object
(similar to this example:
https://www.blocknotejs.org/examples/ui-components/suggestion-menus-slash-menu-component)
- Added a hook to retrieve the data for the UI
- Added the @ trigger to the block editor

### Result 

https://github.com/user-attachments/assets/87dae6a2-ad7a-4642-80b2-8b1751feae24

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 13:12:37 +00:00
nitinandGitHub b7c1d47273 chore(nx): remove leftover Nx wrapper artifacts (#17916)
#17147 removed the root ./nx script, but wrapper-mode artifacts were
still present (installation in
[nx.json](https://github.com/twentyhq/twenty/blob/main/nx.json) and
tracked
[nxw.js](https://github.com/twentyhq/twenty/blob/main/.nx/nxw.js),
leaving an inconsistent setup.

This PR completes that cleanup by:

- removing installation from nx.json
- deleting tracked nxw.js
- ignoring nxw.js to prevent accidental re-introduction

Validated that Nx still works via yarn nx / npx nx.
2026-02-13 13:04:43 +00:00
befbcef824 fix: prevent tab synchronization between different records (#17559)
Fixes issue where tabs were synchronized when opening two records of the
same type in show page and side panel.

The root cause was that tab instance IDs were only based on
`pageLayoutId`, causing all records using the same page layout to share
the same tab state.

This change includes the record ID in the tab instance ID, making tabs
unique per record while maintaining backward compatibility for cases
where no record ID is available.

Fixes #17522

---------

Co-authored-by: Eruis <github@eruis.example>
2026-02-13 13:04:07 +00:00
Baptiste DevessierandGitHub 83b800a077 Drag and drop fields of Fields widgets (#17910)
https://github.com/user-attachments/assets/97fad3f3-8654-4216-b58c-b7411119c8ce
2026-02-13 11:41:25 +00:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>DevessierBaptiste Devessier
d08c098065 Hide delete button for record page layout widgets (#17892)
## Before


https://github.com/user-attachments/assets/ea04e8cc-a445-498d-b0d0-d3230a9eeec8

## After


https://github.com/user-attachments/assets/c88aead2-2d3a-42ec-8829-7595a4faa116

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Devessier <29370468+Devessier@users.noreply.github.com>
Co-authored-by: Baptiste Devessier <baptiste@devessier.fr>
2026-02-13 11:19:06 +00:00
Paul RastoinandGitHub d88fa0cb2b Migrate create syncable entity cursor rule to skills (#17912)
# Introduction
Splitting the create syncable entity rule into dedicated scoped skills
in order to favorise multi agent pattern with more granular context

### Multi-Agent Workflow

For parallel development:
1. **Agent 1** (Foundation): Complete Step 1 first - unblocks everyone
2. **Agent 2** (Cache): Can start immediately after Step 1
3. **Agent 3** (Builder): Can work in parallel with Agent 4 after Step 1
4. **Agent 4** (Runner): Can work in parallel with Agent 3 after Step 1
5. **Agent 5** (Integration): Assembles everything after Steps 2-4
2026-02-13 11:18:31 +00:00
216a7331f8 Speed up twenty-emails build by replacing vite-plugin-dts with tsgo (#17857)
## Summary

- Removed `vite-plugin-dts` (which used `tsc` internally) from the Vite
build and replaced DTS generation with `tsgo` as a sequential post-build
step — **~0.7s vs 1-10s**.
- Disabled `reportCompressedSize` to skip gzip computation for 64 output
files.
- Converted the build target to an explicit `nx:run-commands` executor
with sequential `vite build` → `tsgo` commands.

The `twenty-emails:build` step goes from ~22s to ~7s under load. 

## Test plan

- [x] `nx build twenty-emails` produces both JS (64 files) and DTS (74
files) correctly
- [x] `dist/index.d.ts` exports match the source `src/index.ts`
- [x] Full `nx build twenty-server` succeeds end-to-end
- [ ] CI build passes


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 10:39:26 +00:00
975c64bf3d i18n - translations (#17911)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-13 11:20:56 +01:00
Paul RastoinandGitHub b34f7bcb33 Refactor metadata events to contain scalarEntity (#17908)
# Introduction
Avoid sending flat entity that contains server specific properties such
as foreignKey aggregators and universal properties by transpiling from
flat entity to scalar flat entity

A scalar flat entity is the exact match with the entities columns

Following https://github.com/twentyhq/twenty/pull/17622
2026-02-13 10:01:54 +00:00
14e8c869be i18n - translations (#17905)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-13 10:54:25 +01:00
Félix MalfaitGitHubCursorcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
21c51ec251 Improve AI agent chat, tool display, and workflow agent management (#17876)
## Summary

- **Fix token renewal endpoint**: Use `/metadata` instead of `/graphql`
for token renewal in agent chat, fixing auth issues
- **Improve tool display**: Add `load_skills` support, show formatted
tool names (underscores → spaces) with finish/loading states, display
tool icons during loading, and support custom loading messages from tool
input
- **Refactor workflow agent management**: Replace direct
`AgentRepository` access with `AgentService` for create/delete/find
operations in workflow steps, improving encapsulation and consistency
- **Simplify Apollo client usage**: Remove explicit Apollo client
override in `useGetToolIndex`, add `AgentChatProvider` to
`AppRouterProviders`
- **Fix load-skill tool**: Change parameter type from `string` to `json`
for proper schema parsing
- **Update agent-chat-streaming**: Use `AgentService` for agent
resolution and tool registration instead of direct repository queries

## Test plan

- [ ] Verify AI agent chat works end-to-end (send message, receive
response)
- [ ] Verify tool steps display correctly with icons and proper messages
during loading and after completion
- [ ] Verify workflow AI agent step creation and deletion works
correctly
- [ ] Verify workflow version cloning preserves agent configuration
- [ ] Verify token renewal works when tokens expire during agent chat


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-02-13 09:27:38 +00:00
MarieandGitHub 5c3c2e08a6 Some fixes (#17904) 2026-02-13 09:23:14 +00:00
fdb5d2fbcd Feat 17408 : Add remove option for object permissions rule (#17601)
This PR aims to fix: #17408 

Main modification includes adding a new column for dropdown in the
object permission rule table (containing options for editing and
removal). Removal logic is implemented using existing pattern with hook
```useResetObjectPermission``` (relies on existing hooks
```useUpsertFieldPermissionInDraftRole``` and
```useUpsertObjectPermissionInDraftRole```).

Feel free to suggest any necessary changes. Functionality (unrestricted
access is allowed when permission removal is applied) is already tested.

Demo video:
https://drive.google.com/file/d/1M4RYHw-JEhDdJksKkL3MY_VyXAV3aS9I/view?usp=sharing

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-02-13 09:16:36 +00:00
EtienneandGitHub 5c2c588885 Files - Migrate attachments in activities (#17808)
As attachment files have migrated from fullPath to file files field,
need to migrate richText logic to fit to new attachment file handling +
data migration
2026-02-13 09:11:23 +00:00
MarieandGitHub 90a30263ac [Apps] Content + permission tabs for marketplace and installed apps (#17888)
In this PR
- Content tab for marketplace apps and installed apps: complies with
[figma](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=34845-126856);
addition of field section showing fields added to standard objects;
initiative: arrow opens a sub-table of fields rows. (suggested because
1/ for marketplace apps we cannot redirect to the actual object page in
settings since the object does not exist yet in the workspace 2/ since
we dont have an quick "go back to application page" option, it can be
annoying to be redirected to a different setting page when we just want
to look at the content of the app objects)
- Permission tab for marketplace apps and installed apps
- left to do - settings tab (in another PR)

There are breaking changes but it's behind a feature flag not exposed
(access to marketplace) so not problematic


marketplace apps

https://github.com/user-attachments/assets/4c660101-50fc-47ce-b90a-8d6f17db5e74

installed apps

https://github.com/user-attachments/assets/c9229ee1-e75f-4cad-8766-758b2c5b37b4
2026-02-12 17:38:53 +00:00
13c2234856 feat: emit metadata events for schema changes with actor context for webhooks (#17622)
## Summary

This PR adds **metadata eventing**: when schema metadata
(objectMetadata, fieldMetadata, view, viewField, etc.) is created,
updated, or deleted, we now emit events that can trigger webhooks and
future audit logs. It also adds **actor context** (`userId`,
`workspaceMemberId`) to those events so subscribers can attribute
changes to a user or API key.

## What changed

### 1. Metadata eventing (first commit)

- **MetadataEventEmitter**  
New service that emits batch events after successful workspace
migrations. Event names follow `metadata.{entity}.{action}` (e.g.
`metadata.objectMetadata.created`, `metadata.fieldMetadata.updated`).
- **MetadataEventsToDbListener**  
Listens for metadata events and enqueues webhook delivery via
`CallWebhookJobsForMetadataJob`.
- **Event types** (twenty-shared)  
`MetadataEventAction`, `MetadataEventBatch`, and record event types for
create/update/delete.
- **WorkspaceMigrationValidateBuildAndRunService**  
Calls the metadata event emitter after running migrations so all
metadata changes (from any module) emit events from a single place.
- **Create events**  
Sourced from the create action payload (`flatEntity` /
`flatFieldMetadatas`) because `fromToAllFlatEntityMaps` does not provide
a before/after diff for creates. Update/delete events still use the
fromToAllFlatEntityMaps comparison.

### 2. Actor context (second commit)

- **MetadataEventEmitter**  
Accepts optional `actorContext` (`userId`, `workspaceMemberId`) and
includes it on emitted batch events.
- **WorkspaceMigrationValidateBuildAndRunService**  
Passes `actorContext` from the request into the metadata event emitter.
- **Metadata resolvers & services**  
All metadata modules resolve `@AuthUser({ allowUndefined: true })` and
`@AuthUserWorkspaceId()` and pass `userId` and `workspaceMemberId`
through to the migration/event pipeline. Both are optional so
API-key–authenticated requests (no user) still emit events without a
user identity.
  
Shared some questions on Discord about the PR.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: prastoin <paul@twenty.com>
2026-02-12 17:14:25 +00:00
Paul RastoinandGitHub f6b7ab2251 Scalar and universal flat entity transpilers (#17891)
# Introduction

## Use `flatEntityTranspilers.toScalarFlatEntity` in create action
handler

**Changes:**
- Modified
`BaseWorkspaceMigrationRunnerActionHandlerService.insertFlatEntitiesInRepository()`
to transform flat entities using `toScalarFlatEntity()` before database
insertion

**What it does:**
Strips out TypeORM relation objects and metadata-only properties,
ensuring only scalar values (primitives, IDs, dates) are inserted into
the database.

**Benefits:**
- **Type Safety:** Prevents accidental insertion of nested objects that
TypeORM can't persist
- **Consistency:** All 17+ create action handlers automatically benefit
from proper data transformation
- **Single Source of Truth:** Centralized logic for what constitutes a
database-insertable entity
- **Prevents Errors:** Uses entity configuration schema to ensure only
valid properties are included

## Usage
```ts
  protected async insertFlatEntitiesInRepository({
    flatEntities,
    queryRunner,
  }: {
    queryRunner: QueryRunner;
    flatEntities: MetadataFlatEntity<TMetadataName>[];
  }) {
    const metadataEntity =
      ALL_METADATA_ENTITY_BY_METADATA_NAME[this.metadataName];
    const repository = queryRunner.manager.getRepository(metadataEntity);
    const scalarFlatEntities = flatEntities.map((flatEntity) =>
      flatEntityTranspilers.toScalarFlatEntity({
        flatEntity,
        metadataName: this.metadataName,
      }),
    );

    await repository.insert(scalarFlatEntities);
  }
```

## Upcoming refactor
About to completely split the
`packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface.ts`
into three dedicated boilerplate one for each action type `create`
`delete` `update` will provide a better interfacing and typing + will
allow not requiring the user to provide the metadata execute handler as
required
2026-02-12 17:06:55 +00:00
martmullandGitHub 8ffc554c9a Fix twenty sdk build (#17902)
as title
2026-02-12 17:04:07 +00:00
Charles BochetandGitHub d5a8cc2085 Migrate more to Jotai (#17903)
Here we go again
2026-02-12 18:13:31 +01:00
WeikoandGitHub 3075707bf8 Prefil fields widgets to standard app (#17897)
## Context
Prefill the FIELDS widget configuration in standard page layouts during
workspace creation, linking each widget to a dedicated view with
positioned fields organized into sections (via view field groups)

We wanted something very declarative (by manually setting position and
visibility of each field per standard object).
In this PR I've generated all the compute- utils via AI (😨) for
position/visibility, we'll probably want to confirm with the product
which ordering/visibility we want for each standard object but I feel
like this can be merged as it is since it's behind a feature flag and
this will unblock the work on the frontend
2026-02-12 16:54:30 +00:00
e6d3dc07e2 Fix EMFILE: too many open files, watch on macOS (#17901)
## Summary

Fixes `EMFILE: too many open files, watch` crash that most of the team
is hitting on macOS when running `yarn start` or `npx nx start
twenty-server`.

Adds `rimraf dist` before `nest start --watch` in the `start` and
`start:debug` targets, so the watcher starts with a clean output
directory.

## Root cause

The NestJS SWC compiler (`@nestjs/cli@11`) creates **three overlapping
chokidar watchers** when `nest start --watch` runs:

| Watcher | Watches | Purpose | Handles |
|---|---|---|---|
| SWC CLI (`@swc/cli`) | `src/` | Detects file changes → recompiles |
~1,730 |
| NestJS `watchFilesInSrcDir` | `src/` | Workaround: SWC misses new
files | shared with above |
| NestJS `watchFilesInOutDir` | **`dist/`** | Detects compiled `.js` →
restarts server | **~3,548** |

`@nestjs/cli@11` ships with **chokidar v4**, which dropped macOS
`fsevents` support and uses `fs.watch()` instead — creating **one file
descriptor per directory**. Chokidar v3 used a single `fsevents` kernel
subscription per directory tree.

Total: **~5,000+ `fs.watch()` handles**, far exceeding the default macOS
`ulimit -n` of ~2,560.

### Why it broke now

PR #17851 (`15fc850212`) changed the `start` target from `dependsOn:
["build"]` to `dependsOn: ["^build"]`, removing the `rimraf dist && nest
build` pre-step. Without that cleanup, `dist/` accumulated stale
directories from code reorganizations (e.g. `application-layer/` →
`application/` rename), growing to ~3,548 directories vs ~1,730 in a
clean build.

## What this PR does

Adds `rimraf dist &&` before `nest start --watch` in the `start` and
`start:debug` commands. This ensures `dist/` starts empty and only
contains directories matching the current `src/` structure (~1,730),
keeping watcher count in the ~3,400 range.

We still get the startup speed improvement from #17851 (no redundant
full SWC build), since `rimraf dist` is ~instant while the removed `nest
build` step took 30-60s.

## Future considerations

As the codebase grows, even a clean `dist/` will eventually approach the
macOS default `ulimit -n` (~2,560). Options to consider if that happens:

1. **Yarn resolution to force chokidar 3.6.0** — restores `fsevents`,
reducing watcher count from ~5,000 to ~3-5. This is what Vite 7 does
internally. Simple and effective, but pins to an older major version.

2. **Patch `@nestjs/cli`** to skip the `dist/` watcher — the
`watchFilesInOutDir` watcher accounts for ~65% of all handles and only
exists because NestJS doesn't have a direct hook into SWC's
compilation-complete event. Could be removed via `yarn patch`.

3. **Replace `nest start --watch` entirely** — use `node
--watch-path=src` (Node 22+) with `@swc-node/register` for on-the-fly
compilation. Uses a single native watcher regardless of directory count.
Requires rethinking asset copying (`watchAssets` in `nest-cli.json`).

4. **Wait for upstream fix** — NestJS CLI should either re-add
`fsevents` support or use Node's recursive `fs.watch()` option
(available since Node 20) instead of per-directory watchers.

## Test plan

- [ ] Run `npx nx start twenty-server` on macOS — server starts without
EMFILE error
- [ ] Run `npx nx start:debug twenty-server` — debug mode starts without
EMFILE error
- [ ] Edit a `.ts` file while server is running — hot reload still works
- [ ] Run `yarn start` (frontend + backend + worker) — no crashes

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 16:08:35 +00:00
Charles BochetandGitHub 310d13fd17 Migrate twenty ui to jotai (#17900)
## Remove Recoil from twenty-ui

Completely removes the `recoil` dependency from `twenty-ui` by
converting all atoms, hooks, and providers to Jotai equivalents.

### twenty-ui
- `createState` now returns a Jotai `PrimitiveAtom` instead of a Recoil
atom
- `iconsState`, `IconsProvider`, `useIcons` converted to Jotai
(`useSetAtom`, `useAtomValue`)
- `RecoilRootDecorator` now uses Jotai `Provider` (name kept for compat)
- Deleted unused `invalidAvatarUrlsState` (Avatar already uses
`invalidAvatarUrlsAtomV2`)
- Removed `recoil` from `package.json`

### twenty-front
- Created local Recoil `createState` at
`@/ui/utilities/state/utils/createState` for ~112 state files still on
Recoil
- Updated all imports accordingly
- Removed `iconsState` from Recoil snapshot preservation in `useAuth`
(lives in Jotai store now)
2026-02-12 16:43:34 +01:00
Paul RastoinandGitHub 09e48addb2 Fix index field comparison (#17896)
# Introduction
Related https://github.com/twentyhq/core-team-issues/issues/2227

On a field name update side effect leading to an index field mutation it
wouldn't get caught by the builder leading to an index field desync

We should land on a standard pattern regarding the field index either
jsonb or syncableEntity so this would not occur anymore as it would have
been strictly typed
2026-02-12 14:59:33 +00:00
Charles BochetandGitHub d2f8352cb8 Start Jotai Migration (#17893)
## Recoil → Jotai progressive migration: infrastructure +
ChipFieldDisplay

### Benchmark

In the beginning, there was no hope:
<img width="1180" height="948" alt="image"
src="https://github.com/user-attachments/assets/f8635991-52e6-4958-8240-6ba7214132b2"
/>

Then the hope was reborn
<img width="2070" height="948" alt="image"
src="https://github.com/user-attachments/assets/be1182b9-1c8d-4fdc-ab4c-1484ad74449d"
/>



### Approach

We introduce a **V2 state management layer** backed by Jotai that
mirrors the existing Recoil API, enabling component-by-component
migration without a big-bang rewrite.

#### V2 API (Jotai-backed, Recoil-ergonomic)

- `createStateV2` / `createFamilyStateV2` — drop-in replacements for
`createState` / `createFamilyState`, returning wrapper types over Jotai
atoms
- `useRecoilValueV2`, `useRecoilStateV2`, `useFamilyRecoilValueV2`, etc.
— thin wrappers around Jotai's `useAtomValue` / `useAtom` / `useSetAtom`
- A shared `jotaiStore` (via `createStore()`) passed to a
`<JotaiProvider>` wrapping `<RecoilRoot>`, also accessible imperatively
for dual-writes

#### Dual-write bridge for progressive migration

For state shared between migrated and non-migrated components, we use
**dual-write**: writers update both the Recoil atom and the Jotai V2
atom (via `jotaiStore.set()`). This avoids sync components or extra
subscriptions.

Write sites updated: `useUpsertRecordsInStore`, `useSetRecordTableData`,
`ListenRecordUpdatesEffect`, `RecordShowEffect`,
`useLoadRecordIndexStates`, `useUpdateObjectViewOptions`.

#### First migration: ChipFieldDisplay render path

- `useChipFieldDisplay` → reads `recordStoreFamilyStateV2` via
`useFamilyRecoilValueV2` (was `useRecoilValue(recordStoreFamilyState)`)
- `RecordChip` → reads `recordIndexOpenRecordInStateV2` via
`useRecoilValueV2` (was `useRecoilValue(recordIndexOpenRecordInState)`)
- `Avatar` (twenty-ui) and event handlers (`useOpenRecordInCommandMenu`)
left on Recoil — not on the render path / in a different package

#### Pattern for migrating additional state

1. Create V2 atom: `createStateV2` or `createFamilyStateV2`
2. Add `jotaiStore.set(v2Atom, value)` at each write site
3. Switch readers to `useRecoilValueV2(v2Atom)`
4. Once all readers are migrated, remove the Recoil atom and dual-writes

#### Why not jotai-recoil-adapter?

Evaluated
[jotai-recoil-adapter](https://github.com/clockelliptic/jotai-recoil-adapter)
— not production-ready (21 open issues, no React 19, forces providerless
mode, missing types). We built a purpose-built thin layer instead.
2026-02-12 16:05:38 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Abdullah.
08b962b0d2 Bump @vitest/browser-playwright from 4.0.17 to 4.0.18 (#17884)
Bumps
[@vitest/browser-playwright](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser-playwright)
from 4.0.17 to 4.0.18.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitest-dev/vitest/releases"><code>@​vitest/browser-playwright</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v4.0.18</h2>
<h3>   🚀 Experimental Features</h3>
<ul>
<li><strong>experimental</strong>: Add <code>onModuleRunner</code> hook
to <code>worker.init</code>  -  by <a
href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in
<a
href="https://redirect.github.com/vitest-dev/vitest/issues/9286">vitest-dev/vitest#9286</a>
<a href="https://github.com/vitest-dev/vitest/commit/ea837de7d"><!-- raw
HTML omitted -->(ea837)<!-- raw HTML omitted --></a></li>
</ul>
<h3>   🐞 Bug Fixes</h3>
<ul>
<li>Use <code>meta.url</code> in <code>createRequire</code>  -  by <a
href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in
<a
href="https://redirect.github.com/vitest-dev/vitest/issues/9441">vitest-dev/vitest#9441</a>
<a href="https://github.com/vitest-dev/vitest/commit/e057281ca"><!-- raw
HTML omitted -->(e0572)<!-- raw HTML omitted --></a></li>
<li><strong>browser</strong>: Hide injected data-testid attributes  - 
by <a
href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in
<a
href="https://redirect.github.com/vitest-dev/vitest/issues/9503">vitest-dev/vitest#9503</a>
<a href="https://github.com/vitest-dev/vitest/commit/f89899cd8"><!-- raw
HTML omitted -->(f8989)<!-- raw HTML omitted --></a></li>
<li><strong>ui</strong>: Process artifact attachments when generating
HTML reporter  -  by <a
href="https://github.com/macarie"><code>@​macarie</code></a> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/9472">vitest-dev/vitest#9472</a>
<a href="https://github.com/vitest-dev/vitest/commit/225435647"><!-- raw
HTML omitted -->(22543)<!-- raw HTML omitted --></a></li>
</ul>
<h5>    <a
href="https://github.com/vitest-dev/vitest/compare/v4.0.17...v4.0.18">View
changes on GitHub</a></h5>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vitest-dev/vitest/commit/4d3e3c61b9b237447699deab9aca0eb9d6039978"><code>4d3e3c6</code></a>
chore: release v4.0.18</li>
<li>See full diff in <a
href="https://github.com/vitest-dev/vitest/commits/v4.0.18/packages/browser-playwright">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abdullah. <125115953+mabdullahabaid@users.noreply.github.com>
2026-02-12 13:35:22 +00:00
Thomas TrompetteandGitHub 748e614f6c Workflow bug fixes (#17886)
Fixes https://github.com/twentyhq/private-issues/issues/418 
Currency filtering was not properly managed. Since this is a select, we
were doing 'USD' == [USD]. Replacing by contains as for other select
<img width="952" height="552" alt="Capture d’écran 2026-02-12 à 11 08
42"
src="https://github.com/user-attachments/assets/6945f376-c62b-44a3-9d85-dfcb82f5c478"
/>

Fixes https://github.com/twentyhq/twenty/issues/17611
If-else branches not executed has to be marked as skipped. Otherwise,
the iterator will never start the next iteration. It will wait for some
not started nodes.
<img width="673" height="559" alt="Capture d’écran 2026-02-12 à 10 56
45"
src="https://github.com/user-attachments/assets/2b5396d7-a546-43df-a689-39f2686855ec"
/>
2026-02-12 13:27:36 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Abdullah.
2e80391e85 Bump react-loading-skeleton from 3.4.0 to 3.5.0 (#17883)
Bumps
[react-loading-skeleton](https://github.com/dvtng/react-loading-skeleton)
from 3.4.0 to 3.5.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dvtng/react-loading-skeleton/releases">react-loading-skeleton's
releases</a>.</em></p>
<blockquote>
<h2>v3.5.0</h2>
<h3>Features</h3>
<ul>
<li>Add optional <code>customHighlightBackground</code> prop. (<a
href="https://redirect.github.com/dvtng/react-loading-skeleton/issues/233">#233</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/dvtng/react-loading-skeleton/blob/master/CHANGELOG.md">react-loading-skeleton's
changelog</a>.</em></p>
<blockquote>
<h2>3.5.0</h2>
<h3>Features</h3>
<ul>
<li>Add optional <code>customHighlightBackground</code> prop. (<a
href="https://redirect.github.com/dvtng/react-loading-skeleton/issues/233">#233</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dvtng/react-loading-skeleton/commit/f8b040dade9cfaad7e3e6fbc50243d79f508f1ca"><code>f8b040d</code></a>
Merge pull request <a
href="https://redirect.github.com/dvtng/react-loading-skeleton/issues/233">#233</a>
from dvtng/srmagura/highlight-width</li>
<li><a
href="https://github.com/dvtng/react-loading-skeleton/commit/c0973458b88b1f17cca93dcbf4b7c3ffa029d1c9"><code>c097345</code></a>
Update changelog</li>
<li><a
href="https://github.com/dvtng/react-loading-skeleton/commit/d9f88d9eec4142f5c655f793c6926562ae42f203"><code>d9f88d9</code></a>
update README</li>
<li><a
href="https://github.com/dvtng/react-loading-skeleton/commit/b1b27e8a2a053bd3f476e2814bd0949d00b682ea"><code>b1b27e8</code></a>
Custom highlight background</li>
<li><a
href="https://github.com/dvtng/react-loading-skeleton/commit/d8c1492840c273609b0dac8cbe9cc601ca8bad3c"><code>d8c1492</code></a>
fix: Improved the type of styleOptionsToCssProperties (<a
href="https://redirect.github.com/dvtng/react-loading-skeleton/issues/222">#222</a>)</li>
<li>See full diff in <a
href="https://github.com/dvtng/react-loading-skeleton/compare/v3.4.0...v3.5.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=react-loading-skeleton&package-manager=npm_and_yarn&previous-version=3.4.0&new-version=3.5.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>
Co-authored-by: Abdullah. <125115953+mabdullahabaid@users.noreply.github.com>
2026-02-12 12:39:32 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Abdullah
bef70d2217 Bump @types/bytes from 3.1.4 to 3.1.5 (#17882)
Bumps
[@types/bytes](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bytes)
from 3.1.4 to 3.1.5.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bytes">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/bytes&package-manager=npm_and_yarn&previous-version=3.1.4&new-version=3.1.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>
Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-02-12 12:01:18 +00:00
Raphaël BosiandGitHub cb7d0b83a8 [FRONT COMPONENTS] Twenty UI elements generation (#17866)
## PR description

This PR:
- Creates a TypeScript-based extractor that discovers all exported
twenty-ui components by scanning barrel files, extracting
props/slots/events via ts-morph type analysis, and generating the remote
DOM bindings automatically.

- Adds a new ESLint rule which enforces all *Props types in twenty-ui
components to be exported, which is required for the extractor to
discover component prop types. Existing twenty-ui components are updated
to comply with this rule.

- Extends the remote DOM generation to support slots, per-component
events, forwardRef wrappers, and richer property types (array, object,
function)

## Edge cases to fix in another PR

- Icons cannot be rendered inside buttons
- IconButtons throw an error when mounted
- MenuItems are not displayed correctly
- MenuItemNavigate throws on click

## Video Demo


https://github.com/user-attachments/assets/c2ed67cf-6a15-4896-9fec-e83fac0e862b
2026-02-12 12:48:16 +01:00
nitinandGitHub c48ccbca99 Fix widget and front component queries hitting wrong GraphQL endpoint (#17889) 2026-02-12 12:38:06 +01:00
EtienneandGitHub eb52976e91 File v2 - Backfill mimeType and size - command (#17875) 2026-02-12 10:49:10 +00:00
6d1c7c483f i18n - translations (#17887)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-12 11:48:55 +01:00
martmullandGitHub a4ed043d43 Logic function refactorization (#17861)
As title
2026-02-12 11:40:49 +01:00
Charles BochetandGitHub b456f79167 Reduce leak between gql schema (#17878)
## Reduce type leakage between GraphQL schemas

### Why

Twenty runs two separate GraphQL schemas: **core** and **metadata**.
NestJS's `@nestjs/graphql` uses a global `TypeMetadataStorage` that
accumulates all decorated types across all modules. When each schema is
built, every registered type leaks into both schemas regardless of which
module it belongs to.

This means the core schema's generated TypeScript
(`generated/graphql.ts`) contained ~2,700 lines of types that only
belong to the metadata schema (and vice versa). This creates confusion
about type ownership, inflates generated code, and makes it harder to
reason about which API surface each schema actually exposes.

### How

**1. Patch `@nestjs/graphql` to support schema-scoped type resolution**

- **(Already done)** Added a `resolverSchemaScope` option to
`GqlModuleOptions`, allowing each schema to declare a scope (e.g.
`'metadata'`)
- `ResolversExplorerService` now filters resolvers by a
`RESOLVER_SCHEMA_SCOPE` metadata key, so each schema only sees its own
resolvers
- `GraphQLSchemaFactory` now performs a **reachability walk**
(`computeReachableTypes`) starting from scoped resolver return types and
arguments, only including types that are transitively referenced —
handling unions, interfaces, and prototype chains
- Type definition storage and orphaned reference registry are cleared
between schema builds to prevent cross-contamination

**2. Register `ClientConfig` as orphaned type in metadata schema**

Since `ClientConfig` is needed in the metadata schema but not directly
returned by a resolver, it's explicitly declared via
`buildSchemaOptions.orphanedTypes`.

**3. Regenerate frontend types and fix imports**

- `generated/graphql.ts` shrank by ~2,700 lines (types moved to where
they belong)
- `generated-metadata/graphql.ts` gained types like `ClientConfig` that
were previously missing
- ~500 frontend files updated to import from the correct generated file
2026-02-12 10:58:52 +01:00
fe1ec6fd04 i18n - translations (#17881)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-12 04:37:39 +01:00
Abdul RahmanGitHubCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>Devessier
3fb2352c78 Navbar customization followup (#17848)
Addresses review comments from the [first navbar customization
PR](https://github.com/twentyhq/twenty/pull/17728)

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
2026-02-12 02:35:43 +00:00
91bfd45dc5 i18n - translations (#17877)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-11 22:08:58 +01:00
6aca1dd013 Introducing view field group syncable entity (#17867)
## Context
Introduces a new viewFieldGroup entity that allows grouping view fields
into sections (e.g. "General", "Additional", "Other") within a view.

The page layout fields widget needs a way to organize fields into
sections. Today, views have no concept of field grouping. This PR
introduces the viewFieldGroup entity which sits between a view and its
viewFields, enabling section-based organization.

<img width="401" height="724" alt="Layout - V2 (customize visibility)"
src="https://github.com/user-attachments/assets/6376e2ab-44db-42bf-9d2c-758f56f6b548"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-11 22:02:58 +01:00
Thomas TrompetteandGitHub ed66fbd71b Fix event stream does not exists error (#17873)
[Event stream does not
exists](https://twenty-v7.sentry.io/issues/7238297246/events/441b3c0465cf475a8349c7dec40cae2a/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20%21issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=previous-event&sort=date)

Error happens when we are trying to add a query to a non-existing event
stream. In some cases, this is legit. Stream has expired and needs to be
re-created. Then we try to add the query again.

But it should happen only once per tab, and not often. We have a lot of
errors in sentry for each users.

Potential root cause: a race condition between the event stream creation
and the addition of queries:
- event stream id is created in frontend state + creation query is sent
- event stream id is in state so query can be added
- addQuery happens before the stream is actually created in redis. So an
error is returned
- the error makes the event stream re-generated by frontend 
- => the flow starts again until the event stream is actually created
BEFORE the first query is added

Fix: a new state saying if event stream is ready
- event stream id is created in frontend state BUT ready state is falsy
so query is not added yet
- on stream creation on backend side, an initial event is sent, so the
frontend knows the stream is ready
- addQuery can be triggered safely
2026-02-11 20:24:03 +01:00
Charles BochetandGitHub 9bc63a01c9 Generate GQL schema based on applicationId (#17860)
## Add application-scoped GraphQL schema generation

When an application token is used to authenticate, the `/graphql` schema
is now dynamically filtered to only include entities belonging to that
application (plus the Twenty Standard Application). This enables
third-party applications and the SDK to introspect a schema that is
relevant to their scope, rather than seeing the full workspace schema
with all custom objects.

### Changes

- **New `generateApplicationToken` mutation** on the `/metadata`
endpoint, allowing callers to exchange an API key for an
application-scoped JWT token
- **Schema filtering by application** in `WorkspaceSchemaFactory` — when
`request.application` is present (from an application token), flat
entity maps are filtered by `[appId, standardAppId]` before schema
generation
- **Per-app caching** — both the Yoga in-memory cache and Redis cache
now include the `appId` in their keys to avoid serving wrong schemas
- **Consolidated `getSubFlatEntityMapsByApplicationIdsOrThrow`** —
unified the single-ID and multi-ID filtering utilities into one
- **Integration tests** covering token generation (admin + API key auth)
and schema introspection filtering (standard app token excludes custom
objects)

Schema generated on seeds with applicationToken (see that pets is
missing)
<img width="782" height="994" alt="image"
src="https://github.com/user-attachments/assets/82510031-0965-435d-bc26-77c9f5d74e1f"
/>
2026-02-11 20:21:58 +01:00
15fc850212 Remove redundant self-build from Nx targets that compile on the fly (#17851)
## Summary

- Changed 6 Nx targets (`start`, `start:debug`, `typeorm`, `ts-node`,
`database:migrate`, `database:migrate:revert`) from `dependsOn:
["build"]` to `dependsOn: ["^build"]` to eliminate a redundant full SWC
compilation step (~30-60s per invocation).
- These targets all use `nest start --watch`, `ts-node`, or TypeORM's
CLI (which runs under ts-node), so they already compile TypeScript
themselves. The `build` dependency was causing `rimraf dist && nest
build` to run first, only for the target's own command to recompile
everything again.
- Fixed `start:debug` to call `nest start --watch --debug` directly
instead of routing through `nx start --debug`, which would trigger yet
another build cycle.

Note: targets that run pre-compiled code from `dist/` (like
`database:reset`) intentionally keep `dependsOn: ["build"]`.

## Test plan

- [ ] Run `npx nx start twenty-server` and verify the server starts with
only one SWC compilation pass instead of two
- [ ] Run `npx nx start:debug twenty-server` and verify the debugger
attaches correctly
- [ ] Run `npx nx run twenty-server:database:migrate` and verify
migrations run correctly
- [ ] Run `npx nx run twenty-server:database:reset twenty-server` and
verify it still builds before running (unchanged)


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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 17:38:22 +00:00
e6d5df751b i18n - translations (#17874)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-11 18:25:44 +01:00
5b4fed1afe Fix redirect to deleted workspace subdomain after workspace deletion (#17865)
## Summary
- After deleting a workspace, the app was incorrectly redirecting to the
deleted workspace's subdomain (e.g. `myworkspace.ourapp.com/sign-in-up`)
because `signOut()` only performs a client-side React Router navigation
which stays on the current domain.
- Added an explicit `redirectToDefaultDomain()` call after sign out in
the workspace deletion flow, which does a hard browser redirect to the
base domain (e.g. `app.ourapp.com`).

## Test plan
- [ ] Delete a workspace in a multi-workspace environment
- [ ] Verify the browser redirects to the base domain (`app.ourapp.com`)
instead of staying on the deleted workspace's subdomain
- [ ] Verify normal sign-out (without workspace deletion) still works as
expected

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 18:25:27 +01:00
e757a1418d Fix hardcoded colors in 2FA verification screen for dark mode (#17868)
## Summary
- The dash separator and blinking caret in the sign-in 2FA OTP input had
hardcoded `black` and `white` background colors, making them invisible
in dark mode (black on black / white on white).
- Replaced with theme-aware colors (`theme.font.color.light` for the
dash, `theme.font.color.primary` for the caret) to match the existing
settings 2FA component.

## Test plan
- [ ] Open the 2FA verification screen in dark mode and verify the dash
between digit groups is visible
- [ ] Verify the blinking caret is visible in dark mode
- [ ] Confirm both still look correct in light mode

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 18:23:42 +01:00
EtienneandGitHub 2f298307f9 Handle 413 with user friendly message (#17870)
Add friendly message for 413 errors. Currently, 413 errors originate
from the nginx server.
2026-02-11 17:03:22 +00:00
WeikoandGitHub c15536f9f8 Fix page layout seeding for record page layouts (#17871)
## Context
Fix broken record page layout seeding. This was not detected by the CI
because it doesn't have the env variable yet.

Following the same mechanism as labelIdentifier in object for circular
dependency resolution
2026-02-11 16:56:21 +00:00
Paul RastoinandGitHub b2f7c745f8 Solo transaction application synchronization service refactor (#17864)
# Introduction
Refactoring the application sync service to be making a single validate
build and run transaction instead of calling all the services n times
thanks to the universal workspace migration refactor that allow doing so

## What's next
Migrating all below entities to by syncableEntities so they can be
universalised too ( right now they're still calling the services n times
and won't be returned in the workspace migration )
-
[objectPermission](https://github.com/twentyhq/core-team-issues/issues/2223)
-
[fieldPermission](https://github.com/twentyhq/core-team-issues/issues/2224)
-
[permissionFlag](https://github.com/twentyhq/core-team-issues/issues/2225)
2026-02-11 16:30:01 +00:00
0ee63a0525 i18n - translations (#17872)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-11 17:35:20 +01:00
18880f0385 i18n - translations (#17869)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-11 17:05:37 +01:00
Abdul RahmanGitHubCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>Devessier
0902579fbe Feat: Navbar customization (#17728)
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
2026-02-11 15:33:51 +00:00
52e57e70fd Add wildcard documentation for like/ilike/containsIlike filters (#17825)
Add documentation for issue #16602 
After discussing with the team (Thomas),
https://discord.com/channels/1130383047699738754/1443986309436936212 we
decided that updating the documentation.
The issue is In compute-where-condition-parts.ts, the like/ilike cases
pass values directly to SQL without adding % wildcards for api using, so
they behave like exact matches.

This PR updates the documentation regarding the use of `like`, `ilike`
and `containsIlike` filters. Instead of auto-wrapping values with %
wildcards in the backend, we are choosing to leave the control to the
API users (%value% or value%).

<img width="651" height="409" alt="image"
src="https://github.com/user-attachments/assets/b3537af6-a0b0-4fff-a86d-a9ae334d628e"
/>

But I add wildcard for `startsWith` and `endsWith` because these
operators have a fixed semantic meaning.

(To see the results, please refresh the cache first, then restart the
server.)

<img width="878" height="458" alt="image"
src="https://github.com/user-attachments/assets/ab0f4e7c-df50-45ef-b1c8-e43c8881a9a3"
/><img width="482" height="288" alt="image"
src="https://github.com/user-attachments/assets/20dc39ee-2417-4ecc-810e-ea0ead33d803"
/>

---------

Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
2026-02-11 15:32:41 +00:00
Thomas TrompetteandGitHub 1e01f15182 Fix code step and logic function step in workflows (#17856)
- AI still often forgets to update the code step after creating it.
Adding a next step
- Starting by loading logic functions, so it avoids creating code steps
when a function exists
- Fix create complete workflow logic. Should not create code steps
directly
2026-02-11 15:08:23 +00:00
EtienneandGitHub d0c1841f0f File - Migrate avatarUrl > avatarFile on person (data migration + logic) + Attachment data migration (#17752)
- Migration command
    - Check IS_FILES_FIELD_MIGRATED:false
    - Check or create avatarFile field
    - Fetch all people with avatarUrl
           - Move (Copy/move) file in storage
           - Create core.file record
           - Update person record
           
    - bonus : attachment migration  : fullPath > file (same logic)
   
- BE logic
    - Add avatarFile field on person

- FE logic 
   - Adapt logic to upload on/display avatarFile data

The whole imageIdentifier logic will be done later
2026-02-11 15:07:05 +00:00
Thomas TrompetteandGitHub 5be64bf4be Remove guard from find logic functions (#17862)
As title
2026-02-11 14:41:40 +01:00
Paul RastoinandGitHub d9dab75052 Do not throw on corrupted labelFieldMetadataIdentifier (#17859)
# Introduction
As we don't enforce any FK on object labelIdentifierFieldMetadataId we
have some that are either null or pointing to non-existing field
metadata resulting in exception thrown at cache computation lvl

Commenting the exception throw until we've closed
https://github.com/twentyhq/core-team-issues/issues/2172

closes https://github.com/twentyhq/core-team-issues/issues/2221
2026-02-11 12:33:38 +00:00
2237273869 Fix spurious logouts by deduplicating concurrent token renewals (#17858)
## Summary

- When returning to the app after idle (or after a deploy), the expired
access token causes multiple simultaneous GraphQL queries to fail with
`UNAUTHENTICATED`. Previously, each failure independently triggered its
own `renewToken` call with the same refresh token. If **any single**
renewal failed (e.g. server briefly slow after a deploy), the `catch`
handler would nuke the session and redirect to sign-in — even if another
concurrent renewal had already succeeded and written valid tokens.
- This adds a shared `renewalPromise` so that only the first
`UNAUTHENTICATED` error triggers a server-side renewal. All concurrent
callers await the same promise and replay their operations once it
resolves. This eliminates redundant refresh token rotation on the server
and removes the race condition where a straggling failure could log out
an already-renewed session.

## Test plan

- [ ] Log in, wait >30 minutes (or manually expire the access token),
then interact with the app — should silently renew without redirect to
sign-in
- [ ] Open browser DevTools Network tab, trigger the above scenario, and
verify only **one** `renewToken` mutation is sent (instead of N)
- [ ] With server temporarily stopped, verify that a genuine renewal
failure still correctly redirects to sign-in (single
`onUnauthenticatedError` call)
- [ ] Open multiple browser tabs, let access tokens expire, interact in
one tab — other tabs should also recover gracefully on their next
request


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 12:28:52 +00:00
Charles BochetandGitHub 9e21e55db4 Prevent leak between /metadata and /graphql GQL schemas (#17845)
## Fix resolver schema leaking between `/metadata` and `/graphql`
endpoints

### Summary
- Patch `@nestjs/graphql` to support a `resolverSchemaScope` option that
filters resolvers at both schema generation and runtime, preventing
cross-endpoint leaking
- Introduce `@CoreResolver()` and `@MetadataResolver()` decorators to
explicitly scope each resolver to its endpoint
- Move most resolvers (auth, billing, workspace, user, etc.) to the
metadata schema where the frontend expects them; only workflow and
timeline calendar/messaging resolvers remain on `/graphql`
- Fix frontend `SSEQuerySubscribeEffect` to use the default (metadata)
Apollo client instead of the core client

### Problem
NestJS GraphQL's module-based resolver discovery traverses transitive
imports, causing resolvers from `/metadata` modules to leak into the
`/graphql` schema and vice versa. This made the schemas unpredictable
and tightly coupled to module import order.

### Approach
- Added `resolverSchemaScope` to `GqlModuleOptions` via a patch on
`@nestjs/graphql`, filtering in both `filterResolvers()` (runtime
binding) and `getAllCtors()` (schema generation)
- Each resolver is explicitly decorated with `@CoreResolver()` or
`@MetadataResolver()`
- Organized decorator, constant, and type files under `graphql-config/`
following project conventions


Core GQL Schema: (see: no more fields!)
<img width="827" height="894" alt="image"
src="https://github.com/user-attachments/assets/668f3f0f-485e-43f0-92be-4345aeccacb6"
/>

Metadata GQL Schema (see no more getTimelineCalendarEventsFromCompany)
<img width="827" height="894" alt="image"
src="https://github.com/user-attachments/assets/443913db-e5fe-4161-b0e7-4a971cc80a71"
/>
2026-02-11 10:05:24 +00:00
859241d237 Fix merge records page accumulating duplicate morph items (#17705)
## Why
When opening **Merge records** repeatedly, morph items for the same
command-menu page were appended instead of replaced. This could produce
duplicated IDs (e.g. `[A,B,B,A]`) in the merge flow and extra duplicate
tabs in the UI.

## What
- Update `useCommandMenuUpdateNavigationMorphItemsByPage` to replace
page morph items instead of appending existing ones.
- Add regression tests covering:
  - replacing existing morph items for the same page
  - keeping only the latest payload when called twice for the same page

## Notes
I could not run the full workspace tests locally in this environment
because of existing test/build setup issues unrelated to this change
(missing `packages/twenty-front/tsconfig.spec.json` and
`temporal-polyfill` resolution in dependent tasks).

Co-authored-by: remi <remi@labox-apps.com>
2026-02-11 09:55:19 +00:00
Paul RastoinandGitHub 41e413d7b1 Runner metadata events (#17841)
# Introduction
Followup https://github.com/twentyhq/twenty/pull/17622

Refactoring the actions handler to be returning a metadata event
It has to be done incrementally, as if not update metadata event would
be stale as depends on the incremental action execution order and
optimistic application

## What's next
- Builder should consume and regroup each metadata even in order to
batch emit them ( within a single metadata actions batch order matters )
=> refactoring https://github.com/twentyhq/twenty/pull/17622 in order to
consume runner returned metadata events
2026-02-11 09:09:54 +00:00
b0cb29c11c perf: cache ServerBlockNoteEditor instance in transformRichTextV2Value (#17844)
## Summary
- `transformRichTextV2Value` was calling `await
import('@blocknote/server-util')` and `ServerBlockNoteEditor.create()`
on **every single invocation**, adding ~90ms of overhead each time
(visible as the highest avg-duration frame in profiling at 93.49ms).
- Cache the `ServerBlockNoteEditor` instance at module level so the
dynamic import + creation only happens once for the lifetime of the
process.
- Also removes debug timing instrumentation (`performance.now()`,
`calculateInputSize`, `Logger`) that is no longer needed.

## Test plan
- [ ] Verify rich text fields (blocknote/markdown) still round-trip
correctly on create and update
- [ ] Confirm reduced CPU time for `transformRichTextV2Value` in
profiling


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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 09:08:59 +00:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
b4d957306e fix: show user-friendly error message when duplicate invite is sent (#17827)
Fixes #17822

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

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-11 00:06:42 +01:00
9c984b137f Fix phone validation performance by using Set/Map instead of Array lookups (#17843)
## Summary
- **`isValidCountryCode`** was using `Array.includes()` on ~250 country
codes (O(n) per call). Replaced with a `Set.has()` lookup (O(1)).
- **`getCountryCodesForCallingCode`** was iterating all ~250 countries
and calling `getCountryCallingCode()` on each one **every invocation**.
Replaced with a precomputed `Map<callingCode, CountryCode[]>` built once
at module load (O(1) per call).

Both functions are called from `transformPhonesValue` on every phone
field mutation, causing cumulative overhead visible in profiling (p95
self-time ~20-35ms).

## Test plan
- [ ] Verify phone field creation/update still works correctly (country
code validation, calling code resolution)
- [ ] Verify spreadsheet import with phone fields still validates
properly
- [ ] Confirm no regression in `isValidCountryCode` or
`getCountryCodesForCallingCode` behavior


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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 19:56:43 +00:00
05a6a96b13 i18n - translations (#17842)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-10 20:23:26 +01:00
16d414590b Lowercase email (#17775)
Fixes https://github.com/twentyhq/core-team-issues/issues/120 #16976
Partially related to #17711

Frontend check surprisingly was one-liner covering both email input and
import files

Migration script will be done in next commit

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-02-10 18:24:49 +00:00
bc72879c70 Fix commands order for v1.17.0 (#17839)
Order should be

1. We delete all the file records (from core.file table)
2. We add the foreign key file / applicationId (pg constraint)
3. We further update the table structure: fullPath is deleted; path is
created; unicity constraint between workspaceId/applicationId/path is
created (pg constraint)
4. we migrate the workflow steps (this will create files in core.file)
5. we backfill the application package (same)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-10 18:47:59 +01:00
bb037e13dd Fix blocknote.map crash with generic field-level RICH_TEXT_V2 handler (#17834)
## Summary

Fixes #17667

- **Root cause**: `ActivityQueryResultGetterHandler` called
`JSON.parse()` on the `blocknote` field and assumed the result was
always an array. When the stored value was valid JSON but not an array
(e.g., `"{}"`), `blocknote.map()` crashed with `blocknote.map is not a
function`, breaking the entire notes page.
- **Fix**: Replaced the object-level `ActivityQueryResultGetterHandler`
(hardcoded for `note`/`task` only) with a generic field-level
`RichTextV2FieldQueryResultGetterHandler` that safely parses blocknote
JSON with `Array.isArray` validation and gracefully skips malformed
values instead of crashing.
- **Bonus**: The new handler works for **all** objects with
`RICH_TEXT_V2` fields (not just `note`/`task`), following the same
pattern as the existing `FilesFieldQueryResultGetterHandler`.

## Changes

| File | Change |
|------|--------|
| `rich-text-v2-field-query-result-getter.handler.ts` | New field-level
handler with safe blocknote parsing |
| `common-result-getters.service.ts` | Register new handler, remove
`note`/`task` object handlers |
| `activity-query-result-getter.handler.ts` | Deleted (replaced by
field-level handler) |
| `rich-text-v2-field-query-result-getter.handler.spec.ts` | 9 tests
covering all edge cases |

## Test plan

- [x] Unit tests pass (9 tests covering: null blocknote, non-string
blocknote, invalid JSON, non-array JSON like `"{}"`, no images, external
URLs, internal URLs, multiple fields)
- [x] Lint passes (`lint:diff-with-main`)
- [x] Typecheck passes


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 17:07:49 +00:00
Thomas TrompetteandGitHub 17786a3298 SSO - Check if assertion is signed (#17837)
As title
2026-02-10 15:29:34 +00:00
8f91529153 i18n - translations (#17838)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-10 15:24:40 +01:00
e995e84621 [DASHBOARDS] chat agent improvements + new validation layer (#17722)
https://github.com/user-attachments/assets/09550210-76c5-4a40-83b6-9ab785ca10c3



https://github.com/user-attachments/assets/352427fc-0a2a-4f1b-86e9-db99daea0018

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
2026-02-10 13:43:36 +00:00
Paul RastoinandGitHub 148584c730 Migrate all remaining workspace migration create action to universal (#17836)
# Introduction
Completely finalize the universal migration at builder and runner
levels.
Which mean that from now on the builder only compares
`universalFlatEntity` and produces universal workspace migration that
the runner ingest

## What's done
Migrated all create metadata remaining for
- agent
- skill
- commandMenuItem
- navigationMenuItem
- fieldMetadata
- objectMetadata
- view
- viewField
- viewFilter
- viewGroup
- viewFilterGroup
- index
- logicFunction
- role
- roleTarget
- pageLayout
- pageLayoutTab
- pageLayoutWidget
- rowLevelPermissionPredicate
- rowLevelPermissionPredicateGroup
- frontComponent
2026-02-10 13:39:37 +00:00
Charles BochetandGitHub b7ff587b5e Query cache instead of database for event listener webhook, logicFunction, triggers (#17824)
## Fix
Replaced direct database queries with existing flat entity map caches
for the two that already have cache infrastructure:
- **CallDatabaseEventTriggerJobsJob** - now uses flatLogicFunctionMaps
cache via WorkspaceCacheService.getOrRecompute(), filtering in memory
for non-deleted logic functions with databaseEventTriggerSettings.
- **CallWebhookJobsJob** - now uses flatWebhookMaps cache via
WorkspaceCacheService.getOrRecompute(), filtering in memory for webhooks
matching the event's operations.
- Note: **WorkflowDatabaseEventTriggerListener** - left as-is since
WorkflowAutomatedTriggerWorkspaceEntity extends BaseWorkspaceEntity (not
SyncableEntity) and has no flat entity map cache infrastructure yet.
2026-02-10 13:14:06 +00:00
2e77a68daf i18n - translations (#17835)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-10 12:44:49 +01:00
BOHEUSandGitHub 382746dd52 Clarify forms usage (#17772)
Fixes https://github.com/twentyhq/core-team-issues/issues/1704
2026-02-10 12:35:37 +01:00
WeikoandGitHub 202a130ac8 revert releasing RLS (#17809) (#17832) 2026-02-10 11:48:59 +01:00
Thomas des FrancsandGitHub 24c6d3fef3 Fix row-level permissions 'Where' left spacing (#17831)
<img width="634" height="289" alt="image"
src="https://github.com/user-attachments/assets/c8fe85e1-ecac-4b57-be36-6c4a5e426c60"
/>
2026-02-10 11:48:32 +01:00
martmullandGitHub 52fe21c04b Fix ci + improvements (#17795)
as title
2026-02-10 11:48:10 +01:00
Paul RastoinandGitHub ee0474e287 Remove standard ids (#17833)
Deadcode
2026-02-10 10:37:55 +00:00
Thomas des FrancsandGitHub 3f202c5c6a Fix downgrade to Pro CTA icon and wording (#17829)
## Summary
- Fixes the downgrade CTA icon for `Switch to Pro` from an up arrow to a
down arrow.
- Fixes wording generation so no `undefined` suffix is concatenated in
downgrade/plan-switch confirmation copy.

## Before

<img width="717" height="535" alt="image"
src="https://github.com/user-attachments/assets/f934d130-c9fa-46bb-b677-88edf64bc623"
/>

<img width="508" height="325" alt="image"
src="https://github.com/user-attachments/assets/4c776922-a169-4543-b791-fcefc093fa7f"
/>

## Testing
- Not run locally in this environment.
2026-02-10 09:41:48 +00:00
a63b31931f Extract SecureHttpClientService into its own module (#17828)
## Summary

- **Extract `SecureHttpClientService`** from `tool` module into a
dedicated `core-modules/secure-http-client/` module with proper NestJS
module encapsulation
- **Fix module hygiene**: 12 modules that incorrectly listed
`SecureHttpClientService` as a direct provider now properly import
`SecureHttpClientModule`
- **Add structured logging** for outbound HTTP requests with
workspace/user context (for GuardDuty alert correlation)
- **Rename type files** to follow one-export-per-file convention
(`get-secure-axios-adapter.types.ts` ->
`secure-adapter-dependencies.type.ts`, new
`outbound-request-context.type.ts` / `outbound-request-source.type.ts`)

### Why

`SecureHttpClientService` is a cross-cutting concern (used by auth,
captcha, file upload, geo-map, telemetry, admin-panel, REST API, contact
creation, webhooks, and workflow tools) but was bundled inside the
`tool` module. Most consumers worked around this by listing it as a
direct provider instead of importing a module, which is fragile and not
idiomatic NestJS.

## Test plan

- [x] All 60 unit tests pass (`secure-http-client.service.spec.ts`,
`get-secure-axios-adapter.util.spec.ts`, `is-private-ip.util.spec.ts`)
- [x] Related module tests pass (admin-panel, contact-creation, tool)
- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [x] Server compiles and bootstraps successfully


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 09:16:51 +00:00
3c2aec1894 i18n - translations (#17830)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-10 10:32:49 +01:00
Baptiste DevessierandGitHub b250de216e Remove the code of old show pages (#17811)
Closes https://github.com/twentyhq/core-team-issues/issues/2213
2026-02-10 09:10:00 +00:00
2d42b0a726 - Added accomodation of mobile navigation bar in command menu (#17419)
Fix for #16743 

Added styles to accommodate for mobile navigation bar for better
visibility and better access of the command menu buttons.

## CommandMenuAskAIPage.tsx (Not shown in the issue)
### Before
<img width="1100" height="1792" alt="screenshot-2026-01-24_17-22-45"
src="https://github.com/user-attachments/assets/8a337ffe-9dfc-4e52-b744-622cc6989ae4"
/>

### After 
Since the component here already had some padding on it the mobile
navigation bar offset was lesser than it is in other places.
<img width="1092" height="1778" alt="screenshot-2026-01-24_17-28-33"
src="https://github.com/user-attachments/assets/00a217f1-d98e-4303-bdc1-df112d71a553"
/>


## CommandMenuWorkflowEditStep.tsx
### Before
As mentioned in the issue

### After
<img width="755" height="1061" alt="image"
src="https://github.com/user-attachments/assets/487d0b20-f544-4e85-99d5-eb5add4b2cb4"
/>

## CommandMenuWorkflowRunViewStep.tsx
### Before
As shown in the issue

### After
<img width="1102" height="1784" alt="screenshot-2026-01-24_17-19-31"
src="https://github.com/user-attachments/assets/a95773e3-5f61-46d0-93cb-e678e087a42f"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-09 23:48:45 +01:00
Abdullah.andGitHub 7e5e800f63 fix: remove axios stale entry by ensuring transitive version of axios is also latest (#17823)
Last upgrade PR left stale entries for transitive import of Axios. 

Raising this PR to fix and ensure Axios 1.13.5 is the de-facto in
yarn.lock for consistency.
2026-02-09 23:23:06 +01:00
Baptiste DevessierandGitHub 8c70a875f8 fix: set a margin even for empty side-column widgets (#17806)
| | Read | Edit |
|--------|--------|--------|
| **Before** | <img width="762" height="2144" alt="CleanShot 2026-02-09
at 16 07 06@2x"
src="https://github.com/user-attachments/assets/d93b5928-2c7b-438a-b16a-d17a434276fc"
/> | <img width="762" height="2144" alt="CleanShot 2026-02-09 at 16 07
01@2x"
src="https://github.com/user-attachments/assets/c5fb94db-947d-4832-9bc2-7942faa290eb"
/> |
| **After** | <img width="762" height="2144" alt="CleanShot 2026-02-09
at 16 06 33@2x"
src="https://github.com/user-attachments/assets/761352f3-cf18-4f41-98f0-fb0b4367f486"
/> | <img width="762" height="2144" alt="CleanShot 2026-02-09 at 16 06
41@2x"
src="https://github.com/user-attachments/assets/d1780671-1043-4087-860c-020343fcda87"
/> |


Closes https://github.com/twentyhq/core-team-issues/issues/2193
2026-02-09 22:59:00 +01:00
Abdullah.andGitHub a40612ef9e fix: axios related dependabot alerts (#17821)
Resolves [Dependabot Alert
436](https://github.com/twentyhq/twenty/security/dependabot/436),
[Dependabot Alert
437](https://github.com/twentyhq/twenty/security/dependabot/437) and
[Dependabot Alert
438](https://github.com/twentyhq/twenty/security/dependabot/438).
2026-02-09 22:56:34 +01:00
neo773andGitHub 1979e013e9 Google OAuth check real permissions before creating channels (#17714) 2026-02-09 22:49:03 +01:00
3747005fd5 i18n - translations (#17820)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-09 22:43:06 +01:00
neo773andGitHub 268cc40d80 Show accounts with pending message channel configuration (#17770) 2026-02-09 22:29:27 +01:00
MarieGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Félix Malfait
11fd1a41f3 [Fix] fix timelineActivities on notes and tasks (#17814)
[Fixes
sentry](https://twenty-v7.sentry.io/issues/7238708876/?environment=prod&environment=prod-eu&project=4507072499810304&query=Field%20metadata%20for%20field&referrer=issue-stream&sort=date):
_Field metadata for field "targetTargetPersonId" is missing in object
metadata timelineActivity_

Regression caused by migration of note and task targets - fields were
renamed from personId to targetPersonId, impacting the event names,
while we still expected them in their previous shape.

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
2026-02-09 20:47:43 +00:00
05eca08ad2 releasing RLS (#17809)
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-02-09 21:56:41 +01:00
Abdullah.andGitHub ef1464db73 Improve AI-chat design to match the one provided in Figma (#17816)
Fixed colors, gaps, component nesting, alignment to match Figma.
2026-02-09 18:28:25 +00:00
neo773andGitHub 5ccf18fdf5 Add handling for internal_failure in Gmail API error parser (#17718)
fixes TWENTY-SERVER-D3X
2026-02-09 18:02:09 +00:00
9e6f19d16e Fix RLS creation logic (#17815)
Fix after resolveEntityRelationUniversalIdentifiers introduction
```
FlatEntityMapsException [Error]: Could not find rowLevelPermissionPredicateGroup for given rowLevelPermissionPredicateGroupId
        at resolveEntityRelationUniversalIdentifiers
```

- Creating util for RLS flat entity creation to be aligned with other
entities
- Progressively update the flat entity maps as each new group is built,
following the same optimistic pattern used by
computeUniversalFlatEntityMapsFromTo in the validate build and run. The
updated maps are now passed to computePredicateOperations so predicates
can also resolve references to the newly created groups.
- Adding more coverage

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-02-09 19:19:23 +01:00
Charles BochetandGitHub 987ed845ac Release Twenty SDK 0.5.0 (#17818)
As per title + create-twenty-app
2026-02-09 19:16:24 +01:00
f51291704d [FRONT COMPONENTS] Retrieve the front components from the backend (#17813)
- Pass the auth token to worker
- Fetch the file from the rest API with the auth token
- Create the blob url
- Update the stories to pass a fake token which will be ignored

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-02-09 19:10:29 +01:00
Charles BochetandGitHub 37b9a55382 Migrate metrics to prometheus (#17810)
<img width="1316" height="611" alt="image"
src="https://github.com/user-attachments/assets/277a63ed-2a8b-41ff-be78-281de8891579"
/>
2026-02-09 19:09:13 +01:00
aa7973e5b8 i18n - translations (#17817)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-09 19:08:48 +01:00
8c951d3623 Migrate Views-xxx Index Field Object Skill to be fully universal ( all actions and metadata runner and builder ) + all metadata update actions runner (#17687)
# What this PR does
Overall naming `universal` versus `flat` is not always the most updated
and so on
Will make a big cleaning tour after I've finished the whole migration
Migrating all `view` and ( filter fields etc ) `field` `object` `index`
to the universal pattern on all `services`, `builder` and `runner`
levels

## Universal and flat optimistic tooling

`addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow`
and its delete counterpart maintain the consistency of
`UniversalFlatEntityMaps` when an entity is created or removed. Beyond
inserting/removing the entity from its own maps, they walk through
`ALL_UNIVERSAL_METADATA_RELATIONS` to update the **aggregator arrays**
on related parent entities — the add appends the new entity's
`universalIdentifier` to the parent's aggregator (e.g. a new viewField's
identifier gets appended to its parent view's
`viewFieldUniversalIdentifiers`), and the delete filters it out. This
keeps the maps in sync so that diff computations and relation lookups
remain accurate throughout the migration building process.

## ALL_UNIVERSAL_METADATA_RELATIONS
`ALL_UNIVERSAL_METADATA_RELATIONS` is the universal counterpart of
`ALL_METADATA_RELATIONS`. It maps each metadata entity to its
many-to-one and one-to-many relations using universal foreign keys
(`*UniversalIdentifier`) instead of database IDs (`*Id`). This allows
migration actions to reference related entities in a workspace-agnostic
way. Relations that are workspace-specific (e.g. `workspace`,
`dataSource`, `userWorkspace`) are set to `null` and skipped during
resolution.

## `workspaceMigrationCreateIdEnrichment`
Reserved to API metadata ( will be able to validate at app installation
lvl )
- Workspace migration `create` actions now carry an optional `id` (and
`fieldIdByUniversalIdentifier` for object/field actions) so that
caller-provided IDs flow through the entire build-validate-run pipeline.
- New `enrichCreateWorkspaceMigrationActionsWithIds` utility resolves
`universalIdentifier → id` mappings after the builder runs and injects
them into the migration actions before the runner persists entities.
- Runner action handlers use the provided IDs instead of generating new
UUIDs, enabling deterministic entity creation for synchronization
workflows.


## `resolveUniversalUpdateRelationIdentifiersToIds`
`resolveUniversalUpdateRelationIdentifiersToIds` converts universal
identifiers (workspace-agnostic, stable keys) in a migration update
payload into concrete database UUIDs, so the update can be applied to a
specific workspace.

It iterates over the many-to-one relations defined in
`ALL_UNIVERSAL_METADATA_RELATIONS` for the given entity type, replaces
each `*UniversalIdentifier` property with its corresponding `*Id` by
looking up the target entity in `allFlatEntityMaps`, and throws if a
non-null identifier can't be resolved.

Used by all `update` action handlers in
`transpileUniversalActionToFlatAction`, avoiding duplicated resolution
logic across handlers.

## What this PR does not
- Migrating twenty-standard declaration to universal
- Migrating all the inputs transpilers to universal
- Migrating all metadata to be fully universal ( we still need to
de-scope the type of all of them and refactor their validator very close
)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-09 19:02:38 +01:00
2a76e1791e Allow AI to update code steps (#17761)
https://github.com/user-attachments/assets/35ac5c23-4d5c-4c86-8233-7b58fbeb5a27

- add tool
- move resolver code into a service

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-02-09 15:40:44 +00:00
Weiko 4ae375308f Revert "Releasing RLS"
This reverts commit c01853c349.
2026-02-09 16:52:58 +01:00
Weiko c01853c349 Releasing RLS 2026-02-09 16:52:12 +01:00
MarieandGitHub 21b2b65dbe [Fix] fix relations in apps (#17791)
When syncing an app with relation multiple times, it would fail because
the relation fields could not be identified due to universalIdentifier
being overwritten at field cretion
2026-02-09 15:22:14 +00:00
Raphaël BosiandGitHub c6d04ccced [FRONT COMPONENTS] Serialize events through the worker boundary (#17767)
- Introduces a SerializedEventData type that captures only serializable
properties from DOM/React events
- Updates `wrapEventHandler` in the host component registry to serialize
native events via serializeEvent() before passing them across the worker
boundary via postMessage, avoiding circular references and non-cloneable
DOM nodes
- Updates generated remote element event signatures to use
RemoteEvent<SerializedEventData> instead of bare RemoteEvent, giving
front-component authors typed access to event details
2026-02-09 16:22:54 +01:00
c18726d712 Fix captcha validation failing due to missing URL in secure axios adapter (#17807)
## Summary

- Fixes login failing with `Error: URL is required` when captcha (Google
reCAPTCHA or Turnstile) is enabled
- The secure axios adapter (`getSecureAxiosAdapter`) checked
`config.url` before `baseURL` resolution. In Axios, `baseURL + url`
combination happens inside the default HTTP adapter, not before custom
adapters are called. When captcha drivers configure a `baseURL` and call
`.post('', data)`, the empty string url was incorrectly treated as
missing.
- The adapter now resolves `baseURL` + `url` itself before validation,
matching the default Axios HTTP adapter behavior

## Test plan

- [x] Existing unit tests (23 tests) all pass
- [ ] Verify login works with captcha enabled (`CAPTCHA_DRIVER` set to
`google-recaptcha` or `turnstile`)


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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 16:20:41 +01:00
WeikoandGitHub 96bc3594a3 Serve frontend components (#17798)
## Context
Ability to serve frontend component

See:
```typescript
curl -i 'http://localhost:3000/rest/front-components/35063b3f-bc4c-4358-8966-7762677802a3' \
--header 'Authorization: Bearer eyJhb...'
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/javascript
Date: Mon, 09 Feb 2026 10:28:17 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

// react-globals:react/jsx-runtime
var jsx = globalThis.jsx;
var jsxs = globalThis.jsxs;
var Fragment = globalThis.React.Fragment;

// src/front-components/test.tsx
var RemoteComponents = globalThis.RemoteComponents;
var Component = () => {
  return /* @__PURE__ */ jsxs(RemoteComponents.HtmlDiv, { style: { padding: "20px", fontFamily: "sans-serif" }, children: [
    /* @__PURE__ */ jsx(RemoteComponents.HtmlH1, { children: "My new component!" }),
    /* @__PURE__ */ jsx(RemoteComponents.HtmlP, { children: "This is your front component: test" })
  ] });
};
var test_default = globalThis.jsx(Component, {});
export {
  test_default as default
};
//# sourceMappingURL=test.mjs.map
```

readFile_v2 returns a Node.js Stream object (Readable). Here we are
using stream pipeline which connects the readable stream (file) to the
writable stream (HTTP response) which efficiently streams the file
content directly to the HTTP response without loading the entire file
into memory. (in chunks, handling backpressure and closing the
connection when the file is fully sent)
2026-02-09 14:52:14 +00:00
Baptiste DevessierandGitHub 617f634b6d Use backend types in Record Page Layouts' frontend (#17794) 2026-02-09 14:36:02 +00:00
8e977912e2 i18n - translations (#17804)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-09 15:44:40 +01:00
4cdb7d61b2 Redesign AI chat and add pre-existing prompts. (#17787)
Redesigned AI-chat based on the following provided design.

<p align="center">
<img
src="https://github.com/user-attachments/assets/f10ebbd2-9ee9-402f-b246-6e8f8cedbd53"
width="225" />
</p>

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2026-02-09 15:29:32 +01:00
Raphaël BosiandGitHub 7fbd50e5a0 [FRONT COMPONENTS] Create Icon component in twenty ui (#17797)
This will be used in the remote dom. We need a component which can take
a icon name as a parameter and return an icon component.
2026-02-09 13:57:22 +00:00
Abdullah.andGitHub 0b4cebfe0e Resolve tar related dependabot alerts in the application-layer. (#17801)
Resolves [Dependabot Alert
424](https://github.com/twentyhq/twenty/security/dependabot/424),
[Dependabot Alert
425](https://github.com/twentyhq/twenty/security/dependabot/425) and
[Dependabot Alert
426](https://github.com/twentyhq/twenty/security/dependabot/426).
2026-02-09 14:41:26 +01:00
Charles BochetandGitHub d7c28d6455 Fix file constraint migration (#17796)
## Summary

- Wrap `AddFileEntityUniqueConstraint` migration in a SAVEPOINT
try-catch so it doesn't break when duplicate `(workspaceId,
applicationId, path)` rows exist in `core.file`
- Extend `DeleteFileRecordsCommand` upgrade command to add the unique
constraint after cleaning up file records
2026-02-09 14:40:35 +01:00
Raphaël BosiandGitHub 2b29918bf8 [FRONT COMPONENTS] Navigate from the remote (#17762)
## PR Description

This PR:
- Introduces a `FrontComponentHostCommunicationApi`, which allows us to
pass functions to be executed from the worker
- Exposes a navigate function from the host to the front component
remote workers, enabling SDK components to trigger in-app navigation
- Gets rid of `useSyncExternalStore` and makes the execution context
reactive without it
- Refactors and improves the `esbuild` plugin system

## Video


https://github.com/user-attachments/assets/7b26a1c2-f85f-4898-a71d-f60c70e61711
2026-02-09 14:38:35 +01:00
2106f46e9e i18n - translations (#17803)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-09 14:35:41 +01:00
MarieandGitHub 1edce5088c Flush cache before and after upgrade for self-hosts (#17800)
as per title
2026-02-09 14:35:13 +01:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
3216b634a3 feat: improve AI chat - system prompt, tool output, context window display (#17769)
⚠️ **AI-generated PR — not ready for review** ⚠️

cc @FelixMalfait

---

## Changes

### System prompt improvements
- Explicit skill-before-tools workflow to prevent the model from calling
tools without loading the matching skill first
- Data efficiency guidance (default small limits, use filters)
- Pluralized `load_skill` → `load_skills` for consistency with
`load_tools`

### Token usage reduction
- Output serialization layer: strips null/undefined/empty values from
tool results
- Lowered default `find_*` limit from 100 → 10, max from 1000 → 100

### System object tool generation
- System objects (calendar events, messages, etc.) now generate AI tools
- Only workflow-related and favorite-related objects are excluded

### Context window display fix
- **Bug**: UI compared cumulative tokens (sum of all turns) against
single-request context window → showed 100% after a few turns
- **Fix**: Track `conversationSize` (last step's `inputTokens`) which
represents the actual conversation history size sent to the model
- New `conversationSize` column on thread entity with migration

### Workspace AI instructions
- Support for custom workspace-level AI instructions

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-09 14:26:02 +01:00
Abdullah.andGitHub 6c7c389785 fix: webpack related dependabot alerts (#17792)
Resolves [Dependabot Alert
422](https://github.com/twentyhq/twenty/security/dependabot/422) and
[Dependabot Alert
423](https://github.com/twentyhq/twenty/security/dependabot/423).
2026-02-09 13:12:05 +01:00
martmullandGitHub 9162685b2e Reorganize logic function files (#17766)
reorganize according to

<img width="1243" height="725" alt="Pasted Graphic"
src="https://github.com/user-attachments/assets/ba65dd10-8eec-4b13-ad49-9726edd3b79c"
/>

Not working yet
2026-02-09 12:36:39 +01:00
bf4c348c8b i18n - translations (#17790)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-09 11:18:24 +01:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Etienne
ece265c6e4 Harden local file storage driver path resolution (#17783)
## Summary

- Normalize all file paths with `path.resolve` instead of `join` to
properly handle `..` segments in file path inputs
- Add `assertPathIsWithinStorage` guard on all write, delete, move,
copy, and existence-check operations
- Introduce `ACCESS_DENIED` exception code with i18n-ready user-friendly
message
- Read path already had realpath-based validation; updated its error
code to `ACCESS_DENIED` for consistency

## Test plan

- [x] Typecheck passes
- [x] Lint passes
- [x] Manual: verify file upload/download still works with valid paths
- [x] Manual: verify `../` in file paths is rejected with ACCESS_DENIED


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Etienne <etiennejouan@users.noreply.github.com>
2026-02-09 09:54:10 +00:00
15f09736b2 Fix importing people with mixed-case domain URL failing to match company (#17774)
Fix #17711 
### Reproduce steps
1. import this xlsx file
[test-import.xlsx](https://github.com/user-attachments/files/25156642/test-import.xlsx)

2. import company worksheet at first and then import people worksheet
3. you will see an error <img width="324" height="101"
alt="Snipaste_2026-02-07_19-22-04"
src="https://github.com/user-attachments/assets/bf2703da-a57a-4795-805b-6ddcc689c621"
/>

And I found neither the frontend nor the backend applies lowercase
normalization to the query value. Although the standard UI input always
displays company links in lowercase, user might mixed the case in their
xlsx/csv bulk import. So I did a simple check in frontend.

### Additional findings:
1. Email has the same case sensitivity issue when opportunities import.
You can test same as in test-import.xlsx file (I created a opportunities
worksheet. It will have same issue: <img width="330" height="101"
alt="email error"
src="https://github.com/user-attachments/assets/db36b19b-2abe-4c61-a661-f8d1eb357a5e"
/>

2. API also has: I also tested via the GraphQL API Playground and
confirmed that createOnePerson with a mixed-case URL fails to find an
existing company. <img width="1419" height="513"
alt="Snipaste_2026-02-07_23-33-56"
src="https://github.com/user-attachments/assets/c189f1fd-9793-42d5-a1a9-616cffd2592e"
/>

### Approach:
1. Only change it in frontend, and I will add email check later; (my
current commit)
2. Or backend: Normalize values in computeUniqueConstraintCondition in
twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts
(which handles connect.where queries, and this will cover xlsx import
and api import: createone, createmany, updatemany, updateone. And much
better for future extension.

(Personally, I'd prefer the backend approach. But I'd love to hear your
thoughts on which approach you'd prefer, and whether my analysis is on
the right track.

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-02-09 09:38:09 +00:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
bade5289b6 Fix Cloudflare webhook guard validation logic (#17780)
## Summary

- Restructures the `CloudflareSecretMatchGuard` to properly validate the
`cf-webhook-auth` header before performing the comparison — checks for
missing, empty, or mismatched-length values first
- Removes `@ts-expect-error` workarounds with explicit typed header
access
- Expands test coverage with additional edge cases (missing header,
wrong value, empty string, length mismatch)

## Test plan

- [x] Unit tests pass (6 tests covering matching, missing config,
missing header, wrong value, empty string, length mismatch)


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-09 08:23:57 +00:00
497230a052 i18n - translations (#17789)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-09 00:22:32 +01:00
WeikoandGitHub 9aa63f7ddc Add sync front component (#17748)
## Context
Allow twenty apps to sync front components
2026-02-08 23:00:23 +00:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
40d7e740ef Add token type validation and remove dead code in JWT verification (#17784)
## Summary

- Removes unreachable dead code in `verifyJwtToken` — a legacy API key
verification block where the condition (`!payload.type && type ===
API_KEY`) was logically impossible. Also removes the now-unused
`isLegacyApiKey` parameter and `generateAppSecretLegacy` method.
- Adds explicit token type validation after JWT decode in
`verifyLoginToken`, `verifyRefreshToken`, and `verifyTransientToken`.
Each function now rejects tokens whose `type` field doesn't match what's
expected (defense-in-depth — the HMAC secret already binds the type, but
this makes the contract explicit).

## Test plan

- [x] Updated existing specs for login-token, refresh-token, renew-token
services — all 16 tests passing
- [x] Lint clean

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-08 19:44:42 +00:00
7402edb887 Remove unused GraphQL throttler plugin and graphql-rate-limit dependency (#17785)
## Summary

- Deletes `use-throttler.ts` — an envelop plugin that was never
registered in the GraphQL config plugin chain (dead code since
introduction)
- Removes the `graphql-rate-limit` dependency from `package.json` (only
consumer was the deleted file)

API rate limiting continues to work via `ThrottlerService` in the query
runner layer (for API key auth). Global coverage should be handled at
the infrastructure level (Cloudflare).

## Test plan

- [x] Confirmed no imports of `useThrottler`, `ThrottlerPluginOptions`,
or `UnauthenticatedError` from this file exist anywhere in the codebase
- [x] `graphql-rate-limit` has no other consumers

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 19:52:59 +01:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
d7132b35d3 Centralize outbound HTTP requests through SecureHttpClientService (#17779)
## Summary

- Migrates all direct `axios` and `@nestjs/axios` `HttpService` usages
across the server to go through `SecureHttpClientService`, which
conditionally applies SSRF protection based on the
`OUTBOUND_HTTP_SAFE_MODE_ENABLED` config flag
- `SecureHttpClientService.getHttpClient()` now accepts optional
`AxiosRequestConfig` (e.g., `baseURL`) so callers can configure their
client while still getting protection
- Adds `getInternalHttpClient()` for trusted same-server requests (e.g.,
REST-to-GraphQL proxy, code-interpreter downloading internal files)
- Renames `getSecureAdapter` to `getSecureAxiosAdapter` for clarity
- Captcha drivers now receive a pre-configured `AxiosInstance` from the
module factory instead of creating their own

## Migrated services

| Service | Previous | Risk level |
|---------|----------|-----------|
| `file-upload.service` | `HttpService` | High (user-provided image
URLs) |
| `code-interpreter-tool` | `HttpService` + direct adapter | High
(user-provided file URLs) |
| `search-help-center-tool` | `axios.post()` | Low (hardcoded endpoints)
|
| `http-tool` | Already migrated | High (user-provided URLs) |
| `admin-panel.service` | `axios.get()` | Low (Docker Hub API) |
| `sign-in-up.service` | `HttpService` | Medium (logo URL validation) |
| `google-apis-scopes` | `HttpService` | Low (Google API) |
| `geo-map.service` | `HttpService` | Low (Google Maps API) |
| `telemetry.service` | `HttpService` | Low (telemetry endpoint) |
| `rest-api.service` | `HttpService` | Internal (uses
`getInternalHttpClient`) |
| `create-company.service` | `axios.create()` | Low (Twenty companies
API) |
| `google-recaptcha.driver` | `axios.create()` | Low (Google reCAPTCHA)
|
| `turnstile.driver` | `axios.create()` | Low (Cloudflare Turnstile) |

## Test plan

- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [x] Admin panel unit tests pass
- [x] Secure adapter unit tests pass

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-08 19:52:35 +01:00
bc6268bb29 Rename REFRESH_TOKEN_COOL_DOWN to REFRESH_TOKEN_REUSE_GRACE_PERIOD and anchor the grace window (#17782)
## Summary

- Renames `REFRESH_TOKEN_COOL_DOWN` to
`REFRESH_TOKEN_REUSE_GRACE_PERIOD` — the old name was misleading and
suggested a security mechanism rather than what it actually is: a grace
period for concurrent refresh token use (e.g. two browser tabs
refreshing simultaneously).
- Makes the token revocation in `renew-token.service.ts` conditional
(`revokedAt: IsNull()`), so if the token was already revoked by a
concurrent request, the original `revokedAt` timestamp is preserved and
the grace window stays anchored.
- Updates comments and config description to clarify intent.

## Test plan

- [x] Existing unit tests updated and passing
(`refresh-token.service.spec.ts`, `renew-token.service.spec.ts`)
- [x] Lint clean

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 18:15:37 +01:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
c44d61f324 Validate timezone input in group-by date queries (#17777)
## Summary

- Validates that the timezone parameter in group-by date expressions is
a recognized IANA timezone
- Adds SQL string literal escaping as a defense-in-depth measure for the
timezone value interpolated into SQL expressions
- Moves the `IANA_TIME_ZONES` constant to `twenty-shared` so it can be
reused across frontend and server packages
- Adds `INVALID_TIMEZONE` error code mapped to 400 Bad Request in both
GraphQL and REST API exception handlers

## Test plan

- [x] Unit tests for `validateIanaTimeZone` (valid IANA zones, fixed
offsets, rejects invalid strings)
- [x] Unit tests for `escapeSqlStringLiteral`
- [x] Integration tests for `getGroupByExpression` covering timezone
validation and granularity handling


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-08 16:06:19 +00:00
WeikoandGitHub cc86b9acae Fix record page layout BE (#17758)
## Context
- Add missing conditional display
- Set default tab as null for most of the page layout, letting the FE
handle that
- Fix duplicate "Note" widget in both task and note pages
- Simplify typing, removing redundant layoutName
2026-02-06 18:34:35 +00:00
30cbf0e40e i18n - translations (#17768)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-06 18:27:52 +01:00
Baptiste DevessierandGitHub 4acca5b10d Store record page layouts in a global state (#17759) 2026-02-06 17:12:00 +00:00
Paul RastoinandGitHub 3dc5b162c7 Spread in parent and requires FlatEntity.__universal (#17753)
# Introduction
Requiring the spreaded `__universal` record that aggregates all the
universal identifier ( relations fk and aggregators ) of an entity to
its root

It's blockin for https://github.com/twentyhq/twenty/pull/17687 to be
finalized because if we don't we would have to migrated all related
entities at once in order for them to always have the universal
properties

## `resolveEntityRelationUniversalIdentifiers`
Introduced `resolveEntityRelationUniversalIdentifiers` a centralized
utility that resolves foreign key IDs to universal identifiers using
ALL_METADATA_RELATIONS metadata. It provides strict typing for both
input (foreign keys) and output (universal identifiers), with
nullability dynamically inferred from entity relation types.
Strictly and dynamically typed for both output and input

To do so added a new type and const/runtime grain to
ALL_METADATA_RELATIONS `isNullable`to many-to-one entries, derived from
the entity relation property types. And fixed incorrectly typed typeorm
entities

### Usage
```ts
  const {
    availabilityObjectMetadataUniversalIdentifier,
    frontComponentUniversalIdentifier,
  } = resolveEntityRelationUniversalIdentifiers({
    metadataName: 'commandMenuItem',
    foreignKeyValues: {
      availabilityObjectMetadataId:
        createCommandMenuItemInput.availabilityObjectMetadataId,
      frontComponentId: createCommandMenuItemInput.frontComponentId,
    },
    flatEntityMaps: { flatObjectMetadataMaps, flatFrontComponentMaps },
  });
```
2026-02-06 17:02:02 +00:00
Thomas TrompetteandGitHub a494a7a902 Fix iterator creation (#17763)
On iterator creation, we get an error "Bad configuration". Looks like a
race condition with generation that happens before apollo cache gets
updated. Adding defensive checks
2026-02-06 15:25:09 +00:00
f6beb06364 Migrate favorites to navigation menu items (1.17 upgrade) (#17477)
**What this PR does:**
- Migrates existing `Favorite` and `FavoriteFolder` entities to the new
`NavigationMenuItem` structure
- Preserves user-level vs workspace-level ownership
- Preserves folder structure, positions, and relationships
- Handles both view-based favorites (linked to views) and record-based
favorites (linked to records)
- Soft-deletes original favorites and folders after successful migration
- Enables the `IS_NAVIGATION_MENU_ITEM_ENABLED` feature flag
post-migration
- Skips migration if the feature flag is already enabled
- Idempotent: checks for existing navigation menu items to prevent
duplicates

**Migration flow:**
1. Migrate favorite folders first (creates folder mapping)
2. Migrate favorites
3. Soft-delete migrated favorites and folders
4. Enable feature flag

**What's next:**
- After all workspaces are migrated and the navigation menu item feature
is fully rolled out, we can:
- Remove the old `Favorite` and `FavoriteFolder` entities and related
code
- Remove the feature flag check and make navigation menu items the
default
  - Clean up any deprecated favorites-related code paths in the frontend

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-02-06 13:42:20 +00:00
7074d9ce1b Fix CSV preview duplicate key warning (#10920) (#17754)
Fix issue #10920
<img width="1264" height="630" alt="image"
src="https://github.com/user-attachments/assets/d7a00e4f-85cb-49e1-aa38-4c807f1e1b69"
/>


The root cause is that `@cyntler/react-doc-viewer`'s CSV renderer uses
**cell values as React keys** instead of indices.

### There are two approaches:
1. **patch-package** — Directly fix the key usage in the library's
source code and pull a request to the author of
`@cyntler/react-doc-viewer` to fix it.

<img width="880" height="469" alt="image"
src="https://github.com/user-attachments/assets/819e5b6a-21ae-4333-87f5-3b7f6d7e2738"
/>

<img width="1753" height="568" alt="image"
src="https://github.com/user-attachments/assets/c8a0ee49-9bf4-4002-aad2-65914ac10254"
/>



2. Custom CSV renderer (My current code commit)

### Changes
- `fetchCsvPreview.ts` — Fetches CSV and parses it into headers and rows
- `DocumentViewer.tsx` — Renders CSV with a custom table instead of
passing it to DocViewer

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-02-06 13:35:47 +00:00
WeikoandGitHub b3c95744ef Fix twenty-emails build (#17760)
## Context
I've seen errors in twenty-emails build where I18n from @lingui/core was
resolved to two different declaration files
```typescript
src/components/BaseEmail.tsx:20:19 - error TS2719: Type 'import("/Users/weiko/Projects/twenty/node_modules/@lingui/core/dist/index").I18n' is not assignable to type 'import("/Users/weiko/Projects/twenty/node_modules/@lingui/core/dist/index").I18n'. Two different types with this name exist, but they are unrelated.
  Types have separate declarations of a private property '_locale'.

20     <I18nProvider i18n={i18nInstance}>
                     ~~~~

  node_modules/@lingui/react/dist/shared/react.b2b749a9.d.ts:42:5
    42     i18n: I18n;
           ~~~~
    The expected type comes from property 'i18n' which is declared here on type 'IntrinsicAttributes & Omit<I18nContext, "_"> & { children?: ReactNode; }'
 ```
Seems to be related to https://github.com/twentyhq/twenty/pull/17380
The tsconfig simplification changed how vite plugin resolves types during build. With the inherited moduleResolution: "node", the plugin and source code resolved @lingui/react's types differently, creating two incompatible I18n types from the same package.

## Fix
Add moduleResolution: "bundler" to packages/twenty-emails/tsconfig.json, aligning with twenty-front, twenty-ui, twenty-shared should fix the issue
2026-02-06 11:17:11 +00:00
Lakshay ManchandaandGitHub 10de51fcbd - fixed coloring of item type tag in dark mode (#17745)
Fixed issue #17694 

<img width="963" height="1021" alt="image"
src="https://github.com/user-attachments/assets/a28948a1-126d-4f3c-8a53-c39adbb7f52d"
/>
2026-02-06 10:28:04 +00:00
Félix MalfaitandGitHub d537144ab1 fix: add command to fix morph relation field name mismatches (#17757)
## Summary

When a custom object is renamed after creation, the relation fields on
`noteTarget`, `taskTarget`, `attachment`, and `timelineActivity` keep
their old names but point to the renamed object.

For example:
- User creates custom object `solution`
- System creates field `solution` on NoteTarget → pointing to Solution
- User renames object from `solution` to `productCatalog`
- Field name stays as `solution` but points to `productCatalog`
- Migration runs: `solution` → `targetSolution`
- Now `targetSolution` points to `productCatalog` 

This causes the morph name computation to fail:
- `getMorphNameFromMorphFieldMetadataName("targetSolution",
"productCatalog")`
- Tries to remove `ProductCatalog` from `targetSolution` → no match
- Name stays as `targetSolution` instead of becoming `target`
- Frontend computes: `targetSolution` + `Company` =
`targetSolutionCompany` 💥

## The Fix

This command finds and fixes all such mismatches by:
1. Finding MORPH_RELATION fields where `field.name !=
target${capitalize(targetObject.nameSingular)}`
2. Renaming the column in the workspace schema
3. Updating the field metadata (name + joinColumnName in settings)
4. Invalidating the cache

## Usage

**Dry run (to see what would be fixed):**
```bash
npx nx run twenty-server:command upgrade:1-17:fix-morph-relation-field-names -- --workspace-id <id> --dry-run
```

**Fix a specific workspace:**
```bash
npx nx run twenty-server:command upgrade:1-17:fix-morph-relation-field-names -- --workspace-id <id>
```

**Fix all workspaces:**
```bash
npx nx run twenty-server:command upgrade:1-17:fix-morph-relation-field-names
```

## SQL to find affected workspaces

```sql
SELECT 
  fm."workspaceId",
  COUNT(*) AS mismatched_fields
FROM core."fieldMetadata" fm
JOIN core."objectMetadata" target_om ON fm."relationTargetObjectMetadataId" = target_om.id
JOIN core."objectMetadata" source_om ON fm."objectMetadataId" = source_om.id
WHERE fm.type = 'MORPH_RELATION'
  AND fm.name LIKE 'target%'
  AND source_om."nameSingular" IN ('noteTarget', 'taskTarget', 'attachment', 'timelineActivity')
GROUP BY fm."workspaceId"
HAVING COUNT(*) FILTER (
  WHERE fm.name != 'target' || initcap(replace(target_om."nameSingular", '_', ''))
) > 0;
```
2026-02-06 11:38:50 +01:00
Baptiste DevessierandGitHub 955c1b4916 Allow filtering by page layout type and fetch more fields (#17750)
<img width="3456" height="2160" alt="CleanShot 2026-02-05 at 18 01
28@2x"
src="https://github.com/user-attachments/assets/d8914bdd-e44c-459d-a87f-1b00dd4c29c2"
/>
2026-02-05 17:34:28 +00:00
e505eba978 i18n - translations (#17751)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-05 18:14:47 +01:00
EtienneandGitHub 77d15356e1 Files v2 - Use Files field in attachment (#17707)
- Add FILES field on attachment
- Adapt Attachment logic in front to use new resolver/controller
- Update files-field logic to infer applicationId from fieldMetadataId +
ask for fieldMetadataId in upload resolver
- Design update


To do in next PR : 
- Adapt activity files logic
2026-02-05 16:42:19 +00:00
Félix MalfaitandGitHub e382641ac3 fix: use real founder headshots in seed data (#17749)
## Summary
Updates the avatar URLs for seeded founders to use properly named images
instead of generic numbered placeholder images.

## Changes
Updates `prefill-people.ts` to reference new image paths in
`twentyhq/placeholder-images/founders/`:

| Person | Company | New Image Path |
|--------|---------|----------------|
| Brian Chesky | Airbnb | `founders/brian-chesky.jpg` |
| Dario Amodei | Anthropic | `founders/dario-amodei.jpg` |
| Patrick Collison | Stripe | `founders/patrick-collison.jpg` |
| Dylan Field | Figma | `founders/dylan-field.jpg` |
| Ivan Zhao | Notion | `founders/ivan-zhao.jpg` |

## Related
Requires a corresponding PR on `twentyhq/placeholder-images` to add the
actual founder headshot images to the `founders/` directory.

## Why
The previous placeholder images were generic numbered images that didn't
represent the actual people. This creates a single source of truth for
founder images that all workspaces can reference.
2026-02-05 17:49:43 +01:00
Thomas TrompetteandGitHub 04b8f2aaf5 Add gauge for awaiting jobs (#17747)
<img width="774" height="333" alt="Capture d’écran 2026-02-05 à 16 18
06"
src="https://github.com/user-attachments/assets/ec938a39-801d-4d71-a2de-7ced1795a0bd"
/>
2026-02-05 15:47:53 +00:00
martmullandGitHub 23df33172a Fix twenty sdk build 5 (#17744)
use prepublish instead of prepack
2026-02-05 15:54:06 +01:00
5b701f5ba4 Never display broken relations in record page layouts (#17738)
We generate Field widgets for relations on-the-fly, when a record page
layout is first requested by the user. When the user changed their data
model and then returned to a record page layout, the Field widgets
weren't updated. **This PR ensures Field widgets are recomputed when
relations change.**

Future subjects:

- Now that we will fetch the configuration from the backend and start
storing updates, we will have to think about how we deal with these
generated relation Field widgets.

## Before


https://github.com/user-attachments/assets/99d53b19-b231-435f-b14f-4473ba269ad2

## After


https://github.com/user-attachments/assets/357d956d-8b3b-448c-a983-82569a7dd0a0

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-02-05 14:07:04 +00:00
Thomas TrompetteandGitHub df516a904b Add lock on version creation (#17740)
Should avoid duplicates we have sometimes
2026-02-05 14:01:21 +00:00
c0fd4c7fed i18n - translations (#17743)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-05 15:07:16 +01:00
Thomas TrompetteandGitHub c521406bf8 Improve workflow crons (#17720)
Issue 1: no info to debug cron trigger. Stop catching exception + using
logs instead of throwing for now

Issue 2: sentry often send timeouts errors for workflow crons. Probably
not real ones, it sends it if the job takes more than 5 minutes to run.
To fix, on each workflow cron we do:
- loop over active workspaces
- perform a query check that workspace is relevant, using count for
performances
- send a job if relevant
2026-02-05 13:36:32 +00:00
Félix MalfaitandGitHub 27da9d60eb fix: set isActive=true in morph migration & add system fields toggle (#17736)
## Summary

Three fixes in this PR:

### 1. Migration commands now set `isActive = true` and use generic
'Target' label

When converting relation fields to `MORPH_RELATION` type, the migration
commands now:
- Set `isActive = true` to prevent inactive fields from being selected
as the representative morph field
- Update all field labels to generic **'Target'** instead of keeping
individual labels like 'Company', 'Person'

This ensures the UI shows a coherent label ('Target') alongside 'X
Objects' for the type.

**Files changed:**
- `1-17-migrate-note-target-to-morph-relations.command.ts`
- `1-17-migrate-task-target-to-morph-relations.command.ts`

### 2. Added system fields/relations toggles in Settings

The fields and relations tables in Settings > Data Model were filtering
out system fields with no way to view them. Added a "System fields" /
"System relations" toggle (visible in advanced mode) to allow viewing
these fields.

**Files changed:**
- `SettingsObjectFieldTable.tsx`
- `SettingsObjectRelationsTable.tsx`

This matches the existing behavior on the Objects table which already
has a "System objects" toggle.
2026-02-05 13:35:46 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Abdullah
0473efcef0 Bump drizzle-kit from 0.31.5 to 0.31.8 (#17725)
Bumps [drizzle-kit](https://github.com/drizzle-team/drizzle-orm) from
0.31.5 to 0.31.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/drizzle-team/drizzle-orm/releases">drizzle-kit's
releases</a>.</em></p>
<blockquote>
<h2>drizzle-kit@0.31.8</h2>
<h3>Bug fixes</h3>
<ul>
<li>Fixed <code>algorythm</code> =&gt; <code>algorithm</code> typo.</li>
<li>Fixed external dependencies in build configuration.</li>
</ul>
<h2>drizzle-kit@0.31.6</h2>
<h3>Bug fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/drizzle-team/drizzle-orm/issues/2853">[BUG]:
Importing drizzle-kit/api fails in ESM modules</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/c445637df39366bcf47b12601896ce851771c1c2"><code>c445637</code></a>
Merge pull request <a
href="https://redirect.github.com/drizzle-team/drizzle-orm/issues/5095">#5095</a>
from drizzle-team/main-workflows</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/e7b3aaa26456b88cd23a7843ebc95b3bddde1ba4"><code>e7b3aaa</code></a>
Merge branch 'main' into main-workflows</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/0d885a54ddafd8717f8610cf3d2899f3eef61e65"><code>0d885a5</code></a>
refactor: Update condition for run-feature job to improve clarity and
functio...</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/45a1ffbcbfdd96772d0aba7d9e43744db2dce471"><code>45a1ffb</code></a>
Merge pull request <a
href="https://redirect.github.com/drizzle-team/drizzle-orm/issues/5087">#5087</a>
from drizzle-team/main-workflows</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/6357645bd33b1f444e1d081769dd4b71c3de31f8"><code>6357645</code></a>
chore: Comment out NEON_HTTP_CONNECTION_STRING requirement in release
workflows</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/53dec98a936f549d0cc2e668f19db3a2df842f51"><code>53dec98</code></a>
refactor: Simplify release router workflow by removing unnecessary
switch job...</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/ce88a181e03d8b9b3fd0b62c93cc1faa05b0e000"><code>ce88a18</code></a>
Merge remote-tracking branch 'origin/ext-deps-kit' into
main-workflows</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/5c8a4c508b36813599e6de891166a6888720a307"><code>5c8a4c5</code></a>
+</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/73e2ea486f6781bc7bfd2c287590d9c96e319b51"><code>73e2ea4</code></a>
feat: Add release router workflow to manage feature and latest
releases</li>
<li><a
href="https://github.com/drizzle-team/drizzle-orm/commit/378b0432d549441fa61de200589a790f1171b6fe"><code>378b043</code></a>
Merge pull request <a
href="https://redirect.github.com/drizzle-team/drizzle-orm/issues/5002">#5002</a>
from drizzle-team/main-next-pack</li>
<li>Additional commits viewable in <a
href="https://github.com/drizzle-team/drizzle-orm/compare/drizzle-kit@0.31.5...drizzle-kit@0.31.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 drizzle-kit since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=drizzle-kit&package-manager=npm_and_yarn&previous-version=0.31.5&new-version=0.31.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>
Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-02-05 13:02:33 +00:00
martmullandGitHub de5764ede0 Fix twenty sdk build (#17729)
- remove worksapce:* dependencies from published packages
- Use common tsconfig.ts in create-twenty-app
- Increase version to 0.4.4
2026-02-05 14:25:49 +01:00
Félix MalfaitandGitHub f899804db3 fix: update lockfile for twenty CLI bin path change (#17739)
## Summary

The lockfile had a stale reference to `dist/cli/index.cjs` but the
twenty CLI bin was changed to `dist/cli.cjs`.

This was causing CI to fail with:
```
YN0028: -    twenty: dist/cli/index.cjs
YN0028: +    twenty: dist/cli.cjs
YN0028: The lockfile would have been modified by this install, which is explicitly forbidden.
```

This PR updates the lockfile to match the new path.
2026-02-05 14:12:04 +01:00
Lucas BordeauandGitHub 66b87c641c Fixed SSE connection retry infinite loop (#17723)
This bug was cause by a combination of an expired token stored inside
SSE client on the front end, and a server restart or a cache flush, we
ended up with an SSE client that tries to reconnect infinitely with an
expired token.

The fix is to improve the connection retry handler in the frontend to
detect an expired token and recreate an SSE client.
2026-02-05 13:19:43 +01:00
Raphaël BosiandGitHub 309785f7ff Refactor twenty-sdk folder stucture (#17733)
Only three barrels: `root`, `ui` and `front-component`
2026-02-05 13:14:27 +01:00
Abdullah.andGitHub 60f2a74bf0 fix: fast-xml-parser has rangeerror dos numeric entities bug (#17732)
Resolves [Dependabot Alert
412](https://github.com/twentyhq/twenty/security/dependabot/412).

AWS SDK minor-releases are backward compatible.
2026-02-05 13:09:47 +01:00
Félix MalfaitandGitHub 6851085143 Fix note/task target creation to support morph relations (#17734)
## Overview
Fixes the `unknown fields opportunityId in objectMetadataItem
noteTarget` error when creating notes/tasks from record pages (like the
Notes/Tasks tab on an opportunity).

## Root Cause
The `useOpenCreateActivityDrawer` hook was not handling `MORPH_RELATION`
field types:

1. Code only checked `field.relation` (which is populated for `RELATION`
type fields)
2. When `field.relation` was undefined (because the field is
`MORPH_RELATION`), it fell back to the old naming convention (e.g.,
`opportunityId`)
3. But after the morph migration, the columns are named differently
(e.g., `targetOpportunityId`)

This caused the error on both new workspaces (which are created with
morph relations from the start) and existing workspaces that ran the
migration.

## Solution
Use the existing `findTargetFieldInfo()` utility which properly handles
both `RELATION` and `MORPH_RELATION` field types by:
- Checking `morphRelations` for morph fields and computing the correct
field name
- Checking `relation` for regular relation fields
- Returning the correct `joinColumnName` for each case

This utility is already used elsewhere in the codebase (e.g., in
junction field handling) and is the cleanest solution.
2026-02-05 11:52:23 +00:00
9604dbe78a Front components communication between host and remote (#17716)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-05 11:44:02 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f733f30b62 Bump @cyntler/react-doc-viewer from 1.17.0 to 1.17.1 (#17727)
Bumps
[@cyntler/react-doc-viewer](https://github.com/cyntler/react-doc-viewer)
from 1.17.0 to 1.17.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cyntler/react-doc-viewer/releases"><code>@​cyntler/react-doc-viewer</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v1.17.1</h2>
<ul>
<li>style: prettier fix tr.json (2392605)</li>
<li>Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/305">#305</a>
from wangyinyuan/main (36029ea)</li>
<li>Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/297">#297</a>
from ysaribulut/feature/turkish-translation (fa591ca)</li>
<li>fix: correct Chinese and special characters display in HTML renderer
(c358c9d)</li>
<li>Update README.md (e6fe4e0)</li>
<li>feat: add Turkish translations (4066963)</li>
<li>Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/290">#290</a>
from Pespiri/main (c6ded83)</li>
<li>fix styled components prop pollution (5bd551a)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/955795bca8bb5c81f76674321a019ef4f838f307"><code>955795b</code></a>
Release 1.17.1</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/239260507647a0ac282a45960ac8d7e01b35bbaf"><code>2392605</code></a>
style: prettier fix tr.json</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/36029ea39c28b2a90adc605d46a45265f14ddc16"><code>36029ea</code></a>
Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/305">#305</a>
from wangyinyuan/main</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/fa591caf45e819f125efe979b34ec753362176a3"><code>fa591ca</code></a>
Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/297">#297</a>
from ysaribulut/feature/turkish-translation</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/c358c9d103beaafb1dded42fd142f2ede9482598"><code>c358c9d</code></a>
fix: correct Chinese and special characters display in HTML
renderer</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/e6fe4e089ba869f5e2abdee9b308baa42ff7b1c5"><code>e6fe4e0</code></a>
Update README.md</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/4066963b06f306af9e5029ee29bef89c815c7e4b"><code>4066963</code></a>
feat: add Turkish translations</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/c6ded83f9032f4ed2a44a947e854976e2dce8398"><code>c6ded83</code></a>
Merge pull request <a
href="https://redirect.github.com/cyntler/react-doc-viewer/issues/290">#290</a>
from Pespiri/main</li>
<li><a
href="https://github.com/cyntler/react-doc-viewer/commit/5bd551a7382b636c371733c2e34e1e89e2129daf"><code>5bd551a</code></a>
fix styled components prop pollution</li>
<li>See full diff in <a
href="https://github.com/cyntler/react-doc-viewer/compare/v1.17.0...v1.17.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@cyntler/react-doc-viewer&package-manager=npm_and_yarn&previous-version=1.17.0&new-version=1.17.1)](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-02-05 10:07:38 +01:00
1d6dc04cda i18n - translations (#17724)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-05 02:30:12 +01:00
Charles BochetandGitHub 67a98f77e3 Refactor workflow-logic-function-interaction (#17699)
# Refactor workflow–logic function interaction

## Why

Workflow code steps and standalone logic functions shared the same build
layer and DB layer, which blurred two use cases: code steps belong to a
workflow version; standalone functions are deployable units. That made
workflow code steps harder to own and evolve.

## Goal

Treat code steps as **workflow-owned**: build and run them in workflow
context, and expose workflow-scoped APIs so the editor can load, test,
and save code step source without going through the generic
logic-function layer.
2026-02-05 00:46:55 +00:00
neo773andGitHub 752359335e exclude Gmail category labels only for system folders (#17640)
Previous code in `MESSAGING_GMAIL_DEFAULT_NOT_SYNCED_LABELS` was
problematic as we grouped `category` labels along with `system folders`
labels together.

This fixes partially missing emails issue by splitting it and not
applying category exclusion when querying for a singular custom label.

Also removes old approach of getting message label_id's association from
additional network call overhead to local utility
`filterGmailMessagesByFolderPolicy`
2026-02-04 17:57:48 +00:00
WeikoGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Weiko
64700a00b0 Fix typeorm internal query builder missing twenty internal context (#17719)
## Context
Fix TypeError: Cannot read properties of undefined (reading
'coreDataSource') when updating records with RLS predicates enabled

## Implementation
Use lazy initialization for FilesFieldSync and RelationNestedQueries in
workspace query builder

## Technical details
When Row-Level Security (RLS) predicates are applied during an update
operation, TypeORM internally creates sub-query builders to evaluate
Brackets in WHERE clauses. TypeORM's createQueryBuilder() method calls
new this.constructor(connection, queryRunner) with only 2 arguments, but
WorkspaceUpdateQueryBuilder expects 6 arguments including
internalContext.
This caused FilesFieldSync to be instantiated with undefined context,
resulting in the error when accessing internalContext.coreDataSource.

Converting filesFieldSync and relationNestedQueries from eager
initialization in the constructor to lazy getters. This way:
TypeORM internal sub-query builders work fine (they never access these
dependencies)
Real query builders create dependencies on-demand when execute() runs

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Weiko <Weiko@users.noreply.github.com>
2026-02-04 16:57:42 +00:00
WeikoandGitHub 8674cf973e Display "Not shared" indicator for RLS-restricted relation fields (#17713)
## Context
Previously, if for example a Person had a companyId but the company
relation was null due to RLS, the company column appeared empty. Now it
displays ForbiddenFieldDisplay to indicate the relation exists but is
inaccessible.

When RLS restricts access to a related record, the frontend now shows a
"Not shared" indicator (with lock icon) instead of an empty field.

<img width="1275" height="399" alt="Screenshot 2026-02-04 at 15 37 53"
src="https://github.com/user-attachments/assets/870306f8-f811-4dc0-b23b-a33e89043a37"
/>
2026-02-04 16:49:39 +00:00
Félix MalfaitandGitHub 96c37b30c5 fix: use TwentyConfigService instead of ConfigService for ENTERPRISE_KEY (#17721)
## Summary

Replace NestJS `ConfigService` with `TwentyConfigService` for
`ENTERPRISE_KEY` access in row-level permission services.

## Changes

- `row-level-permission-predicate.service.ts`: Updated to use
`TwentyConfigService`
- `row-level-permission-predicate-group.service.ts`: Updated to use
`TwentyConfigService`

## Why

`TwentyConfigService` also pulls config values from the database, not
just environment variables. This ensures consistent config access across
the codebase.
2026-02-04 16:35:53 +00:00
martmullandGitHub bef643970b Fix twenty sdk build 3 (#17715)
final final fix
2026-02-04 15:21:17 +00:00
MarieandGitHub 6a5974e06c [fix] Some fixes (#17674)
- fixes
[sentry](https://twenty-v7.sentry.io/issues/7239112455/?environment=prod&environment=prod-eu&project=4507072563183616&query=anything%20else&referrer=issue-stream&sort=date)
- adapt useClearField logic to morph relations
- adapt useClearField logic to one-to-many relations (early return)
- fix creation of objects without "name" field from specific parts of
the product (which involved a "name" field)
2026-02-04 14:51:33 +00:00
MarieandGitHub d4303469ff Improve qualification of rest errors (#17629)
Will help close
[sentry](https://twenty-v7.sentry.io/issues/6827612895/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20%21issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=issue-stream&sort=date):
a 400 error (attempt to filter on a `null` id) qualified as a 500 error
from rest api.

I don't know what was our long term plan on validation for rest api, I
suppose it's not a priority, making it acceptable to interceipt some
postgres errors based on their messages. Ideally we would have a
validation earlier, at args parsing level.

before
<img width="1286" height="460" alt="Capture d’écran 2026-02-02 à 14 08
53"
src="https://github.com/user-attachments/assets/bf3d34f9-1436-4739-8f8e-95b18f59045b"
/>

after
<img width="1222" height="438" alt="Capture d’écran 2026-02-02 à 14 09
01"
src="https://github.com/user-attachments/assets/e817eaff-1ab9-49fb-869d-ddfd00671fbf"
/>
2026-02-04 14:33:30 +00:00
4629fb90be i18n - translations (#17712)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-04 15:24:23 +01:00
Paul RastoinandGitHub 48c8fa6809 Refactor workspace migration update action (#17701)
# Introduction
Removing:
- `from` property from actions definition, as it's a legitimate source
of truth. The stored comparison might have been compromised since action
generation. If from is needed it should be computed from the optimistic
cache at runner lvl
- Removed the `FlatEntityPropertyUpdates` Array complexity in favor of

From
```ts
export type PropertyUpdate<T, P extends keyof T> = {
  property: P;
} & FromTo<T[P]>;
```

To
```ts
export type FlatEntityUpdate<T extends AllMetadataName> = Partial<
  Pick<
    MetadataFlatEntity<T>,
    Extract<FlatEntityPropertiesToCompare<T>, keyof MetadataFlatEntity<T>>
  >
>;
```

## New interactions
From
```ts
    const positionUpdate = findFlatEntityPropertyUpdate({
      flatEntityUpdates,
      property: 'position',
    });

    if (
      isDefined(positionUpdate) &&
      (!Number.isInteger(positionUpdate.to) || positionUpdate.to < 0)
    ) {


   const toFlatNavigationMenuItem = {
      ...fromFlatNavigationMenuItem,
      ...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
        updates: flatEntityUpdates,
      }),
    };
```

To
```ts
    const positionUpdate = flatEntityUpdate.position;
    if (
      isDefined(positionUpdate) &&
      (!Number.isInteger(positionUpdate) || positionUpdate < 0)
    ) {

    const toFlatNavigationMenuItem = {
      ...fromFlatNavigationMenuItem,
      ...flatEntityUpdate,
    };
```

## `SanitizeFlatEntityUpdate`
Enforcing the `flatEntityUpdate` to only contains comparable properties
per flat entity by striping out all unexpected keys
In the future we will also move the whole validation at runner lvl at
some point

```ts
export const sanitizeFlatEntityUpdate = <T extends AllMetadataName>({
  flatEntityUpdate,
  metadataName,
}: {
  flatEntityUpdate: FlatEntityUpdate<T>;
  metadataName: T;
}): FlatEntityUpdate<T> => {
  const { propertiesToCompare } =
    ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY[metadataName];

  const initialAccumulator: FlatEntityUpdate<T> = {};

  return propertiesToCompare.reduce((accumulator, property) => {
    const updatedValue =
      flatEntityUpdate[property as MetadataFlatEntityComparableProperties<T>];

    if (updatedValue === undefined) {
      return accumulator;
    }

    return {
      ...accumulator,
      [property]: updatedValue,
    };
  }, initialAccumulator);
};
```
2026-02-04 13:50:46 +00:00
Thomas TrompetteandGitHub b03e3772e6 Add real time image (#17710)
As title
2026-02-04 14:43:42 +01:00
nitinandGitHub 660a15b721 [FRONT COMPONENT] Stories in twenty-sdk for front component generation and upon render interactivity (#17675)
closes https://github.com/twentyhq/core-team-issues/issues/2179
2026-02-04 12:39:54 +00:00
Thomas TrompetteandGitHub 59c36736d4 Put SSE in lab (#17709)
as title
2026-02-04 13:57:25 +01:00
92359603c1 i18n - translations (#17708)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-04 13:44:21 +01:00
Félix MalfaitandGitHub 177cae8c53 feat: audit Logs (#17660) 2026-02-04 13:30:55 +01:00
martmullandGitHub 6407474461 Fix build without nx build (#17700)
as title
2026-02-04 11:14:28 +00:00
bcfacdead9 i18n - translations (#17706)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-04 12:23:29 +01:00
EtienneandGitHub 4fcc424e24 Files field - add files field display and input + filtering (#17637)
This PR introduces a new FILES field type for Twenty CRM, allowing users
to attach multiple files to any record.

- Files field display
- Files field preview
- Files field input
- Files field filtering

To test : 
1/ need to activate feature flag `IS_FILES_FIELD_ENABLED` + create a new
FILES field

- display in read only, edit, inline, table
- edit
- filter
- export


closes https://github.com/twentyhq/core-team-issues/issues/2154


<img width="354" height="134" alt="Screenshot 2026-02-02 at 19 11 01"
src="https://github.com/user-attachments/assets/3c3de89e-f6b6-4526-b710-e4c7ee1a6d30"
/>
<img width="1081" height="933" alt="Screenshot 2026-02-02 at 19 10 41"
src="https://github.com/user-attachments/assets/7c9a7278-edb7-4d4a-882c-f7404689d011"
/>
<img width="1073" height="719" alt="Screenshot 2026-02-02 at 19 10 33"
src="https://github.com/user-attachments/assets/79cc372b-56a2-4cbf-b4fe-023adc4753a8"
/>
2026-02-04 10:55:48 +00:00
Raphaël BosiandGitHub 9b8eacb7f7 [FRONT COMPONENTS] Expose twenty UI in twenty sdk (#17645)
- An app will declare its `twenty-sdk` version in the manifest.
- `twenty-sdk` will be served by a CDN to be imported in front component
host.
- When we render the host we will load twenty-ui through the correct
version of the sdk

We will proceed this way because twenty-ui is not mature enough yet to
be deployed as its own package. So instead, since the sdk is already
deployed as its own package, we will serve the ui with it. It allows us
to have versioning to handle breaking changes in twenty-ui.
2026-02-04 10:43:08 +00:00
EtienneandGitHub f6b210dd0d Files v2 - File table migration + data migration command (#17661)
To add also in 1.16 patch
2026-02-04 10:28:55 +00:00
EtienneandGitHub 50ef231704 Billing - Fix inactivity (#17703)
- Add suspendedAt column on workspace table
- Update suspendedAt field when workspace suspended or re-activated
- Compute inactivity on suspendedAt



Tested : 
- Cancel subscription
- Cancel subscription + Subscribe again
- Cleaning command
2026-02-04 10:17:27 +00:00
aa06dab920 i18n - translations (#17704)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-04 11:11:10 +01:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
414f68fb63 fix(workspace-cache): memory leak in deleteFromLocalCache (#17686)
## Problem

The `deleteFromLocalCache` method was only setting `lastHashCheckedAt=0`
instead of actually deleting the cache entry. This caused old versions
to accumulate in the local cache when `invalidateAndRecompute` was
called.

### Flow that causes the leak:

1. `invalidateAndRecompute()` is called
2. `flush()` → `deleteFromLocalCache()` — **only sets
`lastHashCheckedAt=0`, keeps old data**
3. `recomputeDataFromProvider()` → `setInLocalCache()` — **adds new
version with new hash**
4. `cleanupStaleVersions()` — **never called** (only triggered from
`getFromLocalCache` path)

Result: Each `invalidateAndRecompute` call adds a new version to
`entry.versions` without removing the old one.

### Impact

For migration commands (like
`1-17-migrate-attachment-to-morph-relations`) that process thousands of
workspaces, this caused significant memory growth:
- Each workspace calls `getOrRecompute` (version 1)
- Then calls `invalidateAndRecompute` (version 2 added, version 1 stays)
- Memory accumulates as the command processes more workspaces

## Solution

Actually delete the local cache entry in `deleteFromLocalCache`, so
`recomputeDataFromProvider` starts fresh.

```typescript
// Before (memory leak)
if (isDefined(entry)) {
  entry.lastHashCheckedAt = 0;
}

// After (proper cleanup)
this.localCache.delete(localKey);
```

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-04 09:47:45 +00:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
0edc3a385c fix: prevent anonymous users from bypassing workspace creation restriction (#17635)
When `IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS` is true, anonymous
users could still create workspaces because
`checkWorkspaceCreationIsAllowedOrThrow` checked per-user workspace
count (always 0 for new users) instead of system-wide workspace count.

The bootstrap bypass now only applies when no workspaces exist in the
entire system. Also adds the same check in `signUpOnNewWorkspace` to
guard the `signInUp` code path.

Fixes #17631

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

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-04 09:47:24 +00:00
Baptiste DevessierandGitHub 29093fdbf1 Fetch soft-deleted records in Record Page Layouts (#17698)
## Before


https://github.com/user-attachments/assets/35a69015-8d3a-498f-8cb3-07bbf3f4d307

## After


https://github.com/user-attachments/assets/1232b41f-50a1-4f7d-afbe-f1fe57bd7de0
2026-02-04 09:07:00 +00:00
Charles BochetandGitHub 402d149ee1 Remove logic function layer (#17697)
## Remove logic function layer

Package.json and yarn.lock are now on the application entity, so the
logic function layer is no longer used except as a legacy source for the
1.17 backfill. This PR removes all layer usage outside of that migration
and keeps only the entity for backfill.

### Summary

- **Kept:** `LogicFunctionLayerEntity` and its table, only used by the
1.17 backfill command to read legacy layer data and backfill application
package files.
- **Removed:** All other layer logic: CRUD, cache, resolvers, services,
DTOs, and frontend types. Logic functions now depend only on the
application for package/dependency context.


### Why Dependencies instead of Source for package files

Package.json and yarn.lock are the application’s dependency set and are
stored under the application in **FileFolder.Dependencies**. The build
service and drivers now read them only from Dependencies; nothing is
written to Source for these files.
2026-02-04 10:17:27 +01:00
martmullandGitHub e10e0b337e Fix twenty sdk build (#17696)
- add twenty-ui in dist/vendor folder
- fix ts issue due to react version mismatch
2026-02-04 08:46:13 +00:00
Abdullah.andGitHub 7867617385 Migrate noteTarget and taskTarget to Morph. (#17476)
This PR migrates `noteTarget` and `taskTarget` to morph relations behind
separate feature flags, following the Attachment/TimelineActivity
pattern.

It introduces the `IS_NOTE_TARGET_MIGRATED` and
`IS_TASK_TARGET_MIGRATED` flags, updates standard field metadata and
indexes to use morph relations, and adds two **1.17 workspace
migrations** that:
- rename `noteTarget.*Id` / `taskTarget.*Id` columns to `target*Id`
- convert the corresponding field metadata to `MORPH_RELATION` with a
shared `morphId`

On the frontend, note/task target read and write paths switch to
`target*Id` when the respective flag is enabled. Deleted targets are
filtered on reload to prevent reappearing relations.
2026-02-04 02:53:06 +00:00
9aba2f62e1 i18n - docs translations (#17692)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-04 00:22:17 +01:00
neo773andGitHub 4ae5732483 Migrate gmail fetch by batch to library (#17514)
Replace manual HTTP batch implementation with
`@jrmdayn/googleapis-batcher` library (we already use this for fetching
message list)

Initial real testing works fine but do not merge yet needs more
extensive real test runs
2026-02-04 00:21:56 +01:00
Charles BochetandGitHub 867b03393b Backfill package json for custom and standard app (#17681)
# Backfill application package files for custom and standard apps

- Backfill `package.json` / `yarn.lock` (and related fields) for
existing workspaces; new standard/custom apps get default dependency
files.
- Default package files under
`application/constants/default-package-files/`; util with hardcoded
checksums (comment on how to regenerate).
- New **FileFolder.Dependencies** for app dependency files;
**writeFile_v2** accepts optional `queryRunner` for transactional
writes.

Usages:
- **Upgrade command** `upgrade:1-17:backfill-application-package-files`:
standard/custom apps → default files; other apps → from logic function
layer.
- **Workspace creation**: create workspace with
`workspaceCustomApplicationId` first, then create application (enables
same-transaction insert). Migration makes workspace/application/file FKs
deferrable.
-  dev seeder
2026-02-03 21:23:29 +01:00
60b48339b9 i18n - docs translations (#17689)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-03 20:14:58 +01:00
Thomas TrompetteandGitHub c18f574aee Trigger refetch workflow on version creation (#17685)
https://github.com/user-attachments/assets/ec156aba-3a67-4eef-addf-86158c3f1837
2026-02-03 17:52:30 +00:00
fdd3e6d245 i18n - translations (#17688)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-03 19:01:52 +01:00
neo773andGitHub 29a2635164 Fix message/calendar channels stuck with null syncStageStartedAt (#17684)
PR #17492 fixed `markAsMessagesListFetchOngoing` to set
`syncStageStartedAt`, but also changed `isSyncStale` to return `false`
for null values. This prevented the stale recovery job from recovering
channels that were already stuck before the fix was deployed.

Changing `isSyncStale` to return `true` for null/undefined allows the
stale job to properly reset stuck channels back to PENDING state.
2026-02-03 17:24:00 +00:00
martmullandGitHub b53dfa0533 Publish twenty packages (#17676)
- removes code editor in settings
- update readmes and docs
2026-02-03 17:16:54 +00:00
Baptiste DevessierandGitHub ccd40e7633 feat: make feature flag image optional (#17679)
Prevent layout shift when no image is provided for a feature flag

## Before


https://github.com/user-attachments/assets/b1a98da9-1208-46f7-9fc6-9c34a9012c02

## After 


https://github.com/user-attachments/assets/ed12df0c-1651-4fc9-a0e7-60f5fb1533a4
2026-02-03 17:03:37 +00:00
3db961c038 i18n - translations (#17682)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-03 18:04:07 +01:00
Paul RastoinandGitHub 476bdf764c Refactor flat entity maps to be universal oriented (#17665)
# Introduction
In preparation of the workspace agnostic builder, we're migrating
`FlatEntityMaps` to be universal identifier oriented and based
As in the builder context there're won't be any ids at all
Please also note that the FlatEntity is a UniversalFlatEntity superset

From
```ts
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';

export type FlatEntityMaps<T extends SyncableFlatEntity> = {
  byId: Partial<Record<string, T>>;
  idByUniversalIdentifier: Partial<Record<string, string>>;
  universalIdentifiersByApplicationId: Partial<Record<string, string[]>>;
};
```

To
```ts
export type FlatEntityMaps<
  T extends SyncableFlatEntity | UniversalSyncableFlatEntity,
> = {
  byUniversalIdentifier: Partial<Record<string, T>>;
  universalIdentifierById: Partial<Record<string, string>>;
  universalIdentifiersByApplicationId: Partial<Record<string, string[]>>; // this might make more sense to be migrated to universalIdentifiersByApplicationUniversalIdentifier but it's the main topic of this PR
};
```

## Low level maps tools
Had to refactor find | create | delete | replace | find-many | get-sub
tools ( through mutations and or throw equivalent )
2026-02-03 16:16:02 +00:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>DevessierDevessier
43042d55b2 Scaffold Fields widget edition (#17548)
https://github.com/user-attachments/assets/960b8b0d-10e8-42a1-bf61-e875359ea302

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Devessier <29370468+Devessier@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
2026-02-03 15:56:41 +00:00
Raphaël BosiandGitHub 66d7182d02 Remote dom twenty UI POC (#17652)
Create a proof of concept which allows us to use twenty-ui in the
remote-dom.
For now only the button is allowed.


https://github.com/user-attachments/assets/e5441d2c-eb63-4b99-931b-86ee14621393

Known limitation: The icons are not yet available
2026-02-03 15:54:18 +00:00
WeikoandGitHub 5c91a96e84 Add position to page layout widgets (#17643)
## Context
Introducing a new position column in PageLayoutWidget. This is because
we already have gridPosition but all widgets are not supposed to fit in
a grid (see pageLayoutTab layoutMode (GRID, VERTICAL_LIST, CANVAS...),
position will now support the different layoutMode

## Implementation
- Adding the new column (not renaming to avoid breaking changes in the
dashboards)
- Add proper validation for each layout mode
- Update standard page layout to not always use GRID but rely on the tab
layout mode

<details>

<summary>Graphql Query</summary>

```graphql
{
  "data": {
    "getPageLayouts": [
      {
        "tabs": [
          {
            "title": "Tab 1",
            "layoutMode": "GRID",
            "widgets": [
              {
                "title": "Untitled Rich Text",
                "position": {
                  "column": 0,
                  "columnSpan": 6,
                  "layoutMode": "GRID",
                  "row": 0,
                  "rowSpan": 6
                }
              },
              {
                "title": "Deals by Company",
                "position": {
                  "column": 6,
                  "columnSpan": 6,
                  "layoutMode": "GRID",
                  "row": 0,
                  "rowSpan": 6
                }
              },
              {
                "title": "Pipeline Value by Stage",
                "position": {
                  "column": 0,
                  "columnSpan": 6,
                  "layoutMode": "GRID",
                  "row": 6,
                  "rowSpan": 6
                }
              },
              {
                "title": "Revenue Timeline",
                "position": {
                  "column": 6,
                  "columnSpan": 6,
                  "layoutMode": "GRID",
                  "row": 6,
                  "rowSpan": 6
                }
              },
              {
                "title": "Opportunities by Owner",
                "position": {
                  "column": 0,
                  "columnSpan": 6,
                  "layoutMode": "GRID",
                  "row": 12,
                  "rowSpan": 6
                }
              },
              {
                "title": "Stock market (Iframe)",
                "position": {
                  "column": 6,
                  "columnSpan": 6,
                  "layoutMode": "GRID",
                  "row": 12,
                  "rowSpan": 8
                }
              },
              {
                "title": "Deals created this month",
                "position": {
                  "column": 0,
                  "columnSpan": 3,
                  "layoutMode": "GRID",
                  "row": 18,
                  "rowSpan": 2
                }
              },
              {
                "title": "Deal value created this month",
                "position": {
                  "column": 3,
                  "columnSpan": 3,
                  "layoutMode": "GRID",
                  "row": 18,
                  "rowSpan": 2
                }
              }
            ]
          }
        ]
      },
      {
        "tabs": [
          {
            "title": "Home",
            "layoutMode": "VERTICAL_LIST",
            "widgets": [
              {
                "title": "Fields",
                "position": {
                  "layoutMode": "VERTICAL_LIST",
                  "index": 0
                }
              }
            ]
          },
          {
            "title": "Timeline",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Timeline",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Tasks",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Tasks",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Notes",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Notes",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Files",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Files",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Emails",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Emails",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Calendar",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Calendar",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          }
        ]
      },
      {
        "tabs": [
          {
            "title": "Home",
            "layoutMode": "VERTICAL_LIST",
            "widgets": [
              {
                "title": "Fields",
                "position": {
                  "layoutMode": "VERTICAL_LIST",
                  "index": 0
                }
              }
            ]
          },
          {
            "title": "Timeline",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Timeline",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Tasks",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Tasks",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Notes",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Notes",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Files",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Files",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Emails",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Emails",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Calendar",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Calendar",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          }
        ]
      },
      {
        "tabs": [
          {
            "title": "Home",
            "layoutMode": "VERTICAL_LIST",
            "widgets": [
              {
                "title": "Fields",
                "position": {
                  "layoutMode": "VERTICAL_LIST",
                  "index": 0
                }
              }
            ]
          },
          {
            "title": "Timeline",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Timeline",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Tasks",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Tasks",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Notes",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Notes",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Files",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Files",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Emails",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Emails",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Calendar",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Calendar",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          }
        ]
      },
      {
        "tabs": [
          {
            "title": "Home",
            "layoutMode": "VERTICAL_LIST",
            "widgets": [
              {
                "title": "Fields",
                "position": {
                  "layoutMode": "VERTICAL_LIST",
                  "index": 0
                }
              },
              {
                "title": "Note",
                "position": {
                  "layoutMode": "VERTICAL_LIST",
                  "index": 1
                }
              }
            ]
          },
          {
            "title": "Note",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Note",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Timeline",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Timeline",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Files",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Files",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          }
        ]
      },
      {
        "tabs": [
          {
            "title": "Home",
            "layoutMode": "VERTICAL_LIST",
            "widgets": [
              {
                "title": "Fields",
                "position": {
                  "layoutMode": "VERTICAL_LIST",
                  "index": 0
                }
              },
              {
                "title": "Note",
                "position": {
                  "layoutMode": "VERTICAL_LIST",
                  "index": 1
                }
              }
            ]
          },
          {
            "title": "Note",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Note",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Timeline",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Timeline",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          },
          {
            "title": "Files",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Files",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          }
        ]
      },
      {
        "tabs": [
          {
            "title": "Flow",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Flow",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          }
        ]
      },
      {
        "tabs": [
          {
            "title": "Home",
            "layoutMode": "VERTICAL_LIST",
            "widgets": [
              {
                "title": "Fields",
                "position": {
                  "layoutMode": "VERTICAL_LIST",
                  "index": 0
                }
              }
            ]
          },
          {
            "title": "Flow",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Flow",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          }
        ]
      },
      {
        "tabs": [
          {
            "title": "Home",
            "layoutMode": "VERTICAL_LIST",
            "widgets": [
              {
                "title": "Fields",
                "position": {
                  "layoutMode": "VERTICAL_LIST",
                  "index": 0
                }
              }
            ]
          },
          {
            "title": "Flow",
            "layoutMode": "CANVAS",
            "widgets": [
              {
                "title": "Flow",
                "position": {
                  "layoutMode": "CANVAS"
                }
              }
            ]
          }
        ]
      }
    ]
  }
}
```

</details>
2026-02-03 15:54:01 +00:00
f009913cbe i18n - translations (#17678)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-03 17:01:31 +01:00
Charles BochetandGitHub 97867c11e6 Upgrade application model (#17673) 2026-02-03 16:44:00 +01:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>DevessierBaptiste Devessier
1a39cd6019 Disable initial animation in Toggle component (#17648)
## Before


https://github.com/user-attachments/assets/1d915d04-9917-4999-83ea-5fd2e97867e6

## After



https://github.com/user-attachments/assets/091c7e59-f545-43d7-b077-c502b9509799

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Devessier <29370468+Devessier@users.noreply.github.com>
Co-authored-by: Baptiste Devessier <baptiste@devessier.fr>
2026-02-03 15:15:11 +00:00
nitinandGitHub c31ffef510 [Dashboard] fix rich text widget bugs + text selection in non edit mode (#17624)
closes -
https://discord.com/channels/1130383047699738754/1467809736715141211 and
https://discord.com/channels/1130383047699738754/1467810112453345333
 
before - 

text selection -- didn't work - 


https://github.com/user-attachments/assets/a12c5301-5eb1-4641-8386-87e534675c67

tripple click color picker bug - 


https://github.com/user-attachments/assets/8f2ebdd9-53c1-4635-8b28-6c82110b88a0


immediate cancel (before 300ms) - draft persist -- have to refresh to
get correct oncancel data



https://github.com/user-attachments/assets/d56e5738-d42f-4ba8-a9d3-b83acb8546ca



after - 
text selection - 


https://github.com/user-attachments/assets/e358035a-1424-45c5-a062-bb2ae6b6ca47

tripple click color picker bug - 


https://github.com/user-attachments/assets/d70947b0-b68b-4a85-93d4-8bed50e308fd



immediate cancel (before 300ms) - 


https://github.com/user-attachments/assets/f3cfb013-1d48-4f2d-b535-4b8bc5bb1c50



immediate save (before 300ms) - 



https://github.com/user-attachments/assets/3b017755-8523-429e-a511-f65732edec34
2026-02-03 15:10:34 +00:00
nitinandGitHub 19b56c5d3a [FRONT COMPONENTS] Frontend component widget creation (#17653)
closes https://github.com/twentyhq/core-team-issues/issues/2182



https://github.com/user-attachments/assets/babf154c-9cda-40ff-b2e8-5053e447c35f
2026-02-03 15:09:52 +00:00
Félix MalfaitandGitHub a1f26cda0d fix: increase findByText timeout for lazy-loaded DateTimePicker tests (#17672)
## Root Cause

The `DateTimePicker` component lazy-loads `react-datepicker` using
React's `lazy()` with a `Suspense` fallback that shows skeleton loaders:

```typescript
const ReactDatePicker = lazy<ComponentType<DatePickerPropsType>>(() =>
  import('react-datepicker').then(...)
);

// In render:
<Suspense fallback={<SkeletonLoader />}>
  <ReactDatePicker ... />
</Suspense>
```

On slower CI runners (GitHub Actions vs depot.dev), the lazy load takes
longer, causing tests to timeout while still showing skeletons instead
of the actual date picker content.

## Fix

Increased `findByText` timeout from the default 1000ms to 10000ms for
tests that wait for the date picker to load:

- `DateTimeFieldInput.stories.tsx` - 4 stories fixed
- `InternalDatePicker.stories.tsx` - 2 stories fixed

## Why This Wasn't Flaky Before

depot.dev runners have faster I/O and more consistent performance, so
the lazy load completed quickly. GitHub Actions runners have more
variable performance, causing the load to sometimes exceed the 1000ms
default timeout.
2026-02-03 15:44:58 +01:00
Thomas TrompetteandGitHub f2284579f0 Fix code container display (#17666)
Display is broken on code step
2026-02-03 14:12:37 +00:00
Charles BochetandGitHub 10ba8b9a97 Improve cache-clear behavior (#17662)
## Cache flush: scope and options

**Scope**
- `cache:flush` only flushes the **cache** Redis (REDIS_URL). It no
longer touches the queue Redis.

**Options**
- **`--namespace`** (optional): Flush one namespace or omit to flush
all. Valid: `module:messaging`, `module:calendar`, `module:workflow`,
`engine:workspace`, `engine:lock`, `engine:health`,
`engine:subscriptions`.
- **`--pattern`** (optional, default `*`): Key pattern inside the chosen
namespace(s).

**Troubleshooting**
- **`cache:flush:verify`**: Inserts keys inside and outside
`engine:workspace`, flushes, and checks only namespace keys are removed
(confirms flush scope).

**Usage**
- `yarn command:prod cache:flush` — flush all **KNOWN** namespaces
- `yarn command:prod cache:flush -n engine:workspace` — flush one
namespace
- `yarn command:prod cache:flush -n engine:workspace -p
"feature-flag:*"` — flush keys matching pattern in given namespace

FYI, existing namespaces:
```
export enum CacheStorageNamespace {
  ModuleMessaging = 'module:messaging',
  ModuleCalendar = 'module:calendar',
  ModuleWorkflow = 'module:workflow',
  EngineWorkspace = 'engine:workspace',
  EngineLock = 'engine:lock',
  EngineHealth = 'engine:health',
  EngineSubscriptions = 'engine:subscriptions',
}
```
2026-02-03 15:13:23 +01:00
Félix MalfaitandGitHub 9b063af7ab fix: increase RelationToOneFieldDisplay perf threshold to 0.4ms (#17671)
## Summary

Follow-up to #17656 - the `RelationToOneFieldDisplay` performance test
is still failing on GitHub Actions runners.

**Failure:** 0.313ms actual vs 0.3ms threshold

**Fix:** Increased threshold from 0.3ms → 0.4ms to provide more headroom
for runner variability.
2026-02-03 15:06:33 +01:00
Thomas TrompetteandGitHub 99aab9f40d Fix spaces in code step + suggestion overflow (#17664)
- fixedOverflowWidgets fixes the suggestions being overflow outside the
editor
- on focus, stop event propagation to avoid catching spaces as document
level events
2026-02-03 13:36:51 +01:00
martmullandGitHub 65678ed99f Upload files instead ofsources (#17608) 2026-02-03 12:11:10 +01:00
MarieandGitHub cdaa8f1d9d Allow twenty standard app to access messaging items (#17659) 2026-02-03 11:40:21 +01:00
Thomas des FrancsGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Thomas des Francs
963066c7e3 fix: use red3 for dark mode danger background color (#17658)
## Summary

Fixes the dark mode error chip background color issue by changing
`background.danger` from `red12` to `red3` in the dark theme constants.

Fixes #17657

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

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Thomas des Francs <Bonapara@users.noreply.github.com>
2026-02-03 11:00:28 +01:00
Félix MalfaitandGitHub 2826540bb1 fix: increase performance test thresholds for GitHub Actions runners (#17656)
## Summary

After migrating from depot.dev runners to GitHub Actions runners, three
performance tests are failing due to slower single-threaded performance
on the new infrastructure (even with 16 cores).

## Problem

The performance tests use React's `<Profiler>` API to measure component
render times. depot.dev runners have better single-core performance and
lower memory latency compared to GitHub Actions runners, even larger
ones.

Failed tests:
| Test | Threshold | Actual | Overage |
|------|-----------|--------|---------|
| `DateTimeFieldDisplay` | 0.2ms | 0.281ms | +41% |
| `ChipFieldDisplay` | 0.2ms | 0.222ms | +11% |
| `RelationToOneFieldDisplay` | 0.22ms | 0.237ms | +8% |

## Solution

Increased thresholds to account for GitHub Actions runner performance:

- `DateTimeFieldDisplay`: 0.2ms → 0.35ms
- `ChipFieldDisplay`: 0.2ms → 0.3ms  
- `RelationToOneFieldDisplay`: 0.22ms → 0.3ms

## Future Considerations

For a more robust long-term solution, we could implement baseline
comparison that:
1. Saves performance results on main branch builds
2. Compares PR results against baseline with tolerance (±25%)
3. Self-calibrates to actual runner performance

This would catch real regressions without being sensitive to
infrastructure differences.
2026-02-03 10:03:38 +01:00
4193 changed files with 176157 additions and 62815 deletions
+34
View File
@@ -0,0 +1,34 @@
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
curl \
git \
make \
build-essential \
postgresql-client \
docker.io \
&& rm -rf /var/lib/apt/lists/*
# Install nvm (project recommends nvm + .nvmrc for consistent Node versions)
ENV NVM_DIR=/usr/local/nvm
RUN mkdir -p $NVM_DIR \
&& curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
SHELL ["/bin/bash", "-c"]
# Copy .nvmrc so nvm install picks up the right version
COPY .nvmrc /tmp/.nvmrc
# Install Node.js from .nvmrc, enable Corepack, and symlink binaries
# so they're available on PATH without hardcoding a version
RUN . $NVM_DIR/nvm.sh \
&& nvm install $(cat /tmp/.nvmrc) \
&& nvm alias default $(cat /tmp/.nvmrc) \
&& corepack enable \
&& BIN_DIR=$(dirname $(nvm which default)) \
&& ln -sf $BIN_DIR/node /usr/local/bin/node \
&& ln -sf $BIN_DIR/npm /usr/local/bin/npm \
&& ln -sf $BIN_DIR/npx /usr/local/bin/npx \
&& ln -sf $BIN_DIR/corepack /usr/local/bin/corepack
+4 -12
View File
@@ -1,18 +1,10 @@
{
"install": "curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs && node --version && yarn install && echo 'Installing dependencies complete'",
"start": "sudo service docker start && echo 'Docker service started' && sleep 3 && echo 'Starting PostgreSQL and Redis containers...' && make postgres-on-docker && make redis-on-docker && echo 'Waiting for containers to initialize...' && sleep 20 && echo 'Checking container status...' && docker ps --filter name=twenty_ && echo 'Waiting for PostgreSQL to be ready...' && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'PostgreSQL not ready yet, waiting...'; sleep 3; done && echo 'PostgreSQL is ready!' && echo 'Setting up database...' && cd packages/twenty-server && npx nx database:reset twenty-server || echo 'Database already initialized' && echo 'Environment setup complete!'",
"install": "yarn install",
"start": "sudo service docker start && sleep 2 && (docker start twenty_pg 2>/dev/null || make -C packages/twenty-docker postgres-on-docker) && (docker start twenty_redis 2>/dev/null || make -C packages/twenty-docker redis-on-docker) && until docker exec twenty_pg pg_isready -U postgres -h localhost 2>/dev/null; do sleep 1; done && echo 'PostgreSQL ready' && until docker exec twenty_redis redis-cli ping 2>/dev/null | grep -q PONG; do sleep 1; done && echo 'Redis ready' && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
"terminals": [
{
"name": "Development Server",
"command": "echo 'Waiting for database to be fully ready...' && sleep 30 && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'Waiting for PostgreSQL...'; sleep 2; done && echo 'Starting Twenty development server...' && export SERVER_URL=http://localhost:3000 && export PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres && yarn start"
},
{
"name": "Database Management",
"command": "sleep 25 && echo 'Database management terminal ready' && echo 'Waiting for PostgreSQL to be available...' && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'Waiting for PostgreSQL...'; sleep 2; done && echo 'PostgreSQL is ready for database operations!' && echo 'You can now run database commands like:' && echo ' npx nx database:reset twenty-server' && echo ' npx nx database:migrate twenty-server' && bash"
},
{
"name": "Container Logs & Status",
"command": "sleep 10 && echo '=== Container Status Monitor ===' && while true; do echo '\\n=== Container Status at $(date) ===' && docker ps --filter name=twenty_ --format 'table {{.Names}}\\t{{.Status}}\\t{{.Ports}}' && echo '\\n=== PostgreSQL Status ===' && (docker exec twenty_pg pg_isready -U postgres -h localhost && echo 'PostgreSQL: ✅ Ready') || echo 'PostgreSQL: ❌ Not Ready' && echo '\\n=== Redis Status ===' && (docker exec twenty_redis redis-cli ping && echo 'Redis: ✅ Ready') || echo 'Redis: ❌ Not Ready' && sleep 30; done"
"command": "yarn start"
}
]
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,393 @@
---
name: syncable-entity-builder-and-validation
description: Create validation logic and migration action builders for syncable entities in Twenty. Use when implementing business rule validation, uniqueness checks, foreign key validation, or building workspace migration actions for syncable entities. Validators never throw and never mutate.
---
# Syncable Entity: Builder & Validation (Step 3/6)
**Purpose**: Implement business rule validation and create migration action builders.
**When to use**: After completing Steps 1-2 (Types, Cache, Transform). Required before implementing action handlers.
---
## Quick Start
This step creates:
1. Validator service (business logic validation)
2. Builder service (action creation)
3. Orchestrator wiring (**CRITICAL** - often forgotten!)
**Key principles**:
- Validators **never throw** - return error arrays
- Validators **never mutate** - pass optimistic entity maps
- Use indexed lookups (O(1)) not `Object.values().find()` (O(n))
---
## Step 1: Create Validator Service
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-my-entity-validator.service.ts`
```typescript
import { Injectable } from '@nestjs/common';
import { t, msg } from '@lingui/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
import { type FlatMyEntityMaps } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity-maps.type';
import { WorkspaceMigrationValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/types/workspace-migration-validation-error.type';
import { MyEntityExceptionCode } from 'src/engine/metadata-modules/my-entity/exceptions/my-entity-exception-code.enum';
@Injectable()
export class FlatMyEntityValidatorService {
validateMyEntityForCreate(
flatMyEntity: FlatMyEntity,
optimisticFlatMyEntityMaps: FlatMyEntityMaps,
): WorkspaceMigrationValidationError[] {
const errors: WorkspaceMigrationValidationError[] = [];
// Pattern 1: Required field validation
if (!isDefined(flatMyEntity.name) || flatMyEntity.name.trim() === '') {
errors.push({
code: MyEntityExceptionCode.NAME_REQUIRED,
message: t`Name is required`,
userFriendlyMessage: msg`Please provide a name for this entity`,
});
}
// Pattern 2: Uniqueness check - use indexed map (O(1))
const existingEntityWithName = optimisticFlatMyEntityMaps.byName[flatMyEntity.name];
if (isDefined(existingEntityWithName) && existingEntityWithName.id !== flatMyEntity.id) {
errors.push({
code: MyEntityExceptionCode.MY_ENTITY_ALREADY_EXISTS,
message: t`Entity with name ${flatMyEntity.name} already exists`,
userFriendlyMessage: msg`An entity with this name already exists`,
});
}
// Pattern 3: Foreign key validation
if (isDefined(flatMyEntity.parentEntityId)) {
const parentEntity = optimisticFlatParentEntityMaps.byId[flatMyEntity.parentEntityId];
if (!isDefined(parentEntity)) {
errors.push({
code: MyEntityExceptionCode.PARENT_ENTITY_NOT_FOUND,
message: t`Parent entity with ID ${flatMyEntity.parentEntityId} not found`,
userFriendlyMessage: msg`The specified parent entity does not exist`,
});
} else if (isDefined(parentEntity.deletedAt)) {
errors.push({
code: MyEntityExceptionCode.PARENT_ENTITY_DELETED,
message: t`Parent entity is deleted`,
userFriendlyMessage: msg`Cannot reference a deleted parent entity`,
});
}
}
// Pattern 4: Standard entity protection
if (flatMyEntity.isCustom === false) {
errors.push({
code: MyEntityExceptionCode.STANDARD_ENTITY_CANNOT_BE_CREATED,
message: t`Cannot create standard entity`,
userFriendlyMessage: msg`Standard entities can only be created by the system`,
});
}
return errors;
}
validateMyEntityForUpdate(
flatMyEntity: FlatMyEntity,
updates: Partial<FlatMyEntity>,
optimisticFlatMyEntityMaps: FlatMyEntityMaps,
): WorkspaceMigrationValidationError[] {
const errors: WorkspaceMigrationValidationError[] = [];
// Standard entity protection
if (flatMyEntity.isCustom === false) {
errors.push({
code: MyEntityExceptionCode.STANDARD_ENTITY_CANNOT_BE_UPDATED,
message: t`Cannot update standard entity`,
userFriendlyMessage: msg`Standard entities cannot be modified`,
});
return errors; // Early return if standard
}
// Uniqueness check for name changes
if (isDefined(updates.name) && updates.name !== flatMyEntity.name) {
const existingEntityWithName = optimisticFlatMyEntityMaps.byName[updates.name];
if (isDefined(existingEntityWithName) && existingEntityWithName.id !== flatMyEntity.id) {
errors.push({
code: MyEntityExceptionCode.MY_ENTITY_ALREADY_EXISTS,
message: t`Entity with name ${updates.name} already exists`,
userFriendlyMessage: msg`An entity with this name already exists`,
});
}
}
return errors;
}
validateMyEntityForDelete(
flatMyEntity: FlatMyEntity,
): WorkspaceMigrationValidationError[] {
const errors: WorkspaceMigrationValidationError[] = [];
// Standard entity protection
if (flatMyEntity.isCustom === false) {
errors.push({
code: MyEntityExceptionCode.STANDARD_ENTITY_CANNOT_BE_DELETED,
message: t`Cannot delete standard entity`,
userFriendlyMessage: msg`Standard entities cannot be deleted`,
});
}
return errors;
}
}
```
**Performance warning**: Avoid `Object.values().find()` - use indexed maps instead!
```typescript
// ❌ BAD: O(n) - slow for large datasets
const duplicate = Object.values(optimisticFlatMyEntityMaps.byId).find(
(entity) => entity.name === flatMyEntity.name && entity.id !== flatMyEntity.id
);
// ✅ GOOD: O(1) - use indexed map
const existingEntityWithName = optimisticFlatMyEntityMaps.byName[flatMyEntity.name];
if (isDefined(existingEntityWithName) && existingEntityWithName.id !== flatMyEntity.id) {
// Handle duplicate
}
```
---
## Step 2: Create Builder Service
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/workspace-migration-my-entity-actions-builder.service.ts`
```typescript
import { Injectable } from '@nestjs/common';
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-entity-migration-builder.service';
import { FlatMyEntityValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-my-entity-validator.service';
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
import {
type UniversalCreateMyEntityAction,
type UniversalUpdateMyEntityAction,
type UniversalDeleteMyEntityAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type';
@Injectable()
export class WorkspaceMigrationMyEntityActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
'myEntity',
UniversalFlatMyEntity,
UniversalCreateMyEntityAction,
UniversalUpdateMyEntityAction,
UniversalDeleteMyEntityAction
> {
constructor(
private readonly flatMyEntityValidatorService: FlatMyEntityValidatorService,
) {
super();
}
protected buildCreateAction(
universalFlatMyEntity: UniversalFlatMyEntity,
flatEntityMaps: AllFlatEntityMapsByMetadataName,
): BuildWorkspaceMigrationActionReturnType<UniversalCreateMyEntityAction> {
const validationResult = this.flatMyEntityValidatorService.validateMyEntityForCreate(
universalFlatMyEntity,
flatEntityMaps.flatMyEntityMaps,
);
if (validationResult.length > 0) {
return {
status: 'failed',
errors: validationResult,
};
}
return {
status: 'success',
action: {
type: 'create',
metadataName: 'myEntity',
universalFlatEntity: universalFlatMyEntity,
},
};
}
protected buildUpdateAction(
universalFlatMyEntity: UniversalFlatMyEntity,
universalUpdates: Partial<UniversalFlatMyEntity>,
flatEntityMaps: AllFlatEntityMapsByMetadataName,
): BuildWorkspaceMigrationActionReturnType<UniversalUpdateMyEntityAction> {
const validationResult = this.flatMyEntityValidatorService.validateMyEntityForUpdate(
universalFlatMyEntity,
universalUpdates,
flatEntityMaps.flatMyEntityMaps,
);
if (validationResult.length > 0) {
return {
status: 'failed',
errors: validationResult,
};
}
return {
status: 'success',
action: {
type: 'update',
metadataName: 'myEntity',
universalFlatEntity: universalFlatMyEntity,
universalUpdates,
},
};
}
protected buildDeleteAction(
universalFlatMyEntity: UniversalFlatMyEntity,
): BuildWorkspaceMigrationActionReturnType<UniversalDeleteMyEntityAction> {
const validationResult = this.flatMyEntityValidatorService.validateMyEntityForDelete(
universalFlatMyEntity,
);
if (validationResult.length > 0) {
return {
status: 'failed',
errors: validationResult,
};
}
return {
status: 'success',
action: {
type: 'delete',
metadataName: 'myEntity',
universalFlatEntity: universalFlatMyEntity,
},
};
}
}
```
---
## Step 3: Wire into Orchestrator (**CRITICAL**)
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-build-orchestrator.service.ts`
```typescript
@Injectable()
export class WorkspaceMigrationBuildOrchestratorService {
constructor(
// ... existing builders
private readonly workspaceMigrationMyEntityActionsBuilderService: WorkspaceMigrationMyEntityActionsBuilderService,
) {}
async buildWorkspaceMigration({
allFlatEntityOperationByMetadataName,
flatEntityMaps,
isSystemBuild,
}: BuildWorkspaceMigrationInput): Promise<BuildWorkspaceMigrationOutput> {
// ... existing code
// Add your entity builder
const myEntityResult = await this.workspaceMigrationMyEntityActionsBuilderService.build({
flatEntitiesToCreate: allFlatEntityOperationByMetadataName.myEntity?.flatEntityToCreate ?? [],
flatEntitiesToUpdate: allFlatEntityOperationByMetadataName.myEntity?.flatEntityToUpdate ?? [],
flatEntitiesToDelete: allFlatEntityOperationByMetadataName.myEntity?.flatEntityToDelete ?? [],
flatEntityMaps,
isSystemBuild,
});
// ... aggregate errors
return {
status: aggregatedErrors.length > 0 ? 'failed' : 'success',
errors: aggregatedErrors,
actions: [
...existingActions,
...myEntityResult.actions,
],
};
}
}
```
**⚠️ This step is the most commonly forgotten!** Your entity won't sync without orchestrator wiring.
---
## Validation Patterns
### Pattern 1: Required Field
```typescript
if (!isDefined(field) || field.trim() === '') {
errors.push({ code: ..., message: ..., userFriendlyMessage: ... });
}
```
### Pattern 2: Uniqueness (O(1) lookup)
```typescript
const existing = optimisticMaps.byName[entity.name];
if (isDefined(existing) && existing.id !== entity.id) {
errors.push({ ... });
}
```
### Pattern 3: Foreign Key Validation
```typescript
if (isDefined(entity.parentId)) {
const parent = parentMaps.byId[entity.parentId];
if (!isDefined(parent)) {
errors.push({ code: NOT_FOUND, ... });
} else if (isDefined(parent.deletedAt)) {
errors.push({ code: DELETED, ... });
}
}
```
### Pattern 4: Standard Entity Protection
```typescript
if (entity.isCustom === false) {
errors.push({ code: STANDARD_ENTITY_PROTECTED, ... });
return errors; // Early return
}
```
---
## Checklist
Before moving to Step 4:
- [ ] Validator service created
- [ ] Validator **never throws** (returns error arrays)
- [ ] Validator **never mutates** (uses optimistic maps)
- [ ] All uniqueness checks use indexed maps (O(1))
- [ ] Required field validation implemented
- [ ] Foreign key validation implemented
- [ ] Standard entity protection implemented
- [ ] Builder service extends `WorkspaceEntityMigrationBuilderService`
- [ ] Builder creates actions with universal entities
- [ ] **Builder wired into orchestrator** (**CRITICAL**)
- [ ] **Builder injected in orchestrator constructor**
- [ ] **Builder called in `buildWorkspaceMigration`**
- [ ] **Actions added to orchestrator return statement**
---
## Next Step
Once builder and validation are complete, proceed to:
**[Syncable Entity: Runner & Actions (Step 4/6)](../syncable-entity-runner-and-actions/SKILL.md)**
For complete workflow, see `@creating-syncable-entity` rule.
@@ -0,0 +1,303 @@
---
name: syncable-entity-cache-and-transform
description: Create cache services and transformation utilities for syncable entities in Twenty. Use when implementing entity-to-flat conversions, input DTO transpilation to universal flat entities, or cache recomputation for syncable entities.
---
# Syncable Entity: Cache & Transform (Step 2/6)
**Purpose**: Create cache layer and transformation utilities to convert between different entity representations.
**When to use**: After completing Step 1 (Types & Constants). Required before building validators and action handlers.
---
## Quick Start
This step creates:
1. Cache service for flat entity maps
2. Entity-to-flat conversion utility
3. Input transform utils (DTO → Universal Flat Entity)
**Key principle**: Input transform utils must output **universal flat entities** (with `universalIdentifier` and foreign keys mapped to universal identifiers).
---
## Step 1: Create Cache Service
**File**: `src/engine/metadata-modules/flat-my-entity/services/flat-my-entity-cache.service.ts`
```typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { WorkspaceCache } from 'src/engine/twenty-orm/decorators/workspace-cache.decorator';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { type FlatMyEntityMaps } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity-maps.type';
import { fromMyEntityEntityToFlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/utils/from-my-entity-entity-to-flat-my-entity.util';
@Injectable()
export class FlatMyEntityCacheService {
constructor(
@InjectRepository(MyEntityEntity, 'metadata')
private readonly myEntityRepository: Repository<MyEntityEntity>,
) {}
@WorkspaceCache({ flatMapsKey: 'flatMyEntityMaps' })
async getFlatMyEntityMaps(): Promise<FlatMyEntityMaps> {
const myEntities = await this.myEntityRepository.find({
withDeleted: true, // CRITICAL: Include soft-deleted entities
});
const flatMyEntities = myEntities.map((entity) =>
fromMyEntityEntityToFlatMyEntity(entity),
);
return {
byId: Object.fromEntries(flatMyEntities.map((e) => [e.id, e])),
byName: Object.fromEntries(flatMyEntities.map((e) => [e.name, e])),
};
}
}
```
**Critical rules**:
- Use `@WorkspaceCache` decorator with unique `flatMapsKey`
- **Always** use `withDeleted: true` to include soft-deleted entities
- Cache key pattern: `flat{EntityName}Maps` (camelCase)
---
## Step 2: Entity-to-Flat Conversion
**File**: `src/engine/metadata-modules/flat-my-entity/utils/from-my-entity-entity-to-flat-my-entity.util.ts`
```typescript
import { v4 } from 'uuid';
import { type MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
export const fromMyEntityEntityToFlatMyEntity = (
entity: MyEntityEntity,
): FlatMyEntity => {
return {
id: entity.id,
// Critical: generate a new UUID for universalIdentifier
universalIdentifier: v4(),
workspaceId: entity.workspaceId,
applicationId: entity.applicationId,
name: entity.name,
label: entity.label,
description: entity.description,
isCustom: entity.isCustom,
parentEntityId: entity.parentEntityId,
settings: entity.settings,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
deletedAt: entity.deletedAt?.toISOString() ?? null,
};
};
```
**Critical**: `universalIdentifier` must be a new UUID generated with `v4()` (not `entity.id`)
---
## Step 3: Input Transform Utils (DTO → Universal Flat Entity)
**File**: `src/engine/metadata-modules/flat-my-entity/utils/from-create-my-entity-input-to-universal-flat-my-entity.util.ts`
```typescript
import { v4 } from 'uuid';
import { sanitizeString } from 'twenty-shared/string';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
import { type AllFlatEntityMapsByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps-by-metadata-name.type';
export const fromCreateMyEntityInputToUniversalFlatMyEntity = ({
input,
workspaceId,
flatEntityMaps,
}: {
input: CreateMyEntityInput;
workspaceId: string;
flatEntityMaps?: AllFlatEntityMapsByMetadataName;
}): UniversalFlatMyEntity => {
const id = v4();
const universalIdentifier = v4();
// 1. Extract foreign key IDs BEFORE sanitization
const parentEntityId = input.parentEntityId ?? null;
// 2. Sanitize string properties
const name = sanitizeString(input.name);
const label = sanitizeString(input.label);
const description = input.description ? sanitizeString(input.description) : null;
// 3. Build base flat entity
const baseFlatEntity = {
id,
universalIdentifier,
workspaceId,
applicationId: null,
name,
label,
description,
isCustom: true,
parentEntityId,
settings: input.settings ?? null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
};
// 4. Resolve foreign keys to universal identifiers (if flatEntityMaps provided)
if (flatEntityMaps) {
return resolveEntityRelationUniversalIdentifiers({
metadataName: 'myEntity',
flatEntity: baseFlatEntity,
flatEntityMaps,
});
}
// 5. Return with null universal foreign keys if no maps
return {
...baseFlatEntity,
parentEntityUniversalIdentifier: null,
};
};
```
**Key steps**:
1. Generate IDs (`id` and `universalIdentifier` with `v4()`)
2. Extract foreign keys **before** sanitization
3. Sanitize all string properties
4. Build base flat entity
5. Resolve foreign keys → universal identifiers
---
## Step 4: Create Flat Entity Module
**File**: `src/engine/metadata-modules/flat-my-entity/flat-my-entity.module.ts`
```typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { FlatMyEntityCacheService } from 'src/engine/metadata-modules/flat-my-entity/services/flat-my-entity-cache.service';
@Module({
imports: [TypeOrmModule.forFeature([MyEntityEntity], 'metadata')],
providers: [FlatMyEntityCacheService],
exports: [FlatMyEntityCacheService],
})
export class FlatMyEntityModule {}
```
**Rules**:
- Import entity with `'metadata'` datasource
- Export cache service for use in other modules
---
## Common Patterns
### Pattern: Foreign Key Resolution
```typescript
// Extract foreign keys BEFORE sanitization
const parentEntityId = input.parentEntityId ?? null;
// After building base entity, resolve to universal identifiers
const universalFlatEntity = resolveEntityRelationUniversalIdentifiers({
metadataName: 'myEntity',
flatEntity: baseFlatEntity,
flatEntityMaps,
});
```
### Pattern: JSONB with SerializedRelation
```typescript
// For JSONB properties containing foreign keys
const settings = input.settings
? {
...input.settings,
fieldMetadataId: input.settings.fieldMetadataId,
}
: null;
// After resolution, JSONB foreign keys become universal identifiers
return resolveEntityRelationUniversalIdentifiers({
metadataName: 'myEntity',
flatEntity: { ...baseFlatEntity, settings },
flatEntityMaps,
});
```
### Pattern: Update Transform
```typescript
// from-update-my-entity-input-to-universal-flat-my-entity-updates.util.ts
export const fromUpdateMyEntityInputToUniversalFlatMyEntityUpdates = ({
input,
flatEntityMaps,
}: {
input: UpdateMyEntityInput;
flatEntityMaps?: AllFlatEntityMapsByMetadataName;
}): Partial<UniversalFlatMyEntity> => {
const updates: Partial<UniversalFlatMyEntity> = {};
if (input.name !== undefined) {
updates.name = sanitizeString(input.name);
}
if (input.parentEntityId !== undefined) {
updates.parentEntityId = input.parentEntityId;
}
updates.updatedAt = new Date().toISOString();
// Resolve foreign keys if maps provided
if (flatEntityMaps) {
return resolveEntityRelationUniversalIdentifiers({
metadataName: 'myEntity',
flatEntity: updates as any,
flatEntityMaps,
});
}
return updates;
};
```
---
## Checklist
Before moving to Step 3:
- [ ] Cache service created with `@WorkspaceCache` decorator
- [ ] Cache uses `withDeleted: true`
- [ ] Cache key follows `flat{EntityName}Maps` pattern
- [ ] Entity-to-flat conversion implemented
- [ ] `universalIdentifier` set correctly (generated with `v4()`)
- [ ] Create input transform implemented
- [ ] Update input transform implemented (if needed)
- [ ] Foreign keys extracted before sanitization
- [ ] String properties sanitized
- [ ] Foreign keys resolved to universal identifiers
- [ ] Flat entity module created and exports cache service
---
## Next Step
Once cache and transform utilities are complete, proceed to:
**[Syncable Entity: Builder & Validation (Step 3/6)](../syncable-entity-builder-and-validation/SKILL.md)**
For complete workflow, see `@creating-syncable-entity` rule.
@@ -0,0 +1,326 @@
---
name: syncable-entity-integration
description: Wire syncable entity services into NestJS modules, create service layer and resolvers for Twenty entities. Use when registering builders, validators, and action handlers in modules, creating business services, or exposing entities via GraphQL API with proper exception handling.
---
# Syncable Entity: Integration (Step 5/6)
**Purpose**: Wire everything together, register in modules, create services and resolvers.
**When to use**: After completing Steps 1-4 (all previous steps). Required before testing.
---
## Quick Start
This step:
1. Registers services in 3 NestJS modules
2. Creates service layer (returns flat entities)
3. Creates resolver layer (converts flat → DTO)
4. Uses exception interceptor for GraphQL
**Key principle**: Services return flat entities, resolvers transpile flat → DTO.
---
## Step 1: Register in Builder Module
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts`
```typescript
import { WorkspaceMigrationMyEntityActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/workspace-migration-my-entity-actions-builder.service';
@Module({
imports: [
// ... existing imports
],
providers: [
// ... existing providers
WorkspaceMigrationMyEntityActionsBuilderService,
],
exports: [
// ... existing exports
WorkspaceMigrationMyEntityActionsBuilderService,
],
})
export class WorkspaceMigrationBuilderModule {}
```
**Important**: Add to both `providers` AND `exports` (builder needs to be exported for orchestrator).
---
## Step 2: Register in Validators Module
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts`
```typescript
import { FlatMyEntityValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-my-entity-validator.service';
@Module({
imports: [
// ... existing imports
],
providers: [
// ... existing providers
FlatMyEntityValidatorService,
],
exports: [
// ... existing exports
FlatMyEntityValidatorService,
],
})
export class WorkspaceMigrationBuilderValidatorsModule {}
```
---
## Step 3: Register Action Handlers
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts`
```typescript
import { CreateMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/create-my-entity-action-handler.service';
import { UpdateMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/update-my-entity-action-handler.service';
import { DeleteMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/delete-my-entity-action-handler.service';
@Module({
imports: [
// ... existing imports
],
providers: [
// ... existing providers
CreateMyEntityActionHandlerService,
UpdateMyEntityActionHandlerService,
DeleteMyEntityActionHandlerService,
],
exports: [
// ... existing exports (action handlers typically not exported)
],
})
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
```
**Note**: Action handlers are typically only in `providers`, not `exports`.
---
## Step 4: Create Service Layer
**File**: `src/engine/metadata-modules/my-entity/my-entity.service.ts`
```typescript
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { fromCreateMyEntityInputToUniversalFlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/utils/from-create-my-entity-input-to-universal-flat-my-entity.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class MyEntityService {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async create(input: CreateMyEntityInput, workspaceId: string): Promise<FlatMyEntity> {
// 1. Transform input to universal flat entity
const universalFlatMyEntityToCreate = fromCreateMyEntityInputToUniversalFlatMyEntity({
input,
workspaceId,
});
// 2. Validate, build, and run
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
myEntity: {
flatEntityToCreate: [universalFlatMyEntityToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
// 3. Throw if validation failed
if (isDefined(result)) {
throw new WorkspaceMigrationBuilderException(
result,
'Validation errors occurred while creating entity',
);
}
// 4. Return freshly cached flat entity
const { flatMyEntityMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatMyEntityMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: universalFlatMyEntityToCreate.id,
flatEntityMaps: flatMyEntityMaps,
});
}
}
```
**Service pattern**:
1. Transform input → universal flat entity
2. Call `validateBuildAndRunWorkspaceMigration`
3. Throw if validation errors
4. **Return flat entity** (not DTO)
---
## Step 5: Create Resolver Layer
**File**: `src/engine/metadata-modules/my-entity/my-entity.resolver.ts`
```typescript
import { UseInterceptors } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { MyEntityService } from 'src/engine/metadata-modules/my-entity/my-entity.service';
import { fromFlatMyEntityToMyEntityDto } from 'src/engine/metadata-modules/my-entity/utils/from-flat-my-entity-to-my-entity-dto.util';
@Resolver(() => MyEntityDto)
@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor)
export class MyEntityResolver {
constructor(private readonly myEntityService: MyEntityService) {}
@Mutation(() => MyEntityDto)
async createMyEntity(
@Args('input') input: CreateMyEntityInput,
@Workspace() { id: workspaceId }: Workspace,
): Promise<MyEntityDto> {
// Service returns flat entity
const flatMyEntity = await this.myEntityService.create(input, workspaceId);
// Resolver converts flat entity to DTO
return fromFlatMyEntityToMyEntityDto(flatMyEntity);
}
@Mutation(() => MyEntityDto)
async updateMyEntity(
@Args('id') id: string,
@Args('input') input: UpdateMyEntityInput,
@Workspace() { id: workspaceId }: Workspace,
): Promise<MyEntityDto> {
const flatMyEntity = await this.myEntityService.update(id, input, workspaceId);
return fromFlatMyEntityToMyEntityDto(flatMyEntity);
}
@Mutation(() => Boolean)
async deleteMyEntity(
@Args('id') id: string,
@Workspace() { id: workspaceId }: Workspace,
) {
await this.myEntityService.delete(id, workspaceId);
return true;
}
}
```
**Resolver responsibilities**:
- Receives flat entities from service
- **Converts flat → DTO** using conversion utility
- Returns DTOs to GraphQL API
- Uses exception interceptor for error formatting
---
## Step 6: Flat-to-DTO Conversion
**File**: `src/engine/metadata-modules/my-entity/utils/from-flat-my-entity-to-my-entity-dto.util.ts`
```typescript
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
import { type MyEntityDto } from 'src/engine/metadata-modules/my-entity/dtos/my-entity.dto';
export const fromFlatMyEntityToMyEntityDto = (
flatMyEntity: FlatMyEntity,
): MyEntityDto => {
return {
id: flatMyEntity.id,
name: flatMyEntity.name,
label: flatMyEntity.label,
description: flatMyEntity.description,
isCustom: flatMyEntity.isCustom,
createdAt: flatMyEntity.createdAt,
updatedAt: flatMyEntity.updatedAt,
// Convert foreign key IDs to relation objects if needed
// parentEntity: flatMyEntity.parentEntityId ? { id: flatMyEntity.parentEntityId } : null,
};
};
```
---
## Layer Responsibilities
| Layer | Input | Output | Responsibility |
|-------|-------|--------|----------------|
| **Service** | Input DTO | Flat Entity | Business logic, validation orchestration |
| **Resolver** | Service result | DTO | Flat → DTO conversion, GraphQL exposure |
**Service Layer**:
- Works with flat entities internally
- Returns `FlatMyEntity` type
- No knowledge of DTOs or GraphQL types
**Resolver Layer**:
- Receives flat entities from service
- Converts flat entities to DTOs
- Returns DTOs to GraphQL API
---
## Exception Interceptor
The `WorkspaceMigrationGraphqlApiExceptionInterceptor` automatically handles:
1. `FlatEntityMapsException` → Converts to GraphQL errors (NotFoundError, etc.)
2. `WorkspaceMigrationBuilderException` → Formats validation errors with i18n
3. `WorkspaceMigrationRunnerException` → Formats runner errors
**What it does**:
- Catches exceptions and formats for API responses
- Translates error messages based on user locale
- Ensures consistent error structure for frontend
---
## Checklist
Before moving to Step 6 (Testing):
- [ ] Builder registered in builder module (providers + exports)
- [ ] Validator registered in validators module (providers + exports)
- [ ] All 3 action handlers registered in action handlers module (providers)
- [ ] Service layer created
- [ ] Service returns flat entities (not DTOs)
- [ ] Resolver layer created
- [ ] Resolver uses exception interceptor
- [ ] Resolver converts flat → DTO
- [ ] Flat-to-DTO conversion utility created
---
## Next Step
Once integration is complete, proceed to (**MANDATORY**):
**[Syncable Entity: Integration Testing (Step 6/6)](../syncable-entity-testing/SKILL.md)**
For complete workflow, see `@creating-syncable-entity` rule.
@@ -0,0 +1,355 @@
---
name: syncable-entity-runner-and-actions
description: Implement action handlers for executing workspace migrations in Twenty. Use when creating database operations for syncable entities, implementing universal-to-flat entity transpilation, or handling create/update/delete actions in the runner layer.
---
# Syncable Entity: Runner & Actions (Step 4/6)
**Purpose**: Execute migration actions against the database with proper transpilation from universal to flat entities.
**When to use**: After completing Steps 1-3 (Types, Cache, Builder). Required before integration.
---
## Quick Start
This step creates:
1. Create action handler
2. Update action handler
3. Delete action handler
4. Universal-to-flat conversion utilities
**Key pattern**: Each handler has two phases:
1. **Transpilation**: Universal action → Flat action
2. **Execution**: Flat action → Database operation
---
## Step 1: Create Action Handler
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/create-my-entity-action-handler.service.ts`
```typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceCreateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-create-action-handler.service';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
import {
type UniversalCreateMyEntityAction,
type FlatCreateMyEntityAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type';
@Injectable()
export class CreateMyEntityActionHandlerService extends WorkspaceCreateActionHandlerService<
'myEntity',
UniversalCreateMyEntityAction,
FlatCreateMyEntityAction
> {
constructor(
@InjectRepository(MyEntityEntity, 'metadata')
private readonly myEntityRepository: Repository<MyEntityEntity>,
) {
super();
}
// Phase 1: Transpile universal action to flat action
protected transpileUniversalActionToFlatAction(
universalAction: UniversalCreateMyEntityAction,
flatEntityMaps: AllFlatEntityMapsByMetadataName,
): FlatCreateMyEntityAction {
return {
type: 'create',
metadataName: 'myEntity',
flatEntity: fromUniversalFlatMyEntityToFlatMyEntity(
universalAction.universalFlatEntity,
flatEntityMaps,
),
};
}
// Phase 2: Execute flat action against database
protected async executeForMetadata(
flatActions: FlatCreateMyEntityAction[],
): Promise<void> {
const flatEntities = flatActions.map((action) => action.flatEntity);
await this.insertFlatEntitiesInRepository({
repository: this.myEntityRepository,
flatEntities,
});
}
protected async executeForWorkspaceSchema(): Promise<void> {
// No workspace schema changes needed for metadata-only entity
return;
}
}
```
**Key helper methods**:
- `transpileUniversalActionToFlatAction`: Converts universal → flat
- `insertFlatEntitiesInRepository`: Base class helper for inserts
- `executeForMetadata`: Metadata database operations
- `executeForWorkspaceSchema`: Workspace schema changes (if needed)
---
## Step 2: Update Action Handler
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/update-my-entity-action-handler.service.ts`
```typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceUpdateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-update-action-handler.service';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
@Injectable()
export class UpdateMyEntityActionHandlerService extends WorkspaceUpdateActionHandlerService<
'myEntity',
UniversalUpdateMyEntityAction,
FlatUpdateMyEntityAction
> {
constructor(
@InjectRepository(MyEntityEntity, 'metadata')
private readonly myEntityRepository: Repository<MyEntityEntity>,
) {
super();
}
protected transpileUniversalActionToFlatAction(
universalAction: UniversalUpdateMyEntityAction,
flatEntityMaps: AllFlatEntityMapsByMetadataName,
): FlatUpdateMyEntityAction {
const flatEntity = fromUniversalFlatMyEntityToFlatMyEntity(
universalAction.universalFlatEntity,
flatEntityMaps,
);
// Resolve universal foreign keys in updates to regular IDs
const flatUpdates = resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'myEntity',
universalUpdates: universalAction.universalUpdates,
flatEntityMaps,
});
return {
type: 'update',
metadataName: 'myEntity',
flatEntity,
updates: flatUpdates,
};
}
protected async executeForMetadata(
flatActions: FlatUpdateMyEntityAction[],
): Promise<void> {
for (const action of flatActions) {
await this.myEntityRepository.update(
{ id: action.flatEntity.id },
action.updates,
);
}
}
protected async executeForWorkspaceSchema(): Promise<void> {
return;
}
}
```
**Update-specific helper**:
- `resolveUniversalUpdateRelationIdentifiersToIds`: Maps universal identifiers back to regular IDs in the updates object
---
## Step 3: Delete Action Handler
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/delete-my-entity-action-handler.service.ts`
```typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceDeleteActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-delete-action-handler.service';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
@Injectable()
export class DeleteMyEntityActionHandlerService extends WorkspaceDeleteActionHandlerService<
'myEntity',
UniversalDeleteMyEntityAction,
FlatDeleteMyEntityAction
> {
constructor(
@InjectRepository(MyEntityEntity, 'metadata')
private readonly myEntityRepository: Repository<MyEntityEntity>,
) {
super();
}
protected transpileUniversalActionToFlatAction(
universalAction: UniversalDeleteMyEntityAction,
flatEntityMaps: AllFlatEntityMapsByMetadataName,
): FlatDeleteMyEntityAction {
// Use base class helper for delete transpilation
return this.transpileUniversalDeleteActionToFlatDeleteAction({
universalAction,
flatEntityMaps,
fromUniversalFlatEntityToFlatEntity: fromUniversalFlatMyEntityToFlatMyEntity,
});
}
protected async executeForMetadata(
flatActions: FlatDeleteMyEntityAction[],
): Promise<void> {
const ids = flatActions.map((action) => action.flatEntity.id);
await this.myEntityRepository.delete(ids);
}
protected async executeForWorkspaceSchema(): Promise<void> {
return;
}
}
```
**Delete-specific helper**:
- `transpileUniversalDeleteActionToFlatDeleteAction`: Base class helper that handles standard delete transpilation
---
## Step 4: Universal-to-Flat Conversion
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util.ts`
```typescript
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
import { type AllFlatEntityMapsByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps-by-metadata-name.type';
export const fromUniversalFlatMyEntityToFlatMyEntity = (
universalFlatMyEntity: UniversalFlatMyEntity,
flatEntityMaps: AllFlatEntityMapsByMetadataName,
): FlatMyEntity => {
// Resolve universal foreign keys back to regular IDs
return resolveUniversalRelationIdentifiersToIds({
metadataName: 'myEntity',
universalFlatEntity: universalFlatMyEntity,
flatEntityMaps,
}) as FlatMyEntity;
};
```
**Key utility**:
- `resolveUniversalRelationIdentifiersToIds`: Maps universal identifiers → regular IDs (reverse of `resolveEntityRelationUniversalIdentifiers`)
---
## Action Handler Patterns
### Pattern: Create Handler
```typescript
// 1. Transpile: Universal → Flat
protected transpileUniversalActionToFlatAction(
universalAction,
flatEntityMaps,
) {
return {
type: 'create',
metadataName: 'myEntity',
flatEntity: fromUniversalFlatMyEntityToFlatMyEntity(
universalAction.universalFlatEntity,
flatEntityMaps,
),
};
}
// 2. Execute: Flat → Database
protected async executeForMetadata(flatActions) {
await this.insertFlatEntitiesInRepository({
repository: this.myEntityRepository,
flatEntities: flatActions.map(a => a.flatEntity),
});
}
```
### Pattern: Update Handler
```typescript
// Transpile with update-specific resolution
protected transpileUniversalActionToFlatAction(
universalAction,
flatEntityMaps,
) {
const flatEntity = fromUniversalFlatMyEntityToFlatMyEntity(
universalAction.universalFlatEntity,
flatEntityMaps,
);
const flatUpdates = resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'myEntity',
universalUpdates: universalAction.universalUpdates,
flatEntityMaps,
});
return { type: 'update', metadataName: 'myEntity', flatEntity, updates: flatUpdates };
}
```
### Pattern: Delete Handler
```typescript
// Use base class helper
protected transpileUniversalActionToFlatAction(
universalAction,
flatEntityMaps,
) {
return this.transpileUniversalDeleteActionToFlatDeleteAction({
universalAction,
flatEntityMaps,
fromUniversalFlatEntityToFlatEntity: fromUniversalFlatMyEntityToFlatMyEntity,
});
}
// Delete
protected async executeForMetadata(flatActions) {
const ids = flatActions.map(a => a.flatEntity.id);
await this.myEntityRepository.delete(ids);
}
```
---
## Checklist
Before moving to Step 5:
- [ ] Create action handler implemented
- [ ] Update action handler implemented
- [ ] Delete action handler implemented
- [ ] All handlers extend appropriate base class
- [ ] `transpileUniversalActionToFlatAction` implemented in all handlers
- [ ] `executeForMetadata` implemented in all handlers
- [ ] `executeForWorkspaceSchema` implemented (or returns empty)
- [ ] Universal-to-flat conversion utility created
- [ ] Create handler uses `insertFlatEntitiesInRepository`
- [ ] Update handler uses `resolveUniversalUpdateRelationIdentifiersToIds`
- [ ] Delete handler uses `transpileUniversalDeleteActionToFlatDeleteAction`
- [ ] Delete handler uses hard delete (`delete()`)
---
## Next Step
Once action handlers are complete, proceed to:
**[Syncable Entity: Integration (Step 5/6)](../syncable-entity-integration/SKILL.md)**
For complete workflow, see `@creating-syncable-entity` rule.
@@ -0,0 +1,494 @@
---
name: syncable-entity-testing
description: Create comprehensive integration tests for syncable entities in Twenty. Use when writing integration tests for metadata entities, covering validator exceptions, input transpilation errors, and CRUD operations. Tests are MANDATORY for all syncable entities.
---
# Syncable Entity: Integration Testing (Step 6/6 - MANDATORY)
**Purpose**: Create comprehensive test suite covering all validation scenarios, input transpilation exceptions, and successful use cases.
**When to use**: After completing Steps 1-5. Integration tests are **REQUIRED** for all syncable entities.
---
## Quick Start
Tests must cover:
1. **Failing scenarios** - All validator exceptions and input transpilation errors
2. **Successful scenarios** - All CRUD operations and edge cases
3. **Test utilities** - Reusable query factories and helper functions
**Test pattern**: Two-file pattern (query factory + wrapper) for each operation.
---
## Step 1: Create Test Utilities
### Pattern: Query Factory
**File**: `test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util.ts`
```typescript
import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
export type CreateMyEntityFactoryInput = CreateMyEntityInput;
const DEFAULT_MY_ENTITY_GQL_FIELDS = `
id
name
label
description
isCustom
createdAt
updatedAt
`;
export const createMyEntityQueryFactory = ({
input,
gqlFields = DEFAULT_MY_ENTITY_GQL_FIELDS,
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>) => ({
query: gql`
mutation CreateMyEntity($input: CreateMyEntityInput!) {
createMyEntity(input: $input) {
${gqlFields}
}
}
`,
variables: {
input,
},
});
```
### Pattern: Wrapper Utility
**File**: `test/integration/metadata/suites/my-entity/utils/create-my-entity.util.ts`
```typescript
import {
type CreateMyEntityFactoryInput,
createMyEntityQueryFactory,
} from 'test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type MyEntityDto } from 'src/engine/metadata-modules/my-entity/dtos/my-entity.dto';
export const createMyEntity = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>): CommonResponseBody<{
createMyEntity: MyEntityDto;
}> => {
const graphqlOperation = createMyEntityQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'My entity creation should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'My entity creation has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
```
**Required utilities** (follow same pattern):
- `update-my-entity-query-factory.util.ts` + `update-my-entity.util.ts`
- `delete-my-entity-query-factory.util.ts` + `delete-my-entity.util.ts`
---
## Step 2: Failing Creation Tests
**File**: `test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts`
```typescript
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { isDefined } from 'twenty-shared/utils';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
type TestContext = {
input: CreateMyEntityInput;
};
type GlobalTestContext = {
existingEntityLabel: string;
existingEntityName: string;
};
const globalTestContext: GlobalTestContext = {
existingEntityLabel: 'Existing Test Entity',
existingEntityName: 'existingTestEntity',
};
type CreateMyEntityTestingContext = EachTestingContext<TestContext>[];
describe('My entity creation should fail', () => {
let existingEntityId: string | undefined;
beforeAll(async () => {
// Setup: Create entity for uniqueness tests
const { data } = await createMyEntity({
expectToFail: false,
input: {
name: globalTestContext.existingEntityName,
label: globalTestContext.existingEntityLabel,
},
});
existingEntityId = data.createMyEntity.id;
});
afterAll(async () => {
// Cleanup
if (isDefined(existingEntityId)) {
await deleteMyEntity({
expectToFail: false,
input: { id: existingEntityId },
});
}
});
const failingMyEntityCreationTestCases: CreateMyEntityTestingContext = [
// Input transpilation validation
{
title: 'when name is missing',
context: {
input: {
label: 'Entity Missing Name',
} as CreateMyEntityInput,
},
},
{
title: 'when label is missing',
context: {
input: {
name: 'entityMissingLabel',
} as CreateMyEntityInput,
},
},
{
title: 'when name is empty string',
context: {
input: {
name: '',
label: 'Empty Name Entity',
},
},
},
// Validator business logic
{
title: 'when name already exists (uniqueness)',
context: {
input: {
name: globalTestContext.existingEntityName,
label: 'Duplicate Name Entity',
},
},
},
{
title: 'when trying to create standard entity',
context: {
input: {
name: 'myEntity',
label: 'Standard Entity',
isCustom: false,
} as CreateMyEntityInput,
},
},
// Foreign key validation
{
title: 'when parentEntityId does not exist',
context: {
input: {
name: 'invalidParentEntity',
label: 'Invalid Parent Entity',
parentEntityId: '00000000-0000-0000-0000-000000000000',
},
},
},
];
it.each(eachTestingContextFilter(failingMyEntityCreationTestCases))(
'$title',
async ({ context }) => {
const { errors } = await createMyEntity({
expectToFail: true,
input: context.input,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
},
);
});
```
**Test coverage requirements**:
- ✅ Missing required fields
- ✅ Empty strings
- ✅ Invalid format
- ✅ Uniqueness violations
- ✅ Standard entity protection
- ✅ Foreign key validation
---
## Step 3: Successful Creation Tests
**File**: `test/integration/metadata/suites/my-entity/successful-my-entity-creation.integration-spec.ts`
```typescript
import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
describe('My entity creation should succeed', () => {
let createdEntityId: string;
afterEach(async () => {
if (createdEntityId) {
await deleteMyEntity({
expectToFail: false,
input: { id: createdEntityId },
});
}
});
it('should create entity with minimal required input', async () => {
const { data } = await createMyEntity({
expectToFail: false,
input: {
name: 'minimalEntity',
label: 'Minimal Entity',
},
});
createdEntityId = data?.createMyEntity?.id;
expect(data.createMyEntity).toMatchObject({
id: expect.any(String),
name: 'minimalEntity',
label: 'Minimal Entity',
description: null,
isCustom: true,
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
});
it('should create entity with all optional fields', async () => {
const input = {
name: 'fullEntity',
label: 'Full Entity',
description: 'Entity with all fields specified',
} as const satisfies CreateMyEntityInput;
const { data } = await createMyEntity({
expectToFail: false,
input,
});
createdEntityId = data?.createMyEntity?.id;
expect(data.createMyEntity).toMatchObject({
id: expect.any(String),
name: 'fullEntity',
label: 'Full Entity',
description: 'Entity with all fields specified',
isCustom: true,
});
});
it('should sanitize input by trimming whitespace', async () => {
const { data } = await createMyEntity({
expectToFail: false,
input: {
name: ' entityWithSpaces ',
label: ' Entity With Spaces ',
description: ' Description with spaces ',
},
});
createdEntityId = data?.createMyEntity?.id;
expect(data.createMyEntity).toMatchObject({
id: expect.any(String),
name: 'entityWithSpaces',
label: 'Entity With Spaces',
description: 'Description with spaces',
});
});
it('should handle long text content', async () => {
const longDescription = 'A'.repeat(1000);
const { data } = await createMyEntity({
expectToFail: false,
input: {
name: 'longDescEntity',
label: 'Long Description Entity',
description: longDescription,
},
});
createdEntityId = data?.createMyEntity?.id;
expect(data.createMyEntity).toMatchObject({
id: expect.any(String),
description: longDescription,
});
});
});
```
**Test coverage requirements**:
- ✅ Minimal required input
- ✅ All optional fields
- ✅ Input sanitization
- ✅ Long text content
- ✅ Special characters
---
## Step 4: Update and Delete Tests
Create similar test files for update and delete operations:
**Required files**:
- `failing-my-entity-update.integration-spec.ts`
- `successful-my-entity-update.integration-spec.ts`
- `failing-my-entity-deletion.integration-spec.ts`
- `successful-my-entity-deletion.integration-spec.ts`
---
## Testing Best Practices
### Pattern: Cleanup
```typescript
afterEach(async () => {
if (createdEntityId) {
await deleteMyEntity({
expectToFail: false,
input: { id: createdEntityId },
});
}
});
```
### Pattern: Type-Safe Inputs
```typescript
const input = {
name: 'myEntity',
label: 'My Entity',
} as const satisfies CreateMyEntityInput;
```
### Pattern: Snapshot Testing
```typescript
expectOneNotInternalServerErrorSnapshot({
errors,
});
```
---
## Running Tests
```bash
# Run all entity tests
npx jest test/integration/metadata/suites/my-entity --config=packages/twenty-server/jest.config.mjs
# Run specific test file
npx jest test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts --config=packages/twenty-server/jest.config.mjs
# Update snapshots
npx jest test/integration/metadata/suites/my-entity --updateSnapshot --config=packages/twenty-server/jest.config.mjs
```
---
## Complete Test Checklist
### Test Utilities
- [ ] `create-my-entity-query-factory.util.ts` created
- [ ] `create-my-entity.util.ts` created
- [ ] `update-my-entity-query-factory.util.ts` created
- [ ] `update-my-entity.util.ts` created
- [ ] `delete-my-entity-query-factory.util.ts` created
- [ ] `delete-my-entity.util.ts` created
### Failing Tests Coverage
- [ ] Missing required fields
- [ ] Empty string validation
- [ ] Uniqueness violations
- [ ] Standard entity protection
- [ ] Foreign key validation
- [ ] JSONB property validation (if applicable)
### Successful Tests Coverage
- [ ] Create with minimal input
- [ ] Create with all optional fields
- [ ] Input sanitization (whitespace)
- [ ] Long text content
- [ ] Update single field
- [ ] Update multiple fields
- [ ] Successful deletion
### Snapshot Tests
- [ ] All failing tests use `expectOneNotInternalServerErrorSnapshot`
- [ ] Snapshots committed to `__snapshots__/` directory
---
## Success Criteria
Your integration tests are complete when:
✅ All test utilities created (minimum 6 files)
✅ Failing creation tests cover all validators
✅ Failing update tests cover business rules
✅ Failing deletion tests cover protection rules
✅ Successful tests cover all use cases
✅ All snapshots generated and committed
✅ All tests pass consistently
✅ Test coverage meets requirements (>80%)
---
## Final Step
**Step 6 Complete!** → Your syncable entity is fully tested and production-ready!
**Congratulations!** You've successfully created a new syncable entity in Twenty's workspace migration system.
For complete workflow, see `@creating-syncable-entity` rule.
@@ -0,0 +1,340 @@
---
name: syncable-entity-types-and-constants
description: Define types, entities, and central constant registrations for syncable entities in Twenty's workspace migration system. Use when creating new syncable entities, defining TypeORM entities, flat entity types, or registering in central constants (ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME, ALL_ONE_TO_MANY_METADATA_RELATIONS, ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY, ALL_MANY_TO_ONE_METADATA_RELATIONS).
---
# Syncable Entity: Types & Constants (Step 1/6)
**Purpose**: Define all types, entities, and register in central constants. This is the foundation - everything else depends on these types being correct.
**When to use**: First step when creating any new syncable entity. Must be completed before other steps.
---
## Quick Start
This step creates:
1. Metadata name constant (twenty-shared)
2. TypeORM entity (extends `SyncableEntity`)
3. Flat entity types
4. Action types (universal + flat)
5. Central constant registrations (5 constants)
---
## Step 1: Add Metadata Name
**File**: `packages/twenty-shared/src/metadata/all-metadata-name.constant.ts`
```typescript
export const ALL_METADATA_NAME = {
// ... existing entries
myEntity: 'myEntity',
} as const;
```
---
## Step 2: Create TypeORM Entity
**File**: `src/engine/metadata-modules/my-entity/entities/my-entity.entity.ts`
```typescript
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity({ name: 'myEntity' })
export class MyEntityEntity extends SyncableEntity {
@Column({ type: 'varchar' })
name: string;
@Column({ type: 'varchar' })
label: string;
@Column({ type: 'boolean', default: true })
isCustom: boolean;
// Foreign key example (optional)
@Column({ type: 'uuid', nullable: true })
parentEntityId: string | null;
@ManyToOne(() => ParentEntityEntity, { nullable: true })
@JoinColumn({ name: 'parentEntityId' })
parentEntity: ParentEntityEntity | null;
// JSONB column example (optional)
@Column({ type: 'jsonb', nullable: true })
settings: Record<string, any> | null;
}
```
**Key rules**:
- Must extend `SyncableEntity` (provides `id`, `universalIdentifier`, `applicationId`, etc.)
- Must have `isCustom` boolean column
- Use `@Column({ type: 'jsonb' })` for JSON data
---
## Step 3: Define Flat Entity Types
**File**: `src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type.ts`
```typescript
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
export type FlatMyEntity = FlatEntityFrom<MyEntityEntity>;
```
**Maps file** (if entity has indexed lookups):
```typescript
// flat-my-entity-maps.type.ts
export type FlatMyEntityMaps = {
byId: Record<string, FlatMyEntity>;
byName: Record<string, FlatMyEntity>;
// Add other indexes as needed
};
```
---
## Step 4: Define Editable Properties
**File**: `src/engine/metadata-modules/flat-my-entity/constants/editable-flat-my-entity-properties.constant.ts`
```typescript
export const EDITABLE_FLAT_MY_ENTITY_PROPERTIES = [
'name',
'label',
'description',
'parentEntityId',
'settings',
] as const satisfies ReadonlyArray<keyof FlatMyEntity>;
```
**Rule**: Only include properties that can be updated (exclude `id`, `createdAt`, `universalIdentifier`, etc.)
---
## Step 5: Define Action Types
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type.ts`
```typescript
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
// Universal actions (used by builder/runner)
export type UniversalCreateMyEntityAction = {
type: 'create';
metadataName: 'myEntity';
universalFlatEntity: UniversalFlatMyEntity;
};
export type UniversalUpdateMyEntityAction = {
type: 'update';
metadataName: 'myEntity';
universalFlatEntity: UniversalFlatMyEntity;
universalUpdates: Partial<UniversalFlatMyEntity>;
};
export type UniversalDeleteMyEntityAction = {
type: 'delete';
metadataName: 'myEntity';
universalFlatEntity: UniversalFlatMyEntity;
};
// Flat actions (internal to runner)
export type FlatCreateMyEntityAction = {
type: 'create';
metadataName: 'myEntity';
flatEntity: FlatMyEntity;
};
export type FlatUpdateMyEntityAction = {
type: 'update';
metadataName: 'myEntity';
flatEntity: FlatMyEntity;
updates: Partial<FlatMyEntity>;
};
export type FlatDeleteMyEntityAction = {
type: 'delete';
metadataName: 'myEntity';
flatEntity: FlatMyEntity;
};
```
---
## Step 6: Register in Central Constants
### 6a. AllFlatEntityTypesByMetadataName
**File**: `src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts`
```typescript
export type AllFlatEntityTypesByMetadataName = {
// ... existing entries
myEntity: {
flatEntityMaps: FlatMyEntityMaps;
universalActions: {
create: UniversalCreateMyEntityAction;
update: UniversalUpdateMyEntityAction;
delete: UniversalDeleteMyEntityAction;
};
flatActions: {
create: FlatCreateMyEntityAction;
update: FlatUpdateMyEntityAction;
delete: FlatDeleteMyEntityAction;
};
flatEntity: FlatMyEntity;
universalFlatEntity: UniversalFlatMyEntity;
entity: MyEntityEntity;
};
};
```
### 6b. ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME
**File**: `src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts`
```typescript
export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
// ... existing entries
myEntity: {
name: { toCompare: true },
label: { toCompare: true },
description: { toCompare: true },
parentEntityId: {
toCompare: true,
universalProperty: 'parentEntityUniversalIdentifier',
},
settings: {
toCompare: true,
toStringify: true,
universalProperty: 'universalSettings',
},
},
} as const;
```
**Rules**:
- `toCompare: true` → Editable property (checked for changes)
- `toStringify: true` → JSONB/object property (needs JSON serialization)
- `universalProperty` → Maps to universal version (for foreign keys & JSONB with `SerializedRelation`)
### 6c. ALL_ONE_TO_MANY_METADATA_RELATIONS
**File**: `src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts`
This constant is **type-checked** — values for `metadataName`, `flatEntityForeignKeyAggregator`, and `universalFlatEntityForeignKeyAggregator` are derived from entity type definitions. The aggregator names follow the pattern: remove trailing `'s'` from the relation property name, then append `Ids` or `UniversalIdentifiers`.
```typescript
export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
// ... existing entries
myEntity: {
// If myEntity has a `childEntities: ChildEntityEntity[]` property:
childEntities: {
metadataName: 'childEntity',
flatEntityForeignKeyAggregator: 'childEntityIds',
universalFlatEntityForeignKeyAggregator: 'childEntityUniversalIdentifiers',
},
// null for relations to non-syncable entities
someNonSyncableRelation: null,
},
} as const;
```
### 6d. ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY
**File**: `src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts`
Low-level primitive constant. Only contains `foreignKey` — the column name ending in `Id` that stores the foreign key. Type-checked against entity properties.
```typescript
export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
// ... existing entries
myEntity: {
workspace: null,
application: null,
parentEntity: {
foreignKey: 'parentEntityId',
},
},
} as const;
```
### 6e. ALL_MANY_TO_ONE_METADATA_RELATIONS
**File**: `src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts`
Derived from both `ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY` (for `foreignKey` type and `universalForeignKey` derivation) and `ALL_ONE_TO_MANY_METADATA_RELATIONS` (for `inverseOneToManyProperty` key constraint). This is the main constant consumed by utils and optimistic tooling.
```typescript
export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
// ... existing entries
myEntity: {
workspace: null,
application: null,
parentEntity: {
metadataName: 'parentEntity',
foreignKey: 'parentEntityId',
inverseOneToManyProperty: 'myEntities', // key in ALL_ONE_TO_MANY_METADATA_RELATIONS['parentEntity'], or null if no inverse
isNullable: false,
universalForeignKey: 'parentEntityUniversalIdentifier',
},
},
} as const;
```
**Derivation dependency graph**:
```
ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY ALL_ONE_TO_MANY_METADATA_RELATIONS
(foreignKey only) (metadataName, aggregators)
│ │
│ FK type + universalFK derivation │ inverseOneToManyProperty keys
│ │
└────────────────┬───────────────────────┘
ALL_MANY_TO_ONE_METADATA_RELATIONS
(metadataName, foreignKey, inverseOneToManyProperty,
isNullable, universalForeignKey)
```
**Rules**:
- `workspace: null`, `application: null` — always present, always null (non-syncable relations)
- `inverseOneToManyProperty` — must be a key in `ALL_ONE_TO_MANY_METADATA_RELATIONS[targetMetadataName]`, or `null` if the target entity doesn't expose an inverse one-to-many relation
- `universalForeignKey` — derived from `foreignKey` by replacing the `Id` suffix with `UniversalIdentifier`
- Optimistic utils resolve `flatEntityForeignKeyAggregator` / `universalFlatEntityForeignKeyAggregator` at runtime by looking up `inverseOneToManyProperty` in `ALL_ONE_TO_MANY_METADATA_RELATIONS`
---
## Checklist
Before moving to Step 2:
- [ ] Metadata name added to `ALL_METADATA_NAME`
- [ ] TypeORM entity created (extends `SyncableEntity`)
- [ ] `isCustom` column added
- [ ] Flat entity type defined
- [ ] Flat entity maps type defined (if needed)
- [ ] Editable properties constant defined
- [ ] Universal and flat action types defined
- [ ] Registered in `AllFlatEntityTypesByMetadataName`
- [ ] Registered in `ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME`
- [ ] Registered in `ALL_ONE_TO_MANY_METADATA_RELATIONS` (if entity has one-to-many relations)
- [ ] Registered in `ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY`
- [ ] Registered in `ALL_MANY_TO_ONE_METADATA_RELATIONS`
- [ ] TypeScript compiles without errors
---
## Next Step
Once all types and constants are defined, proceed to:
**[Syncable Entity: Cache & Transform (Step 2/6)](../syncable-entity-cache-and-transform/SKILL.md)**
For complete workflow, see `@creating-syncable-entity` rule.
+1
View File
@@ -20,6 +20,7 @@ jobs:
with:
files: |
packages/create-twenty-app/**
!packages/create-twenty-app/package.json
create-app-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
+12
View File
@@ -28,11 +28,14 @@ jobs:
packages/twenty-ui/**
packages/twenty-shared/**
packages/twenty-sdk/**
!packages/twenty-sdk/package.json
changed-files-check-e2e:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
playwright.config.ts
.github/workflows/ci-front.yaml
front-sb-build:
@@ -194,10 +197,19 @@ jobs:
tag: scope:frontend
tasks: reset:env
- name: Run ${{ matrix.task }} task
id: run-task
uses: ./.github/actions/nx-affected
with:
tag: scope:frontend
tasks: ${{ matrix.task }}
- name: Check for coverage threshold failure
if: always() && steps.run-task.outcome == 'failure' && matrix.task == 'test'
shell: bash
run: |
echo "::error::The test task failed. If no individual test is failing, this is likely a coverage threshold not being met."
echo ""
echo "To debug locally, run: npx nx run twenty-front:test:ci"
exit 1
- name: Save ${{ matrix.task }} cache
uses: ./.github/actions/save-cache
with:
+49 -58
View File
@@ -18,6 +18,7 @@ jobs:
with:
files: |
packages/twenty-sdk/**
!packages/twenty-sdk/package.json
sdk-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
@@ -25,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test:unit]
task: [lint, typecheck, test:unit, storybook:build, storybook:test, test:integration]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
@@ -39,72 +40,62 @@ jobs:
uses: ./.github/actions/yarn-install
- name: Build
run: npx nx build twenty-sdk
- name: Install Playwright
if: contains(matrix.task, 'storybook')
run: npx playwright install chromium
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:sdk
tasks: ${{ matrix.task }}
# TODO uncomment when syncApplication resolver is fixed
# sdk-e2e-integration-test:
# timeout-minutes: 30
# runs-on: ubuntu-latest-8-cores
# needs: [changed-files-check, sdk-test]
# strategy:
# matrix:
# task: [test:integration, test:e2e]
# if: needs.changed-files-check.outputs.any_changed == 'true'
# services:
# postgres:
# image: twentycrm/twenty-postgres-spilo
# env:
# PGUSER_SUPERUSER: postgres
# PGPASSWORD_SUPERUSER: postgres
# ALLOW_NOSSL: 'true'
# SPILO_PROVIDER: 'local'
# ports:
# - 5432:5432
# options: >-
# --health-cmd pg_isready
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# redis:
# image: redis
# ports:
# - 6379:6379
# env:
# NODE_ENV: test
# steps:
# - name: Fetch custom Github Actions and base branch history
# uses: actions/checkout@v4
# with:
# fetch-depth: 0
# - name: Install dependencies
# uses: ./.github/actions/yarn-install
# - name: Server / Append billing config to .env.test
# working-directory: packages/twenty-server
# run: |
# echo "" >> .env.test
# echo "IS_BILLING_ENABLED=true" >> .env.test
# echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
# echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
# echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
# echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test
# - name: Server / Create Test DB
# run: |
# PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
# - name: SDK / Run ${{ matrix.task }} Tests
# uses: ./.github/actions/nx-affected
# with:
# tag: scope:sdk
# tasks: ${{ matrix.task }}
sdk-e2e-test:
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
needs: [changed-files-check, sdk-test]
if: needs.changed-files-check.outputs.any_changed == 'true'
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
NODE_ENV: test
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
run: npx nx build twenty-sdk
- name: Server / Create Test DB
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: SDK / Run e2e Tests
uses: ./.github/actions/nx-affected
with:
tag: scope:sdk
tasks: test:e2e
ci-sdk-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, sdk-test]
# TODO uncomment when syncApplication resolver is fixed
# needs: [changed-files-check, sdk-test, sdk-e2e-integration-test]
needs: [changed-files-check, sdk-test, sdk-e2e-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
+2
View File
@@ -10,6 +10,7 @@
.nx/installation
.nx/cache
.nx/workspace-data
.nx/nxw.js
.pnp.*
.yarn/*
@@ -49,3 +50,4 @@ dump.rdb
mcp.json
/.junie/
TRANSLATION_QA_REPORT.md
.playwright-mcp/
-115
View File
@@ -1,115 +0,0 @@
"use strict";
// This file should be committed to your repository! It wraps Nx and ensures
// that your local installation matches nx.json.
// See: https://nx.dev/recipes/installation/install-non-javascript for more info.
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require('fs');
const path = require('path');
const cp = require('child_process');
const installationPath = path.join(__dirname, 'installation', 'package.json');
function matchesCurrentNxInstall(currentInstallation, nxJsonInstallation) {
if (!currentInstallation.devDependencies ||
!Object.keys(currentInstallation.devDependencies).length) {
return false;
}
try {
if (currentInstallation.devDependencies['nx'] !==
nxJsonInstallation.version ||
require(path.join(path.dirname(installationPath), 'node_modules', 'nx', 'package.json')).version !== nxJsonInstallation.version) {
return false;
}
for (const [plugin, desiredVersion] of Object.entries(nxJsonInstallation.plugins || {})) {
if (currentInstallation.devDependencies[plugin] !== desiredVersion) {
return false;
}
}
return true;
}
catch {
return false;
}
}
function ensureDir(p) {
if (!fs.existsSync(p)) {
fs.mkdirSync(p, { recursive: true });
}
}
function getCurrentInstallation() {
try {
return require(installationPath);
}
catch {
return {
name: 'nx-installation',
version: '0.0.0',
devDependencies: {},
};
}
}
function performInstallation(currentInstallation, nxJson) {
fs.writeFileSync(installationPath, JSON.stringify({
name: 'nx-installation',
devDependencies: {
nx: nxJson.installation.version,
...nxJson.installation.plugins,
},
}));
try {
cp.execSync('npm i', {
cwd: path.dirname(installationPath),
stdio: 'inherit',
});
}
catch (e) {
// revert possible changes to the current installation
fs.writeFileSync(installationPath, JSON.stringify(currentInstallation));
// rethrow
throw e;
}
}
function ensureUpToDateInstallation() {
const nxJsonPath = path.join(__dirname, '..', 'nx.json');
let nxJson;
try {
nxJson = require(nxJsonPath);
if (!nxJson.installation) {
console.error('[NX]: The "installation" entry in the "nx.json" file is required when running the nx wrapper. See https://nx.dev/recipes/installation/install-non-javascript');
process.exit(1);
}
}
catch {
console.error('[NX]: The "nx.json" file is required when running the nx wrapper. See https://nx.dev/recipes/installation/install-non-javascript');
process.exit(1);
}
try {
ensureDir(path.join(__dirname, 'installation'));
const currentInstallation = getCurrentInstallation();
if (!matchesCurrentNxInstall(currentInstallation, nxJson.installation)) {
performInstallation(currentInstallation, nxJson);
}
}
catch (e) {
const messageLines = [
'[NX]: Nx wrapper failed to synchronize installation.',
];
if (e instanceof Error) {
messageLines.push('');
messageLines.push(e.message);
messageLines.push(e.stack);
}
else {
messageLines.push(e.toString());
}
console.error(messageLines.join('\n'));
process.exit(1);
}
}
if (!process.env.NX_WRAPPER_SKIP_INSTALL) {
ensureUpToDateInstallation();
}
require('./installation/node_modules/nx/bin/nx');
+2
View File
@@ -28,6 +28,8 @@ npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
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
+8 -2
View File
@@ -11,7 +11,9 @@ import unicornPlugin from 'eslint-plugin-unicorn';
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
import jsoncParser from 'jsonc-eslint-parser';
const twentyRules = await nxPlugin.loadWorkspaceRules('packages/twenty-eslint-rules');
const twentyRules = await nxPlugin.loadWorkspaceRules(
'packages/twenty-eslint-rules',
);
export default [
// Base JavaScript configuration
@@ -65,6 +67,10 @@ export default [
sourceTag: 'scope:sdk',
onlyDependOnLibsWithTags: ['scope:sdk', 'scope:shared'],
},
{
sourceTag: 'scope:create-app',
onlyDependOnLibsWithTags: ['scope:create-app', 'scope:shared'],
},
{
sourceTag: 'scope:shared',
onlyDependOnLibsWithTags: ['scope:shared'],
@@ -197,7 +203,7 @@ export default [
plugins: {
...mdxPlugin.flat.plugins,
'@nx': nxPlugin,
'twenty': { rules: twentyRules },
twenty: { rules: twentyRules },
},
},
mdxPlugin.flatCodeBlocks,
+1 -3
View File
@@ -118,6 +118,7 @@
"outputs": ["{projectRoot}/coverage"],
"options": {
"jestConfig": "{projectRoot}/jest.config.mjs",
"silent": true,
"coverage": true,
"coverageReporters": ["text-summary"],
"cacheDirectory": "../../.cache/jest/{projectRoot}"
@@ -272,9 +273,6 @@
"inputs": ["default", "^default"]
}
},
"installation": {
"version": "22.3.3"
},
"generators": {
"@nx/react": {
"application": {
+6 -4
View File
@@ -22,6 +22,7 @@
"googleapis": "105",
"hex-rgb": "^5.0.0",
"immer": "^10.1.1",
"jotai": "^2.17.1",
"libphonenumber-js": "^1.10.26",
"lodash.camelcase": "^4.3.0",
"lodash.chunk": "^4.2.0",
@@ -137,9 +138,9 @@
"@typescript-eslint/utils": "^8.39.0",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"@vitejs/plugin-react-swc": "3.11.0",
"@vitest/browser-playwright": "^4.0.17",
"@vitest/coverage-istanbul": "^4.0.17",
"@vitest/coverage-v8": "^4.0.17",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-istanbul": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"@yarnpkg/types": "^4.0.0",
"chromatic": "^6.18.0",
"concurrently": "^8.2.2",
@@ -182,10 +183,11 @@
"ts-jest": "^29.1.1",
"ts-loader": "^9.2.3",
"ts-node": "10.9.1",
"tsc-alias": "^1.8.16",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0",
"vite": "^7.0.0",
"vitest": "^4.0.17"
"vitest": "^4.0.18"
},
"engines": {
"node": "^24.5.0",
+64 -21
View File
@@ -15,7 +15,7 @@
Create Twenty App is the official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). It sets up a readytorun project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
- Zeroconfig project bootstrap
- Preconfigured scripts for auth, dev mode (watch & sync), generate, uninstall, and function management
- Preconfigured scripts for auth, dev mode (watch & sync), uninstall, and function management
- Strong TypeScript support and typed client generation
## Documentation
@@ -35,41 +35,84 @@ cd my-twenty-app
corepack enable
yarn install
# Get help
yarn run help
# Get help and list all available commands
yarn twenty help
# Authenticate using your API key (you'll be prompted)
yarn auth:login
yarn twenty auth:login
# Add a new entity to your application (guided)
yarn entity:add
# Generate a typed Twenty client and workspace entity types
yarn app:generate
yarn twenty entity:add
# Start dev mode: watches, builds, and syncs local changes to your workspace
yarn app:dev
# (also auto-generates a typed API client in node_modules/twenty-sdk/generated)
yarn twenty app:dev
# Watch your application's function logs
yarn function:logs
yarn twenty function:logs
# Execute a function with a JSON payload
yarn function:execute -n my-function -p '{"key": "value"}'
yarn twenty function:execute -n my-function -p '{"key": "value"}'
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn app:uninstall
yarn twenty app:uninstall
```
## Scaffolding modes
Control which example files are included when creating a new app:
| Flag | Behavior |
|------|----------|
| `-e, --exhaustive` | **(default)** Creates all example files without prompting |
| `-m, --minimal` | Creates only core files (`application-config.ts` and `default-role.ts`) |
| `-i, --interactive` | Prompts you to select which examples to include |
```bash
# Default: all examples included
npx create-twenty-app@latest my-app
# Minimal: only core files
npx create-twenty-app@latest my-app -m
# Interactive: choose which examples to include
npx create-twenty-app@latest my-app -i
```
In interactive mode, you can pick from:
- **Example object** — a custom CRM object definition (`objects/example-object.ts`)
- **Example field** — a custom field on the example object (`fields/example-field.ts`)
- **Example logic function** — a server-side handler with HTTP trigger (`logic-functions/hello-world.ts`)
- **Example front component** — a React UI component (`front-components/hello-world.tsx`)
- **Example view** — a saved view for the example object (`views/example-view.ts`)
- **Example navigation menu item** — a sidebar link (`navigation-menu-items/example-navigation-menu-item.ts`)
## What gets scaffolded
- A minimal app structure ready for Twenty
- TypeScript configuration
- Prewired scripts that wrap the `twenty` CLI from twenty-sdk
- Example placeholders to help you add entities, actions, and sync logic
**Core files (always created):**
- `application-config.ts` — Application metadata configuration
- `roles/default-role.ts` — Default role for logic functions
- `logic-functions/post-install.ts` — Post-install logic function (runs after app installation)
- TypeScript configuration, ESLint, package.json, .gitignore
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
**Example files (controlled by scaffolding mode):**
- `objects/example-object.ts` — Example custom object with a text field
- `fields/example-field.ts` — Example standalone field extending the example object
- `logic-functions/hello-world.ts` — Example logic function with HTTP trigger
- `front-components/hello-world.tsx` — Example front component
- `views/example-view.ts` — Example saved view for the example object
- `navigation-menu-items/example-navigation-menu-item.ts` — Example sidebar navigation link
## Next steps
- Explore the generated project and add your first entity with `yarn entity:add` (functions, front components, objects, roles).
- Keep your types uptodate using `yarn app:generate`.
- Use `yarn app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items).
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- Types are autogenerated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`.
## Publish your application
@@ -97,8 +140,8 @@ git push
Our team reviews contributions for quality, security, and reusability before merging.
## Troubleshooting
- Auth prompts not appearing: run `yarn auth:login` again and verify the API key permissions.
- Types not generated: ensure `yarn app:generate` runs without errors, then restart `yarn app:dev`.
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
- Types not generated: ensure `yarn twenty app:dev` is running — it autogenerates the typed client.
## Contributing
- See our [GitHub](https://github.com/twentyhq/twenty)
+8 -99
View File
@@ -1,111 +1,20 @@
import js from '@eslint/js';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import prettierPlugin from 'eslint-plugin-prettier';
import baseConfig from '../../eslint.config.mjs';
export default [
js.configs.recommended,
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
globals: {
// Node.js globals
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
global: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
// Browser globals that Node.js also has
URL: 'readonly',
URLSearchParams: 'readonly',
// Node.js types
NodeJS: 'readonly',
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
prettier: prettierPlugin,
},
ignores: ['**/dist/**'],
},
{
files: ['**/*.{js,jsx,ts,tsx}'],
rules: {
...typescriptEslint.configs.recommended.rules,
'prettier/prettier': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'no-useless-escape': 'off',
},
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
global: 'readonly',
},
},
},
{
files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**/*.ts'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
globals: {
// Node.js globals
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
global: 'readonly',
// Jest globals
describe: 'readonly',
it: 'readonly',
test: 'readonly',
expect: 'readonly',
jest: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
beforeAll: 'readonly',
afterAll: 'readonly',
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
prettier: prettierPlugin,
},
rules: {
...typescriptEslint.configs.recommended.rules,
'prettier/prettier': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'no-useless-escape': 'off',
'no-console': 'off',
},
},
{
ignores: ['dist/**', 'node_modules/**'],
ignores: ['src/**/*.ts', '!src/cli/**/*.ts'],
},
];
+7 -2
View File
@@ -1,11 +1,13 @@
{
"name": "create-twenty-app",
"version": "0.4.0",
"version": "0.6.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
"files": [
"dist/**/*"
"dist",
"README.md",
"package.json"
],
"scripts": {
"build": "npx rimraf dist && npx vite build"
@@ -43,6 +45,9 @@
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.startcase": "^4",
"@types/node": "^20.0.0",
"twenty-sdk": "workspace:*",
"twenty-shared": "workspace:*",
"typescript": "^5.9.2",
"vite": "^7.0.0",
"vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^4.2.1"
+52 -11
View File
@@ -2,6 +2,7 @@
import chalk from 'chalk';
import { Command, CommanderError } from 'commander';
import { CreateAppCommand } from '@/create-app.command';
import { type ScaffoldingMode } from '@/types/scaffolding-options';
import packageJson from '../package.json';
const program = new Command(packageJson.name)
@@ -12,18 +13,58 @@ const program = new Command(packageJson.name)
'Output the current version of create-twenty-app.',
)
.argument('[directory]')
.option('-e, --exhaustive', 'Create all example entities (default)')
.option(
'-m, --minimal',
'Create only core entities (application-config and default-role)',
)
.option(
'-i, --interactive',
'Interactively choose which entity examples to include',
)
.helpOption('-h, --help', 'Display this help message.')
.action(async (directory?: string) => {
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
console.error(
chalk.red(
`Invalid directory "${directory}". Must contain only lowercase letters, numbers, and hyphens`,
),
);
process.exit(1);
}
await new CreateAppCommand().execute(directory);
});
.action(
async (
directory?: string,
options?: {
exhaustive?: boolean;
minimal?: boolean;
interactive?: boolean;
},
) => {
const modeFlags = [
options?.exhaustive,
options?.minimal,
options?.interactive,
].filter(Boolean);
if (modeFlags.length > 1) {
console.error(
chalk.red(
'Error: --exhaustive, --minimal, and --interactive are mutually exclusive.',
),
);
process.exit(1);
}
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
console.error(
chalk.red(
`Invalid directory "${directory}". Must contain only lowercase letters, numbers, and hyphens`,
),
);
process.exit(1);
}
const mode: ScaffoldingMode = options?.minimal
? 'minimal'
: options?.interactive
? 'interactive'
: 'exhaustive';
await new CreateAppCommand().execute(directory, mode);
},
);
program.exitOverride();
@@ -5,34 +5,35 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn auth:login
yarn twenty auth:login
```
Then, start development mode to sync your app and watch for changes:
```bash
yarn app:dev
yarn twenty app:dev
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
## Available Commands
Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Authentication
yarn auth:login # Authenticate with Twenty
yarn auth:logout # Remove credentials
yarn auth:status # Check auth status
yarn auth:switch # Switch default workspace
yarn auth:list # List all configured workspaces
yarn twenty auth:login # Authenticate with Twenty
yarn twenty auth:logout # Remove credentials
yarn twenty auth:status # Check auth status
yarn twenty auth:switch # Switch default workspace
yarn twenty auth:list # List all configured workspaces
# Application
yarn app:dev # Start dev mode (watch, build, and sync)
yarn entity:add # Add a new entity (function, front-component, object, role)
yarn app:generate # Generate typed Twenty client
yarn function:logs # Stream function logs
yarn function:execute # Execute a function with JSON payload
yarn app:uninstall # Uninstall app from workspace
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty function:logs # Stream function logs
yarn twenty function:execute # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
```
## Learn More
@@ -8,14 +8,24 @@ import inquirer from 'inquirer';
import kebabCase from 'lodash.kebabcase';
import * as path from 'path';
import {
type ExampleOptions,
type ScaffoldingMode,
} from '@/types/scaffolding-options';
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
export class CreateAppCommand {
async execute(directory?: string): Promise<void> {
async execute(
directory?: string,
mode: ScaffoldingMode = 'exhaustive',
): Promise<void> {
try {
const { appName, appDisplayName, appDirectory, appDescription } =
await this.getAppInfos(directory);
const exampleOptions = await this.resolveExampleOptions(mode);
await this.validateDirectory(appDirectory);
this.logCreationInfo({ appDirectory, appName });
@@ -27,6 +37,7 @@ export class CreateAppCommand {
appDisplayName,
appDescription,
appDirectory,
exampleOptions,
});
await install(appDirectory);
@@ -92,6 +103,95 @@ export class CreateAppCommand {
return { appName, appDisplayName, appDirectory, appDescription };
}
private async resolveExampleOptions(
mode: ScaffoldingMode,
): Promise<ExampleOptions> {
if (mode === 'minimal') {
return {
includeExampleObject: false,
includeExampleField: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: false,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
};
}
if (mode === 'exhaustive') {
return {
includeExampleObject: true,
includeExampleField: true,
includeExampleLogicFunction: true,
includeExampleFrontComponent: true,
includeExampleView: true,
includeExampleNavigationMenuItem: true,
};
}
const { selectedExamples } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selectedExamples',
message: 'Select which example files to include:',
choices: [
{
name: 'Example object (custom object definition)',
value: 'object',
checked: true,
},
{
name: 'Example field (custom field on the example object)',
value: 'field',
checked: true,
},
{
name: 'Example logic function (server-side handler)',
value: 'logicFunction',
checked: true,
},
{
name: 'Example front component (React UI component)',
value: 'frontComponent',
checked: true,
},
{
name: 'Example view (saved view for the example object)',
value: 'view',
checked: true,
},
{
name: 'Example navigation menu item (sidebar link)',
value: 'navigationMenuItem',
checked: true,
},
],
},
]);
const includeField = selectedExamples.includes('field');
const includeView = selectedExamples.includes('view');
const includeObject =
selectedExamples.includes('object') || includeField || includeView;
if ((includeField || includeView) && !selectedExamples.includes('object')) {
console.log(
chalk.yellow(
'Note: Example object auto-included because example field/view depends on it.',
),
);
}
return {
includeExampleObject: includeObject,
includeExampleField: includeField,
includeExampleLogicFunction: selectedExamples.includes('logicFunction'),
includeExampleFrontComponent: selectedExamples.includes('frontComponent'),
includeExampleView: includeView,
includeExampleNavigationMenuItem:
selectedExamples.includes('navigationMenuItem'),
};
}
private async validateDirectory(appDirectory: string): Promise<void> {
if (!(await fs.pathExists(appDirectory))) {
return;
@@ -0,0 +1,10 @@
export type ScaffoldingMode = 'exhaustive' | 'minimal' | 'interactive';
export type ExampleOptions = {
includeExampleObject: boolean;
includeExampleField: boolean;
includeExampleLogicFunction: boolean;
includeExampleFrontComponent: boolean;
includeExampleView: boolean;
includeExampleNavigationMenuItem: boolean;
};
@@ -2,6 +2,7 @@ import * as fs from 'fs-extra';
import { join } from 'path';
import { tmpdir } from 'os';
import { copyBaseApplicationProject } from '@/utils/app-template';
import { type ExampleOptions } from '@/types/scaffolding-options';
// Mock fs-extra's copy function to skip copying base template (not available during tests)
jest.mock('fs-extra', () => {
@@ -12,6 +13,27 @@ jest.mock('fs-extra', () => {
};
});
const APPLICATION_FILE_NAME = 'application-config.ts';
const DEFAULT_ROLE_FILE_NAME = 'default-role.ts';
const ALL_EXAMPLES: ExampleOptions = {
includeExampleObject: true,
includeExampleField: true,
includeExampleLogicFunction: true,
includeExampleFrontComponent: true,
includeExampleView: true,
includeExampleNavigationMenuItem: true,
};
const NO_EXAMPLES: ExampleOptions = {
includeExampleObject: false,
includeExampleField: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: false,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
};
describe('copyBaseApplicationProject', () => {
let testAppDirectory: string;
@@ -38,18 +60,19 @@ describe('copyBaseApplicationProject', () => {
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
// Verify src/app/ folder exists
// Verify src/ folder exists
const srcAppPath = join(testAppDirectory, 'src');
expect(await fs.pathExists(srcAppPath)).toBe(true);
// Verify application.config.ts exists in src/app/
const appConfigPath = join(srcAppPath, 'application.config.ts');
// Verify application-config.ts exists in src/
const appConfigPath = join(srcAppPath, APPLICATION_FILE_NAME);
expect(await fs.pathExists(appConfigPath)).toBe(true);
// Verify default.role.ts exists in src/app/
const roleConfigPath = join(srcAppPath, 'default.role.ts');
// Verify default-role.ts exists in src/
const roleConfigPath = join(srcAppPath, 'roles', DEFAULT_ROLE_FILE_NAME);
expect(await fs.pathExists(roleConfigPath)).toBe(true);
});
@@ -59,6 +82,7 @@ describe('copyBaseApplicationProject', () => {
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const packageJsonPath = join(testAppDirectory, 'package.json');
@@ -67,8 +91,8 @@ describe('copyBaseApplicationProject', () => {
const packageJson = await fs.readJson(packageJsonPath);
expect(packageJson.name).toBe('my-test-app');
expect(packageJson.version).toBe('0.1.0');
expect(packageJson.dependencies['twenty-sdk']).toBe('0.4.0');
expect(packageJson.scripts['app:dev']).toBe('twenty app:dev');
expect(packageJson.dependencies['twenty-sdk']).toBe('latest');
expect(packageJson.scripts['twenty']).toBe('twenty');
});
it('should create .gitignore file', async () => {
@@ -77,6 +101,7 @@ describe('copyBaseApplicationProject', () => {
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const gitignorePath = join(testAppDirectory, '.gitignore');
@@ -93,6 +118,7 @@ describe('copyBaseApplicationProject', () => {
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const yarnLockPath = join(testAppDirectory, 'yarn.lock');
@@ -102,19 +128,16 @@ describe('copyBaseApplicationProject', () => {
expect(yarnLockContent).toContain('yarn lockfile v1');
});
it('should create application.config.ts with defineApplication and correct values', async () => {
it('should create application-config.ts with defineApplication and correct values', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const appConfigPath = join(
testAppDirectory,
'src',
'application.config.ts',
);
const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME);
const appConfigContent = await fs.readFile(appConfigPath, 'utf8');
// Verify it uses defineApplication
@@ -125,7 +148,7 @@ describe('copyBaseApplicationProject', () => {
// Verify it imports the role identifier
expect(appConfigContent).toContain(
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role'",
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'",
);
// Verify display name and description
@@ -143,15 +166,21 @@ describe('copyBaseApplicationProject', () => {
);
});
it('should create default.role.ts with defineRole and correct values', async () => {
it('should create default-role.ts with defineRole and correct values', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const roleConfigPath = join(testAppDirectory, 'src', 'default.role.ts');
const roleConfigPath = join(
testAppDirectory,
'src',
'roles',
DEFAULT_ROLE_FILE_NAME,
);
const roleConfigContent = await fs.readFile(roleConfigPath, 'utf8');
// Verify it uses defineRole
@@ -188,6 +217,7 @@ describe('copyBaseApplicationProject', () => {
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
// Verify fs.copy was called with correct destination
@@ -204,13 +234,10 @@ describe('copyBaseApplicationProject', () => {
appDisplayName: 'My Test App',
appDescription: '',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const appConfigPath = join(
testAppDirectory,
'src',
'application.config.ts',
);
const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME);
const appConfigContent = await fs.readFile(appConfigPath, 'utf8');
expect(appConfigContent).toContain("description: ''");
@@ -225,6 +252,7 @@ describe('copyBaseApplicationProject', () => {
appDisplayName: 'App One',
appDescription: 'First app',
appDirectory: firstAppDir,
exampleOptions: ALL_EXAMPLES,
});
// Create second app
@@ -235,15 +263,16 @@ describe('copyBaseApplicationProject', () => {
appDisplayName: 'App Two',
appDescription: 'Second app',
appDirectory: secondAppDir,
exampleOptions: ALL_EXAMPLES,
});
// Read both app configs
const firstAppConfig = await fs.readFile(
join(firstAppDir, 'src', 'application.config.ts'),
join(firstAppDir, 'src', APPLICATION_FILE_NAME),
'utf8',
);
const secondAppConfig = await fs.readFile(
join(secondAppDir, 'src', 'application.config.ts'),
join(secondAppDir, 'src', APPLICATION_FILE_NAME),
'utf8',
);
@@ -267,6 +296,7 @@ describe('copyBaseApplicationProject', () => {
appDisplayName: 'App One',
appDescription: 'First app',
appDirectory: firstAppDir,
exampleOptions: ALL_EXAMPLES,
});
// Create second app
@@ -277,15 +307,16 @@ describe('copyBaseApplicationProject', () => {
appDisplayName: 'App Two',
appDescription: 'Second app',
appDirectory: secondAppDir,
exampleOptions: ALL_EXAMPLES,
});
const firstRoleConfig = await fs.readFile(
join(firstAppDir, 'src', 'default.role.ts'),
join(firstAppDir, 'src', 'roles', DEFAULT_ROLE_FILE_NAME),
'utf8',
);
const secondRoleConfig = await fs.readFile(
join(secondAppDir, 'src', 'default.role.ts'),
join(secondAppDir, 'src', 'roles', DEFAULT_ROLE_FILE_NAME),
'utf8',
);
@@ -299,4 +330,345 @@ describe('copyBaseApplicationProject', () => {
expect(secondUuid).toBeDefined();
expect(firstUuid).not.toBe(secondUuid);
});
describe('scaffolding modes', () => {
describe('exhaustive mode (all examples)', () => {
it('should create all example files when all options are enabled', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const srcPath = join(testAppDirectory, 'src');
expect(
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
).toBe(true);
expect(
await fs.pathExists(join(srcPath, 'fields', 'example-field.ts')),
).toBe(true);
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'hello-world.ts'),
),
).toBe(true);
expect(
await fs.pathExists(
join(srcPath, 'front-components', 'hello-world.tsx'),
),
).toBe(true);
expect(
await fs.pathExists(join(srcPath, 'views', 'example-view.ts')),
).toBe(true);
expect(
await fs.pathExists(
join(
srcPath,
'navigation-menu-items',
'example-navigation-menu-item.ts',
),
),
).toBe(true);
});
});
describe('minimal mode (no examples)', () => {
it('should create only core files when no examples are enabled', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const srcPath = join(testAppDirectory, 'src');
// Core files should exist
expect(await fs.pathExists(join(srcPath, APPLICATION_FILE_NAME))).toBe(
true,
);
expect(
await fs.pathExists(join(srcPath, 'roles', DEFAULT_ROLE_FILE_NAME)),
).toBe(true);
// Example files should not exist
expect(
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
).toBe(false);
expect(
await fs.pathExists(join(srcPath, 'fields', 'example-field.ts')),
).toBe(false);
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'hello-world.ts'),
),
).toBe(false);
expect(
await fs.pathExists(
join(srcPath, 'front-components', 'hello-world.tsx'),
),
).toBe(false);
expect(
await fs.pathExists(join(srcPath, 'views', 'example-view.ts')),
).toBe(false);
expect(
await fs.pathExists(
join(
srcPath,
'navigation-menu-items',
'example-navigation-menu-item.ts',
),
),
).toBe(false);
});
});
describe('selective examples', () => {
it('should create only front component when only that option is enabled', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: {
includeExampleObject: false,
includeExampleField: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: true,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
},
});
const srcPath = join(testAppDirectory, 'src');
expect(
await fs.pathExists(
join(srcPath, 'front-components', 'hello-world.tsx'),
),
).toBe(true);
expect(
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
).toBe(false);
expect(
await fs.pathExists(join(srcPath, 'fields', 'example-field.ts')),
).toBe(false);
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'hello-world.ts'),
),
).toBe(false);
});
it('should create only logic function when only that option is enabled', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: {
includeExampleObject: false,
includeExampleField: false,
includeExampleLogicFunction: true,
includeExampleFrontComponent: false,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
},
});
const srcPath = join(testAppDirectory, 'src');
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'hello-world.ts'),
),
).toBe(true);
expect(
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
).toBe(false);
});
});
});
describe('example object', () => {
it('should create example-object.ts with defineObject and correct structure', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const objectPath = join(
testAppDirectory,
'src',
'objects',
'example-object.ts',
);
expect(await fs.pathExists(objectPath)).toBe(true);
const content = await fs.readFile(objectPath, 'utf8');
expect(content).toContain(
"import { defineObject, FieldType } from 'twenty-sdk'",
);
expect(content).toContain('export default defineObject({');
expect(content).toContain(
'export const EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER',
);
expect(content).toContain('export const NAME_FIELD_UNIVERSAL_IDENTIFIER');
expect(content).toContain("nameSingular: 'exampleItem'");
expect(content).toContain("namePlural: 'exampleItems'");
expect(content).toContain('FieldType.TEXT');
expect(content).toContain(
'labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER',
);
});
it('should generate unique UUIDs for example objects across apps', async () => {
const firstAppDir = join(testAppDirectory, 'app1');
await fs.ensureDir(firstAppDir);
await copyBaseApplicationProject({
appName: 'app-one',
appDisplayName: 'App One',
appDescription: 'First app',
appDirectory: firstAppDir,
exampleOptions: ALL_EXAMPLES,
});
const secondAppDir = join(testAppDirectory, 'app2');
await fs.ensureDir(secondAppDir);
await copyBaseApplicationProject({
appName: 'app-two',
appDisplayName: 'App Two',
appDescription: 'Second app',
appDirectory: secondAppDir,
exampleOptions: ALL_EXAMPLES,
});
const firstContent = await fs.readFile(
join(firstAppDir, 'src', 'objects', 'example-object.ts'),
'utf8',
);
const secondContent = await fs.readFile(
join(secondAppDir, 'src', 'objects', 'example-object.ts'),
'utf8',
);
const uuidRegex =
/EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
const firstUuid = firstContent.match(uuidRegex)?.[1];
const secondUuid = secondContent.match(uuidRegex)?.[1];
expect(firstUuid).toBeDefined();
expect(secondUuid).toBeDefined();
expect(firstUuid).not.toBe(secondUuid);
});
});
describe('example field', () => {
it('should create example-field.ts with defineField referencing the object', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const fieldPath = join(
testAppDirectory,
'src',
'fields',
'example-field.ts',
);
expect(await fs.pathExists(fieldPath)).toBe(true);
const content = await fs.readFile(fieldPath, 'utf8');
expect(content).toContain(
"import { defineField, FieldType } from 'twenty-sdk'",
);
expect(content).toContain(
"import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object'",
);
expect(content).toContain('export default defineField({');
expect(content).toContain(
'objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER',
);
expect(content).toContain('FieldType.NUMBER');
expect(content).toContain("name: 'priority'");
});
});
describe('example view', () => {
it('should create example-view.ts with defineView referencing the object', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const viewPath = join(
testAppDirectory,
'src',
'views',
'example-view.ts',
);
expect(await fs.pathExists(viewPath)).toBe(true);
const content = await fs.readFile(viewPath, 'utf8');
expect(content).toContain("import { defineView } from 'twenty-sdk'");
expect(content).toContain(
"import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object'",
);
expect(content).toContain('export default defineView({');
expect(content).toContain(
'objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER',
);
expect(content).toContain("name: 'example-view'");
});
});
describe('example navigation menu item', () => {
it('should create example-navigation-menu-item.ts with defineNavigationMenuItem', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const navPath = join(
testAppDirectory,
'src',
'navigation-menu-items',
'example-navigation-menu-item.ts',
);
expect(await fs.pathExists(navPath)).toBe(true);
const content = await fs.readFile(navPath, 'utf8');
expect(content).toContain(
"import { defineNavigationMenuItem } from 'twenty-sdk'",
);
expect(content).toContain('export default defineNavigationMenuItem({');
expect(content).toContain("name: 'example-navigation-menu-item'");
expect(content).toContain("icon: 'IconList'");
expect(content).toContain('position: 0');
});
});
});
@@ -3,6 +3,8 @@ import { join } from 'path';
import { v4 } from 'uuid';
import { ASSETS_DIR } from 'twenty-shared/application';
import { type ExampleOptions } from '@/types/scaffolding-options';
const SRC_FOLDER = 'src';
export const copyBaseApplicationProject = async ({
@@ -10,11 +12,13 @@ export const copyBaseApplicationProject = async ({
appDisplayName,
appDescription,
appDirectory,
exampleOptions,
}: {
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
exampleOptions: ExampleOptions;
}) => {
await fs.copy(join(__dirname, './constants/base-application'), appDirectory);
@@ -33,20 +37,69 @@ export const copyBaseApplicationProject = async ({
await createDefaultRoleConfig({
displayName: appDisplayName,
appDirectory: sourceFolderPath,
fileFolder: 'roles',
fileName: 'default-role.ts',
});
await createDefaultFrontComponent({
appDirectory: sourceFolderPath,
});
if (exampleOptions.includeExampleObject) {
await createExampleObject({
appDirectory: sourceFolderPath,
fileFolder: 'objects',
fileName: 'example-object.ts',
});
}
await createDefaultFunction({
if (exampleOptions.includeExampleField) {
await createExampleField({
appDirectory: sourceFolderPath,
fileFolder: 'fields',
fileName: 'example-field.ts',
});
}
if (exampleOptions.includeExampleLogicFunction) {
await createDefaultFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
fileName: 'hello-world.ts',
});
}
if (exampleOptions.includeExampleFrontComponent) {
await createDefaultFrontComponent({
appDirectory: sourceFolderPath,
fileFolder: 'front-components',
fileName: 'hello-world.tsx',
});
}
if (exampleOptions.includeExampleView) {
await createExampleView({
appDirectory: sourceFolderPath,
fileFolder: 'views',
fileName: 'example-view.ts',
});
}
if (exampleOptions.includeExampleNavigationMenuItem) {
await createExampleNavigationMenuItem({
appDirectory: sourceFolderPath,
fileFolder: 'navigation-menu-items',
fileName: 'example-navigation-menu-item.ts',
});
}
await createDefaultPostInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
fileName: 'post-install.ts',
});
await createApplicationConfig({
displayName: appDisplayName,
description: appDescription,
appDirectory: sourceFolderPath,
fileName: 'application-config.ts',
});
};
@@ -108,9 +161,13 @@ yarn-error.log*
const createDefaultRoleConfig = async ({
displayName,
appDirectory,
fileFolder,
fileName,
}: {
displayName: string;
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
@@ -130,13 +187,18 @@ export default defineRole({
});
`;
await fs.writeFile(join(appDirectory, 'default.role.ts'), content);
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultFrontComponent = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
@@ -159,19 +221,20 @@ export default defineFrontComponent({
});
`;
await fs.writeFile(
join(appDirectory, 'hello-world.front-component.tsx'),
content,
);
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultFunction = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const triggerUniversalIdentifier = v4();
const content = `import { defineLogicFunction } from 'twenty-sdk';
@@ -179,52 +242,216 @@ const handler = async (): Promise<{ message: string }> => {
return { message: 'Hello, World!' };
};
// Logic function handler - rename and implement your logic
export default defineLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'hello-world-logic-function',
description: 'A simple logic function',
timeoutSeconds: 5,
handler,
triggers: [
httpRouteTriggerSettings: {
path: '/hello-world-logic-function',
httpMethod: 'GET',
isAuthRequired: false,
},
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultPostInstallFunction = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '${universalIdentifier}';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleObject = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const objectUniversalIdentifier = v4();
const nameFieldUniversalIdentifier = v4();
const content = `import { defineObject, FieldType } from 'twenty-sdk';
export const EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER =
'${objectUniversalIdentifier}';
export const NAME_FIELD_UNIVERSAL_IDENTIFIER =
'${nameFieldUniversalIdentifier}';
export default defineObject({
universalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'exampleItem',
namePlural: 'exampleItems',
labelSingular: 'Example item',
labelPlural: 'Example items',
description: 'A sample custom object',
icon: 'IconBox',
labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: '${triggerUniversalIdentifier}',
type: 'route',
path: '/hello-world-logic-function',
httpMethod: 'GET',
isAuthRequired: false,
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT,
name: 'name',
label: 'Name',
description: 'Name of the example item',
icon: 'IconAbc',
},
],
});
`;
await fs.writeFile(
join(appDirectory, 'hello-world.logic-function.ts'),
content,
);
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleField = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineField, FieldType } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
export default defineField({
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
universalIdentifier: '${universalIdentifier}',
type: FieldType.NUMBER,
name: 'priority',
label: 'Priority',
description: 'Priority level for the example item (1-10)',
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleView = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineView } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
export default defineView({
universalIdentifier: '${universalIdentifier}',
name: 'example-view',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleNavigationMenuItem = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineNavigationMenuItem } from 'twenty-sdk';
export default defineNavigationMenuItem({
universalIdentifier: '${universalIdentifier}',
name: 'example-navigation-menu-item',
icon: 'IconList',
position: 0,
// Link to a view:
// viewUniversalIdentifier: '...',
// Or link to an object:
// targetObjectUniversalIdentifier: '...',
// Or link to an external URL:
// link: 'https://example.com',
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createApplicationConfig = async ({
displayName,
description,
appDirectory,
fileFolder,
fileName,
}: {
displayName: string;
description?: string;
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const content = `import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '${v4()}',
displayName: '${displayName}',
description: '${description ?? ''}',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
`;
await fs.writeFile(join(appDirectory, 'application.config.ts'), content);
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createPackageJson = async ({
@@ -245,29 +472,18 @@ const createPackageJson = async ({
},
packageManager: 'yarn@4.9.2',
scripts: {
'auth:login': 'twenty auth:login',
'auth:logout': 'twenty auth:logout',
'auth:status': 'twenty auth:status',
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'function:logs': 'twenty function:logs',
'function:execute': 'twenty function:execute',
'app:uninstall': 'twenty app:uninstall',
help: 'twenty help',
twenty: 'twenty',
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': '0.4.0',
'twenty-sdk': 'latest',
},
devDependencies: {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^19.0.2',
react: '^19.0.2',
'@types/react': '^18.2.0',
react: '^18.2.0',
eslint: '^9.32.0',
'typescript-eslint': '^8.50.0',
},
+2 -1
View File
@@ -12,7 +12,8 @@
"types": ["jest", "node"],
"paths": {
"@/*": ["./src/*"]
}
},
"jsx": "react"
},
"include": [
"src/**/*.ts",
+18 -7
View File
@@ -4,6 +4,7 @@ import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import tsconfigPaths from 'vite-tsconfig-paths';
import packageJson from './package.json';
import type { PackageJson } from 'type-fest';
const moduleEntries = Object.keys((packageJson as any).exports || {})
.filter(
@@ -70,13 +71,23 @@ export default defineConfig(() => {
outDir: 'dist',
lib: { entry: entries, name: 'create-twenty-app' },
rollupOptions: {
external: [
...Object.keys((packageJson as any).dependencies || {}),
'path',
'fs',
'child_process',
'util',
],
external: (id: string) => {
if (/^node:/.test(id)) {
return true;
}
const builtins = ['path', 'fs', 'child_process', 'util'];
if (builtins.includes(id)) {
return true;
}
const deps = Object.keys(
(packageJson as PackageJson).dependencies || {},
);
return deps.some((dep) => id === dep || id.startsWith(dep + '/'));
},
output: [
{
format: 'es',
@@ -17,7 +17,6 @@
},
"scripts": {
"auth": "twenty auth login",
"generate": "twenty app generate",
"dev": "twenty app dev",
"sync": "twenty app sync",
"uninstall": "twenty app uninstall",
@@ -17,7 +17,6 @@
},
"scripts": {
"auth": "twenty auth login",
"generate": "twenty app generate",
"dev": "twenty app dev",
"sync": "twenty app sync",
"uninstall": "twenty app uninstall",
@@ -17,7 +17,6 @@
},
"scripts": {
"auth": "twenty auth login",
"generate": "twenty app generate",
"dev": "twenty app dev",
"sync": "twenty app sync",
"uninstall": "twenty app uninstall",
@@ -11,7 +11,6 @@
"scripts": {
"create-entity": "twenty app add",
"dev": "twenty app dev",
"generate": "twenty app generate",
"sync": "twenty app sync",
"uninstall": "twenty app uninstall",
"auth": "twenty auth login"
@@ -13,7 +13,7 @@ const config: ApplicationConfig = {
isSecret: false,
},
},
functionRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
};
export default config;
@@ -17,7 +17,6 @@
"app:dev": "twenty app dev",
"app:sync": "twenty app sync",
"entity:add": "twenty entity add",
"app:generate": "twenty app generate",
"function:logs": "twenty function logs",
"function:execute": "twenty function execute",
"app:uninstall": "twenty app uninstall",
@@ -17,7 +17,10 @@ setup_and_migrate_db() {
yarn database:migrate:prod
fi
yarn command:prod cache:flush
yarn command:prod upgrade
yarn command:prod cache:flush
echo "Successfully migrated DB!"
}
@@ -16,9 +16,6 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
- Build logic functions with custom triggers
- Deploy the same app across multiple workspaces
**Coming soon:**
- Custom UI layouts and components
## Prerequisites
- Node.js 24+ and Yarn 4
@@ -29,7 +26,7 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
Create a new app using the official scaffolder, then authenticate and start developing:
```bash filename="Terminal"
# Scaffold a new app
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
@@ -38,32 +35,45 @@ corepack enable
yarn install
# Authenticate using your API key (you'll be prompted)
yarn auth:login
yarn twenty auth:login
# Start dev mode: automatically syncs local changes to your workspace
yarn app:dev
yarn twenty app:dev
```
The scaffolder supports three modes for controlling which example files are included:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
# Interactive: select which examples to include
npx create-twenty-app@latest my-app --interactive
```
From here you can:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn entity:add
# Generate a typed Twenty client and workspace entity types
yarn app:generate
yarn twenty entity:add
# Watch your application's function logs
yarn function:logs
yarn twenty function:logs
# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn app:uninstall
yarn twenty app:uninstall
# Display commands' help
yarn help
yarn twenty help
```
See also: the CLI reference pages for [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) and [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@@ -75,9 +85,9 @@ When you run `npx create-twenty-app@latest my-twenty-app`, the scaffolder:
- Copies a minimal base application into `my-twenty-app/`
- Adds a local `twenty-sdk` dependency and Yarn 4 configuration
- Creates config files and scripts wired to the `twenty` CLI
- Generates a default application config and a default function role
- Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
A freshly scaffolded app looks like this:
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -93,87 +103,75 @@ my-twenty-app/
README.md
public/ # Public assets folder (images, fonts, etc.)
src/
application.config.ts # Required - main application configuration
default-function.role.ts # Default role for serverless functions
hello-world.function.ts # Example serverless function
hello-world.front-component.tsx # Example front component
// your entities (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
├── application-config.ts # Required - main application configuration
├── roles/
└── default-role.ts # Default role for logic functions
├── objects/
│ └── example-object.ts # Example custom object definition
├── fields/
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
├── front-components/
│ └── hello-world.tsx # Example front component
├── views/
│ └── example-view.ts # Example saved view definition
└── navigation-menu-items/
└── example-navigation-menu-item.ts # Example sidebar navigation link
```
### Convention-over-configuration
Applications use a **convention-over-configuration** approach where entities are detected by their file suffix. This allows flexible organization within the `src/app/` folder:
| File suffix | Entity type |
|-------------|-------------|
| `*.object.ts` | Custom object definitions |
| `*.function.ts` | Serverless function definitions |
| `*.front-component.tsx` | Front component definitions |
| `*.role.ts` | Role definitions |
### Supported folder organizations
You can organize your entities in any of these patterns:
**Traditional (by type):**
```text
src/
├── application.config.ts
├── objects/
│ └── postCard.object.ts
├── functions/
│ └── createPostCard.function.ts
├── components/
│ └── card.front-component.tsx
└── roles/
└── admin.role.ts
```
**Feature-based:**
```text
src/
├── application.config.ts
└── post-card/
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── postCardAdmin.role.ts
```
**Flat:**
```text
src/
├── application.config.ts
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── admin.role.ts
```
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
At a high level:
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and `auth:login` that delegate to the local `twenty` CLI.
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus a `twenty` script that delegates to the local `twenty` CLI. Run `yarn twenty help` to list all available commands.
- **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `generated/` (typed client), `dist/`, `build/`, coverage folders, log files, and `.env*` files.
- **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Lock and configure the Yarn 4 toolchain used by the project.
- **.nvmrc**: Pins the Node.js version expected by the project.
- **eslint.config.mjs** and **tsconfig.json**: Provide linting and TypeScript configuration for your app's TypeScript sources.
- **README.md**: A short README in the app root with basic instructions.
- **public/**: A folder for storing public assets (images, fonts, static files) that will be served with your application. Files placed here are uploaded during sync and accessible at runtime.
- **src/**: The main place where you define your application-as-code:
- `application.config.ts`: Global configuration for your app (metadata and runtime wiring). See "Application config" below.
- `*.role.ts`: Role definitions used by your logic functions. See "Default function role" below.
- `*.object.ts`: Custom object definitions.
- `*.function.ts`: Logic function definitions.
- `*.front-component.tsx`: Front component definitions.
- **src/**: The main place where you define your application-as-code
### Entity detection
The SDK detects entities by parsing your TypeScript files for **`export default define<Entity>({...})`** calls. Each entity type has a corresponding helper function exported from `twenty-sdk`:
| Helper function | Entity type |
|-----------------|-------------|
| `defineObject()` | Custom object definitions |
| `defineLogicFunction()` | Logic function definitions |
| `defineFrontComponent()` | Front component definitions |
| `defineRole()` | Role definitions |
| `defineField()` | Field extensions for existing objects |
| `defineView()` | Saved view definitions |
| `defineNavigationMenuItem()` | Navigation menu item definitions |
<Note>
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement.
</Note>
Example of a detected entity:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
Later commands will add more files and folders:
- `yarn app:generate` will create a `generated/` folder (typed Twenty client + workspace types).
- `yarn entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
- `yarn twenty app:dev` will auto-generate a typed API client in `node_modules/twenty-sdk/generated` (typed Twenty client + workspace types).
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
## Authentication
The first time you run `yarn auth:login`, you'll be prompted for:
The first time you run `yarn twenty auth:login`, you'll be prompted for:
- API URL (defaults to http://localhost:3000 or your current workspace profile)
- API key
@@ -184,25 +182,25 @@ Your credentials are stored per-user in `~/.twenty/config.json`. You can maintai
```bash filename="Terminal"
# Login interactively (recommended)
yarn auth:login
yarn twenty auth:login
# Login to a specific workspace profile
yarn auth:login --workspace my-custom-workspace
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn auth:list
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn auth:switch
yarn twenty auth:switch
# Switch to a specific workspace
yarn auth:switch production
yarn twenty auth:switch production
# Check current authentication status
yarn auth:status
yarn twenty auth:status
```
Once you've switched workspaces with `auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
Once you've switched workspaces with `yarn twenty auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
## Use the SDK resources (types & config)
@@ -210,16 +208,20 @@ The twenty-sdk provides typed building blocks and helper functions you use insid
### Helper functions
The SDK provides four helper functions with built-in validation for defining your app entities:
The SDK provides helper functions for defining your app entities. As described in [Entity detection](#entity-detection), you must use `export default define<Entity>({...})` for your entities to be detected:
| Function | Purpose |
|----------|---------|
| `defineApplication()` | Configure application metadata |
| `defineApplication()` | Configure application metadata (required, one per app) |
| `defineObject()` | Define custom objects with fields |
| `defineFunction()` | Define logic functions with handlers |
| `defineLogicFunction()` | Define logic functions with handlers |
| `defineFrontComponent()` | Define front components for custom UI |
| `defineRole()` | Configure role permissions and object access |
| `defineField()` | Extend existing objects with additional fields |
| `defineView()` | Define saved views for objects |
| `defineNavigationMenuItem()` | Define sidebar navigation links |
These functions validate your configuration at runtime and provide better IDE autocompletion and type safety.
These functions validate your configuration at build time and provide IDE autocompletion and type safety.
### Defining objects
@@ -300,27 +302,33 @@ Key points:
- The `universalIdentifier` must be unique and stable across deployments.
- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`.
- The `fields` array is optional — you can define objects without custom fields.
- You can scaffold new objects using `yarn entity:add`, which guides you through naming, fields, and relationships.
- You can scaffold new objects using `yarn twenty entity:add`, which guides you through naming, fields, and relationships.
<Note>
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields such as `name`, `createdAt`, `updatedAt`, `createdBy`, `position`, and `deletedAt`. You don't need to define these in your `fields` array — only add your custom fields.
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields
such as `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` and `deletedAt`.
You don't need to define these in your `fields` array — only add your custom fields.
You can override default fields by defining a field with the same name in your `fields` array,
but this is not recommended.
</Note>
### Application config (application.config.ts)
### Application config (application-config.ts)
Every app has a single `application.config.ts` file that describes:
Every app has a single `application-config.ts` file that describes:
- **Who the app is**: identifiers, display name, and description.
- **How its functions run**: which role they use for permissions.
- **(Optional) variables**: keyvalue pairs exposed to your functions as environment variables.
- **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
```typescript
// src/app/application.config.ts
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -335,18 +343,20 @@ export default defineApplication({
isSecret: false,
},
},
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Notes:
- `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs.
- `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
- `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
- `defaultRoleUniversalIdentifier` must match the role file (see below).
- `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### Roles and permissions
Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `defaultRoleUniversalIdentifier` in `application-config.ts` designates the default role used by your app's logic functions.
- The runtime API key injected as `TWENTY_API_KEY` is derived from this default function role.
- The typed client will be restricted to the permissions granted to that role.
@@ -357,7 +367,7 @@ Applications can define roles that encapsulate permissions on your workspace's o
When you scaffold a new app, the CLI also creates a default role file. Use `defineRole()` to define roles with built-in validation:
```typescript
// src/app/default-function.role.ts
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
@@ -396,10 +406,10 @@ export default defineRole({
});
```
The `universalIdentifier` of this role is then referenced in `application.config.ts` as `roleUniversalIdentifier`. In other words:
The `universalIdentifier` of this role is then referenced in `application-config.ts` as `defaultRoleUniversalIdentifier`. In other words:
- **\*.role.ts** defines what the default function role can do.
- **application.config.ts** points to that role so your functions inherit its permissions.
- **application-config.ts** points to that role so your functions inherit its permissions.
Notes:
- Start from the scaffolded role, then progressively restrict it following leastprivilege.
@@ -409,11 +419,11 @@ Notes:
### Logic function config and entrypoint
Each function file uses `defineFunction()` to export a configuration with a handler and optional triggers. Use the `*.function.ts` file suffix for automatic detection.
Each function file uses `defineLogicFunction()` to export a configuration with a handler and optional triggers.
```typescript
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
@@ -433,7 +443,7 @@ const handler = async (params: RoutePayload) => {
return result;
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
@@ -475,6 +485,54 @@ Notes:
- The `triggers` array is optional. Functions without triggers can be used as utility functions called by other functions.
- You can mix multiple trigger types in a single function.
### Post-install functions
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.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Key points:
- Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
- The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
- Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### Route trigger payload
<Warning>
@@ -502,7 +560,7 @@ const handler = async (event: RoutePayload) => {
When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the AWS HTTP API v2 format. Import the type from `twenty-sdk`:
```typescript
import { defineFunction, type RoutePayload } from 'twenty-sdk';
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
@@ -532,7 +590,7 @@ The `RoutePayload` type has the following structure:
By default, HTTP headers from incoming requests are **not** passed to your logic function for security reasons. To access specific headers, explicitly list them in the `forwardedRequestHeaders` array:
```typescript
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
@@ -567,12 +625,113 @@ const handler = async (event: RoutePayload) => {
You can create new functions in two ways:
- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new function. This generates a starter file with a handler and config.
- **Manual**: Create a new `*.function.ts` file and use `defineFunction()`, following the same pattern.
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new logic function. This generates a starter file with a handler and config.
- **Manual**: Create a new `*.logic-function.ts` file and use `defineLogicFunction()`, following the same pattern.
### Marking a logic function as a tool
Logic functions can be exposed as **tools** for AI agents and workflows. When a function is marked as a tool, it becomes discoverable by Twenty's AI features and can be selected as a step in workflow automations.
To mark a logic function as a tool, set `isTool: true` and provide a `toolInputSchema` describing the expected input parameters using [JSON Schema](https://json-schema.org/):
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
Key points:
- **`isTool`** (`boolean`, default: `false`): When set to `true`, the function is registered as a tool and becomes available to AI agents and workflow automations.
- **`toolInputSchema`** (`object`, optional): A JSON Schema object that describes the parameters your function accepts. AI agents use this schema to understand what inputs the tool expects and to validate calls. If omitted, the schema defaults to `{ type: 'object', properties: {} }` (no parameters).
- Functions with `isTool: false` (or unset) are **not** exposed as tools. They can still be executed directly or called by other functions, but will not appear in tool discovery.
- **Tool naming**: When exposed as a tool, the function name is automatically normalized to `logic_function_<name>` (lowercased, non-alphanumeric characters replaced with underscores). For example, `enrich-company` becomes `logic_function_enrich_company`.
- You can combine `isTool` with triggers — a function can be both a tool (callable by AI agents) and triggered by events (cron, database events, routes) at the same time.
<Note>
**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>
### Front components
Front components let you build custom React components that render within Twenty's UI. Use `defineFrontComponent()` to define components with built-in validation:
```typescript
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
Key points:
- Front components are React components that render in isolated contexts within Twenty.
- Use the `*.front-component.tsx` file suffix for automatic detection.
- The `component` field references your React component.
- Components are built and synced automatically during `yarn twenty app:dev`.
You can create new front components in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new front component.
- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`.
### Generated typed client
Run yarn app:generate to create a local typed client in generated/ based on your workspace schema. Use it in your functions:
The typed client is auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema. Use it in your functions:
```typescript
import Twenty from '~/generated';
@@ -581,7 +740,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
The client is re-generated by `yarn app:generate`. Re-run after changing your objects or when onboarding to a new workspace.
The client is re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
#### Runtime credentials in logic functions
@@ -592,50 +751,39 @@ When your function runs on Twenty, the platform injects credentials as environme
Notes:
- You do not need to pass URL or API key to the generated client. It reads `TWENTY_API_URL` and `TWENTY_API_KEY` from process.env at runtime.
- The API key's permissions are determined by the role referenced in your `application.config.ts` via `roleUniversalIdentifier`. This is the default role used by logic functions of your application.
- Applications can define roles to follow leastprivilege. Grant only the permissions your functions need, then point `roleUniversalIdentifier` to that role's universal identifier.
- The API key's permissions are determined by the role referenced in your `application-config.ts` via `defaultRoleUniversalIdentifier`. This is the default role used by logic functions of your application.
- Applications can define roles to follow leastprivilege. Grant only the permissions your functions need, then point `defaultRoleUniversalIdentifier` to that role's universal identifier.
### Hello World example
Explore a minimal, end-to-end example that demonstrates objects, functions, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
Explore a minimal, end-to-end example that demonstrates objects, logic functions, front components, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Manual setup (without the scaffolder)
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire scripts in your package.json:
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire a single script in your package.json:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
Then add scripts like these:
Then add a `twenty` script:
```json filename="package.json"
{
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
"twenty": "twenty"
}
}
```
Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:generate`, etc.
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty app:dev`, `yarn twenty help`, etc.
## Troubleshooting
- Authentication errors: run `yarn auth:login` and ensure your API key has the required permissions.
- Authentication errors: run `yarn twenty auth:login` and ensure your API key has the required permissions.
- Cannot connect to server: verify the API URL and that the Twenty server is reachable.
- Types or client missing/outdated: run `yarn app:generate`.
- Dev mode not syncing: ensure `yarn app:dev` is running and that changes are not ignored by your environment.
- Types or client missing/outdated: restart `yarn twenty app:dev` — it auto-generates the typed client.
- Dev mode not syncing: ensure `yarn twenty app:dev` is running and that changes are not ignored by your environment.
Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -17,10 +17,6 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
* أنشئ وظائف منطقية مع مشغلات مخصصة
* انشر التطبيق نفسه عبر مساحات عمل متعددة
**قريبًا:**
* تخطيطات ومكونات واجهة مستخدم مخصصة
## المتطلبات الأساسية
* Node.js 24+ وYarn 4
@@ -31,7 +27,7 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
أنشئ تطبيقًا جديدًا باستخدام المُهيئ الرسمي، ثم قم بالمصادقة وابدأ التطوير:
```bash filename="Terminal"
# إنشاء تطبيق جديد
# إنشاء تطبيق جديد (يتضمن جميع الأمثلة افتراضيًا)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
@@ -40,32 +36,45 @@ corepack enable
yarn install
# قم بالمصادقة باستخدام مفتاح واجهة برمجة التطبيقات الخاص بك (سيُطلب منك ذلك)
yarn auth:login
yarn twenty auth:login
# ابدأ وضع التطوير: يُزامن التغييرات المحلية تلقائيًا مع مساحة العمل الخاصة بك
yarn app:dev
yarn twenty app:dev
```
يدعم المُنشئ ثلاثة أوضاع للتحكم في ملفات الأمثلة التي سيتم تضمينها:
```bash filename="Terminal"
# الافتراضي (شامل): جميع الأمثلة (كائن، حقل، دالة منطقية، مكوّن الواجهة الأمامية، عرض، عنصر قائمة التنقل)
npx create-twenty-app@latest my-app
# الأدنى: الملفات الأساسية فقط (application-config.ts و default-role.ts)
npx create-twenty-app@latest my-app --minimal
# التفاعلي: اختر الأمثلة التي تريد تضمينها
npx create-twenty-app@latest my-app --interactive
```
من هنا يمكنك:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn entity:add
# Generate a typed Twenty client and workspace entity types
yarn app:generate
yarn twenty entity:add
# Watch your application's function logs
yarn function:logs
yarn twenty function:logs
# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn app:uninstall
yarn twenty app:uninstall
# Display commands' help
yarn help
yarn twenty help
```
راجع أيضًا: صفحات مرجع CLI لـ [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) و[twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@@ -77,9 +86,9 @@ yarn help
* ينسخ تطبيقًا أساسيًا مصغّرًا إلى `my-twenty-app/`
* يضيف اعتمادًا محليًا `twenty-sdk` وتهيئة Yarn 4
* ينشئ ملفات ضبط ونصوصًا مرتبطة بـ `twenty` CLI
* يُولّد ضبطًا افتراضيًا للتطبيق ودورًا افتراضيًا للوظيفة
* يُنشئ الملفات الأساسية (تهيئة التطبيق، دور الدالة الافتراضي، دالة ما بعد التثبيت) بالإضافة إلى ملفات الأمثلة بحسب وضع الإنشاء
يبدو التطبيق المُنشأ حديثًا بالقالب كما يلي:
يبدو التطبيق المُنشأ حديثًا باستخدام الوضع الافتراضي `--exhaustive` كما يلي:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -95,90 +104,76 @@ my-twenty-app/
README.md
public/ # مجلد الأصول العامة (صور، خطوط، إلخ)
src/
application.config.ts # مطلوب - التكوين الرئيسي للتطبيق
default-function.role.ts # الدور الافتراضي للوظائف بدون خادم
hello-world.function.ts # مثال لوظيفة بدون خادم
hello-world.front-component.tsx # مثال لمكوّن الواجهة الأمامية
// كياناتك (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
├── application-config.ts # مطلوب - إعدادات التطبيق الرئيسية
├── roles/
│ └── default-role.ts # الدور الافتراضي للدوال المنطقية
├── objects/
│ └── example-object.ts # تعريف كائن مخصص — مثال
├── fields/
│ └── example-field.ts # تعريف حقل مستقل — مثال
├── logic-functions/
│ ├── hello-world.ts # دالة منطقية — مثال
│ └── post-install.ts # دالة منطقية لما بعد التثبيت
├── front-components/
│ └── hello-world.tsx # مكوّن واجهة أمامية — مثال
├── views/
│ └── example-view.ts # تعريف عرض محفوظ — مثال
└── navigation-menu-items/
└── example-navigation-menu-item.ts # رابط تنقّل في الشريط الجانبي — مثال
```
### الاتفاقية فوق التهيئة
تستخدم التطبيقات نهج **الاتفاقية فوق التهيئة** حيث تُكتشف الكيانات عبر لاحقة اسم الملف. يتيح ذلك تنظيمًا مرنًا داخل مجلد `src/app/`:
| لاحقة الملف | نوع الكيان |
| ----------------------- | --------------------------- |
| `*.object.ts` | تعريفات كائنات مخصصة |
| `*.function.ts` | تعريفات وظائف بلا خادم |
| `*.front-component.tsx` | Front component definitions |
| `*.role.ts` | تعريفات الأدوار |
### طرق تنظيم المجلدات المدعومة
يمكنك تنظيم الكيانات بأي من الأنماط التالية:
**تقليدي (حسب النوع):**
```text
src/
├── application.config.ts
├── objects/
│ └── postCard.object.ts
├── functions/
│ └── createPostCard.function.ts
├── components/
│ └── card.front-component.tsx
└── roles/
└── admin.role.ts
```
**حسب الميزة:**
```text
src/
├── application.config.ts
└── post-card/
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── postCardAdmin.role.ts
```
**مسطح:**
```text
src/
├── application.config.ts
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── admin.role.ts
```
مع `--minimal`، سيتم إنشاء الملفات الأساسية فقط (`application-config.ts` و`roles/default-role.ts` و`logic-functions/post-install.ts`). مع `--interactive`، تختار ملفات الأمثلة التي تريد تضمينها.
بشكل عام:
* **package.json**: يصرّح باسم التطبيق والإصدار والمحرّكات (Node 24+، Yarn 4)، ويضيف `twenty-sdk` فضلًا عن نصوص مثل `app:dev` و`app:generate` و`entity:add` و`function:logs` و`function:execute` و`app:uninstall` و`auth:login` التي تفوِّض إلى `twenty` CLI المحلي.
* **package.json**: يصرّح باسم التطبيق والإصدار والمحرّكات (Node 24+، Yarn 4)، ويضيف `twenty-sdk` بالإضافة إلى نص برمجي `twenty` يفوِّض إلى `twenty` CLI المحلي. شغِّل `yarn twenty help` لعرض جميع الأوامر المتاحة.
* **.gitignore**: يتجاهل العناصر الشائعة مثل `node_modules` و`.yarn` و`generated/` (عميل مضبوط الأنواع) و`dist/` و`build/` ومجلدات التغطية وملفات السجلات وملفات `.env*`.
* **yarn.lock**، **.yarnrc.yml**، **.yarn/**: تقوم بقفل وتكوين حزمة أدوات Yarn 4 المستخدمة في المشروع.
* **.nvmrc**: يثبّت إصدار Node.js المتوقع للمشروع.
* **eslint.config.mjs** و**tsconfig.json**: يقدّمان إعدادات الفحص والتهيئة لـ TypeScript لمصادر TypeScript في تطبيقك.
* **README.md**: ملف README قصير في جذر التطبيق يتضمن تعليمات أساسية.
* **public/**: مجلد لتخزين الأصول العامة (صور، خطوط، ملفات ثابتة) التي سيتم تقديمها مع تطبيقك. الملفات الموضوعة هنا تُرفع أثناء المزامنة وتكون متاحة أثناء وقت التشغيل.
* **src/**: المكان الرئيسي حيث تعرّف تطبيقك ككود:
* `application.config.ts`: التكوين العام لتطبيقك (بيانات وصفية وربط وقت التشغيل). انظر "تكوين التطبيق" أدناه.
* `*.role.ts`: تعريفات الأدوار المستخدمة بواسطة وظائفك المنطقية. انظر "الدور الافتراضي للوظيفة" أدناه.
* `*.object.ts`: تعريفات كائنات مخصصة.
* `*.function.ts`: تعريفات الوظائف المنطقية.
* `*.front-component.tsx`: تعريفات المكونات الواجهية.
* **src/**: المكان الرئيسي حيث تعرّف تطبيقك ككود
### اكتشاف الكيانات
يكتشف SDK الكيانات عبر تحليل ملفات TypeScript الخاصة بك بحثًا عن استدعاءات **`export default define<Entity>({...})`**. يحتوي كل نوع كيان على دالة مساعدة مقابلة يتم تصديرها من `twenty-sdk`:
| دالة مساعدة | نوع الكيان |
| ---------------------------- | --------------------------------- |
| `defineObject()` | تعريفات كائنات مخصصة |
| `defineLogicFunction()` | تعريفات الوظائف المنطقية |
| `defineFrontComponent()` | Front component definitions |
| `defineRole()` | تعريفات الأدوار |
| `defineField()` | امتدادات الحقول للكائنات الموجودة |
| `defineView()` | تعريفات العروض المحفوظة |
| `defineNavigationMenuItem()` | تعريفات عناصر قائمة التنقل |
<Note>
**تسمية الملفات مرنة.** يعتمد اكتشاف الكيانات على بنية الشجرة المجردة (AST) — إذ يقوم SDK بفحص ملفات المصدر لديك بحثًا عن النمط `export default define<Entity>({...})`. يمكنك تنظيم ملفاتك ومجلداتك كيفما تشاء. التجميع حسب نوع الكيان (مثلًا، `logic-functions/` و`roles/`) هو مجرد عرف لتنظيم الشيفرة، وليس مطلبًا إلزاميًا.
</Note>
مثال على كيان تم اكتشافه:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
ستضيف الأوامر اللاحقة مزيدًا من الملفات والمجلدات:
* `yarn app:generate` سيُنشئ مجلدًا `generated/` (عميل Twenty مضبوط الأنواع + أنواع مساحة العمل).
* `yarn entity:add` سيضيف ملفات تعريف الكيانات تحت `src/` لكائناتك المخصصة أو الوظائف أو المكونات الواجهية أو الأدوار.
* `yarn twenty app:dev` سيولّد تلقائيًا عميل API مضبوط الأنواع في `node_modules/twenty-sdk/generated` (عميل Twenty مضبوط الأنواع + أنواع مساحة العمل).
* `yarn twenty entity:add` سيضيف ملفات تعريف الكيانات تحت `src/` لكائناتك المخصصة أو الوظائف أو المكونات الواجهية أو الأدوار.
## المصادقة
في المرة الأولى التي تشغّل فيها `yarn auth:login`، سيُطلب منك إدخال:
في المرة الأولى التي تشغّل فيها `yarn twenty auth:login`، سيُطلب منك إدخال:
* عنوان URL لواجهة برمجة التطبيقات (الافتراضي http://localhost:3000 أو ملف تعريف مساحة العمل الحالية لديك)
* مفتاح واجهة برمجة التطبيقات
@@ -188,26 +183,26 @@ src/
### Managing workspaces
```bash filename="Terminal"
# Login interactively (recommended)
yarn auth:login
# تسجيل الدخول تفاعليًا (مُوصى به)
yarn twenty auth:login
# Login to a specific workspace profile
yarn auth:login --workspace my-custom-workspace
# تسجيل الدخول إلى ملف تعريف لمساحة عمل محددة
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn auth:list
# عرض جميع مساحات العمل المُكوَّنة
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn auth:switch
# تبديل مساحة العمل الافتراضية (تفاعليًا)
yarn twenty auth:switch
# Switch to a specific workspace
yarn auth:switch production
# التبديل إلى مساحة عمل محددة
yarn twenty auth:switch production
# Check current authentication status
yarn auth:status
# التحقق من حالة المصادقة الحالية
yarn twenty auth:status
```
Once you've switched workspaces with `auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
بمجرد أن تقوم بالتبديل بين مساحات العمل باستخدام `yarn twenty auth:switch`، ستستخدم جميع الأوامر اللاحقة تلك المساحة افتراضيًا. You can still override it temporarily with `--workspace <name>`.
## استخدم موارد SDK (الأنواع والتكوين)
@@ -215,16 +210,20 @@ Once you've switched workspaces with `auth:switch`, all subsequent commands will
### دوال مساعدة
يوفّر SDK أربع دوال مساعدة مع تحقق مدمج لتعريف كيانات تطبيقك:
يوفّر SDK دوالًا مساعدة لتعريف كيانات تطبيقك. كما هو موضح في [اكتشاف الكيانات](#entity-detection)، يجب استخدام `export default define<Entity>({...})` كي يتم اكتشاف كياناتك:
| دالة | الغرض |
| --------------------- | ---------------------------------------- |
| `defineApplication()` | تهيئة بيانات التطبيق الوصفية |
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
| `defineFunction()` | تعريف وظائف منطقية مع معالجات |
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
| دالة | الغرض |
| ---------------------------- | ---------------------------------------------------- |
| `defineApplication()` | تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق) |
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
| `defineLogicFunction()` | تعريف وظائف منطقية مع معالجات |
| `defineFrontComponent()` | عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة |
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
| `defineField()` | وسّع الكائنات الموجودة بحقول إضافية |
| `defineView()` | تعريف العروض المحفوظة للكائنات |
| `defineNavigationMenuItem()` | تعريف روابط التنقل في الشريط الجانبي |
تتحقق هذه الدوال من تكوينك في وقت التشغيل وتوفر إكمالًا تلقائيًا أفضل في بيئة التطوير وأمان أنواع أعلى.
تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
### تعريف الكائنات
@@ -305,26 +304,32 @@ export default defineObject({
* `universalIdentifier` يجب أن يكون فريدًا وثابتًا عبر عمليات النشر.
* يتطلب كل حقل `name` و`type` و`label` ومعرّف `universalIdentifier` ثابتًا خاصًا به.
* المصفوفة `fields` اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
* يمكنك إنشاء كائنات جديدة باستخدام `yarn entity:add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
* يمكنك إنشاء كائنات جديدة باستخدام `yarn twenty entity:add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
<Note>
**يتم إنشاء الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، يضيف Twenty تلقائيًا حقولًا قياسية مثل `name` و`createdAt` و`updatedAt` و`createdBy` و`position` و`deletedAt`. لا تحتاج إلى تعريف هذه في مصفوفة `fields` — أضف فقط حقولك المخصصة.
**يتم إنشاء الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، يضيف Twenty تلقائيًا حقولًا قياسية
مثل `id` و`name` و`createdAt` و`updatedAt` و`createdBy` و`updatedBy` و`deletedAt`.
لا تحتاج إلى تعريف هذه في مصفوفة `fields` — أضف فقط حقولك المخصصة.
يمكنك تجاوز الحقول الافتراضية من خلال تعريف حقل بالاسم نفسه في مصفوفة `fields` الخاصة بك،
لكن هذا غير مستحسن.
</Note>
### تكوين التطبيق (application.config.ts)
### تكوين التطبيق (application-config.ts)
كل تطبيق لديه ملف واحد `application.config.ts` يصف:
كل تطبيق لديه ملف واحد `application-config.ts` يصف:
* **هوية التطبيق**: المعرفات، اسم العرض، والوصف.
* **كيفية تشغيل وظائفه**: الدور الذي تستخدمه للأذونات.
* **متغيرات (اختياري)**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
* **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
```typescript
// src/app/application.config.ts
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -339,7 +344,8 @@ export default defineApplication({
isSecret: false,
},
},
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -347,11 +353,12 @@ export default defineApplication({
* حقول `universalIdentifier` هي معرّفات حتمية تخصك؛ أنشئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
* `applicationVariables` تصبح متغيرات بيئة لوظائفك (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`).
* `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
* `defaultRoleUniversalIdentifier` يجب أن يطابق ملف الدور (انظر أدناه).
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### الأدوار والصلاحيات
يمكن للتطبيقات تعريف أدوار تُغلّف الصلاحيات على كائنات وإجراءات مساحة العمل لديك. The field `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
يمكن للتطبيقات تعريف أدوار تُغلّف الصلاحيات على كائنات وإجراءات مساحة العمل لديك. يعين الحقل `defaultRoleUniversalIdentifier` في `application-config.ts` الدور الافتراضي الذي تستخدمه وظائف المنطق في تطبيقك.
* مفتاح واجهة البرمجة في وقت التشغيل المحقون باسم `TWENTY_API_KEY` مستمد من دور الوظيفة الافتراضي هذا.
* سيُقيَّد العميل مضبوط الأنواع بالأذونات الممنوحة لذلك الدور.
@@ -362,7 +369,7 @@ export default defineApplication({
عند توليد تطبيق جديد بالقالب، ينشئ CLI أيضًا ملف دور افتراضي. استخدم `defineRole()` لتعريف أدوار مع تحقق مدمج:
```typescript
// src/app/default-function.role.ts
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
@@ -401,10 +408,10 @@ export default defineRole({
});
```
يُشار بعد ذلك إلى `universalIdentifier` لهذا الدور في `application.config.ts` باسم `roleUniversalIdentifier`. بعبارة أخرى:
يُشار بعد ذلك إلى `universalIdentifier` لهذا الدور في `application-config.ts` باسم `defaultRoleUniversalIdentifier`. بعبارة أخرى:
* **\\*.role.ts** يحدد ما يمكن أن يفعله الدور الافتراضي للوظيفة.
* **application.config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك صلاحياته.
* **application-config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك أذوناته.
الملاحظات:
@@ -415,11 +422,11 @@ export default defineRole({
### تكوين الوظيفة المنطقية ونقطة الدخول
كل ملف وظيفة يستخدم `defineFunction()` لتصدير تكوين مع معالج ومشغلات اختيارية. استخدم لاحقة الملف `*.function.ts` للاكتشاف التلقائي.
كل ملف وظيفة يستخدم `defineLogicFunction()` لتصدير تكوين مع معالج ومشغّلات اختيارية.
```typescript
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
@@ -439,7 +446,7 @@ const handler = async (params: RoutePayload) => {
return result;
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
@@ -486,6 +493,55 @@ export default defineFunction({
* المصفوفة `triggers` اختيارية. يمكن استخدام الوظائف بدون مشغلات كوظائف مساعدة تُستدعى بواسطة وظائف أخرى.
* يمكنك مزج أنواع متعددة من المشغلات في وظيفة واحدة.
### Post-install functions
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.
عند إنشاء هيكل تطبيق جديد باستخدام `create-twenty-app`، يتم إنشاء دالة ما بعد التثبيت لك في `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
يتم ربط الدالة بتطبيقك من خلال الإشارة إلى المعرِّف العالمي الخاص بها في `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
يمكنك أيضًا تنفيذ دالة ما بعد التثبيت يدويًا في أي وقت باستخدام CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
النقاط الرئيسية:
* دوال ما بعد التثبيت هي دوال منطقية قياسية — فهي تستخدم `defineLogicFunction()` مثل أي دالة أخرى.
* حقل `postInstallLogicFunctionUniversalIdentifier` في `defineApplication()` اختياري. إذا تم تجاهله، لن يتم تشغيل أي دالة بعد التثبيت.
* تم تعيين مهلة افتراضية إلى 300 ثانية (5 دقائق) للسماح بمهام الإعداد الأطول مثل تهيئة البيانات.
* لا تحتاج دوال ما بعد التثبيت إلى مُشغِّلات — حيث يستدعيها النظام الأساسي أثناء التثبيت أو يدويًا عبر `function:execute --postInstall`.
### حمولة مشغل المسار
<Warning>
@@ -515,7 +571,7 @@ export default defineFunction({
عندما يستدعي مشغّل المسار وظيفتك المنطقية، يتلقى كائنًا من النوع `RoutePayload` يتبع تنسيق AWS HTTP API v2. استورد النوع من `twenty-sdk`:
```typescript
import { defineFunction, type RoutePayload } from 'twenty-sdk';
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
@@ -545,7 +601,7 @@ const handler = async (event: RoutePayload) => {
افتراضيًا، **لا** تُمرَّر رؤوس HTTP من الطلبات الواردة إلى وظيفتك المنطقية لأسباب أمنية. للوصول إلى رؤوس محددة، قم بإدراجها صراحةً في مصفوفة `forwardedRequestHeaders`:
```typescript
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
@@ -580,12 +636,114 @@ const handler = async (event: RoutePayload) => {
يمكنك إنشاء وظائف جديدة بطريقتين:
* **مُنشأ بالقالب**: شغّل `yarn entity:add` واختر خيار إضافة وظيفة جديدة. يُولّد هذا ملفًا مبدئيًا مع معالج وتكوين.
* **يدوي**: أنشئ ملفًا جديدًا `*.function.ts` واستخدم `defineFunction()` مع اتباع النمط نفسه.
* **مُنشأ بالقالب**: شغّل `yarn twenty entity:add` واختر خيار إضافة وظيفة منطقية جديدة. يُولّد هذا ملفًا مبدئيًا مع معالج وتكوين.
* **يدوي**: أنشئ ملفًا جديدًا `*.logic-function.ts` واستخدم `defineLogicFunction()` مع اتباع النمط نفسه.
### تمييز دالة منطقية كأداة
يمكن إتاحة الدوال المنطقية بوصفها **أدوات** لوكلاء الذكاء الاصطناعي وسير العمل. عندما يتم تمييز دالة كأداة، تصبح قابلة للاكتشاف بواسطة ميزات الذكاء الاصطناعي الخاصة بـ Twenty ويمكن اختيارها كخطوة في أتمتة سير العمل.
لتمييز دالة منطقية كأداة، عيّن `isTool: true` وقدّم `toolInputSchema` يصف معاملات الإدخال المتوقعة باستخدام [مخطط JSON](https://json-schema.org/):
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
النقاط الرئيسية:
* **`isTool`** (`boolean`, الافتراضي: `false`): عند ضبطه على `true`، يتم تسجيل الدالة كأداة وتصبح متاحة لوكلاء الذكاء الاصطناعي ولأتمتة سير العمل.
* **`toolInputSchema`** (`object`, اختياري): كائن JSON Schema يصف المعلمات التي تقبلها دالتك. يستخدم وكلاء الذكاء الاصطناعي هذا المخطط لفهم المدخلات التي تتوقعها الأداة وللتحقق من صحة الاستدعاءات. إذا تم إغفاله، فالقيمة الافتراضية للمخطط هي `{ type: 'object', properties: {} }` (من دون معلمات).
* الدوال التي لديها `isTool: false` (أو غير معيَّنة) **غير** معروضة كأدوات. لا يزال بالإمكان تنفيذها مباشرةً أو استدعاؤها بواسطة دوال أخرى، لكنها لن تظهر في اكتشاف الأدوات.
* **تسمية الأداة**: عند كشفها كأداة، يتم تطبيع اسم الدالة تلقائيًا إلى `logic_function_<name>` (تحويله إلى أحرف صغيرة، واستبدال المحارف غير الأبجدية الرقمية بشرطات سفلية). على سبيل المثال، `enrich-company` تصبح `logic_function_enrich_company`.
* يمكنك دمج `isTool` مع المشغِّلات — إذ يمكن للدالة أن تكون أداة (قابلة للاستدعاء من قِبل وكلاء الذكاء الاصطناعي) وأن تُشغَّل بواسطة أحداث (cron، وأحداث قاعدة البيانات، والمسارات) في الوقت نفسه.
<Note>
**اكتب `description` جيدًا.** يعتمد وكلاء الذكاء الاصطناعي على حقل `description` الخاص بالدالة لتحديد وقت استخدام الأداة. كن محددًا بشأن ما تفعله الأداة ومتى ينبغي استدعاؤها.
</Note>
### المكوّنات الأمامية
تتيح لك المكوّنات الأمامية إنشاء مكوّنات React مخصّصة تُعرَض داخل واجهة مستخدم Twenty. استخدم `defineFrontComponent()` لتعريف مكوّنات مع تحقّق مدمج:
```typescript
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
النقاط الرئيسية:
* المكوّنات الأمامية هي مكوّنات React تُعرَض ضمن سياقات معزولة داخل Twenty.
* استخدم لاحقة الملف `*.front-component.tsx` للاكتشاف التلقائي.
* يشير الحقل `component` إلى مكوّن React الخاص بك.
* يتم بناء المكوّنات ومزامنتها تلقائيًا أثناء `yarn twenty app:dev`.
يمكنك إنشاء مكوّنات أمامية جديدة بطريقتين:
* **مُنشأ بالقالب**: شغّل `yarn twenty entity:add` واختر خيار إضافة مكوّن أمامي جديد.
* **يدوي**: أنشئ ملفًا جديدًا `*.front-component.tsx` واستخدم `defineFrontComponent()`.
### عميل مُولَّد مضبوط الأنواع
شغّل yarn app:generate لإنشاء عميل محلي مضبوط الأنواع في generated/ استنادًا إلى مخطط مساحة العمل لديك. استخدمه في وظائفك:
يُولَّد العميل مضبوط الأنواع تلقائيًا بواسطة `yarn twenty app:dev` ويُخزَّن في `node_modules/twenty-sdk/generated` استنادًا إلى مخطط مساحة العمل لديك. استخدمه في وظائفك:
```typescript
import Twenty from '~/generated';
@@ -594,7 +752,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
يُعاد توليد العميل بواسطة `yarn app:generate`. أعِد التشغيل بعد تغيير كائناتك أو عند الانضمام إلى مساحة عمل جديدة.
يُعاد توليد العميل تلقائيًا بواسطة `yarn twenty app:dev` كلما تغيّرت كائناتك أو حقولك.
#### بيانات الاعتماد وقت التشغيل في الوظائف المنطقية
@@ -606,49 +764,38 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
الملاحظات:
* لا تحتاج إلى تمرير عنوان URL أو مفتاح واجهة برمجة التطبيقات إلى العميل المُولَّد. يقوم بقراءة `TWENTY_API_URL` و`TWENTY_API_KEY` من process.env وقت التشغيل.
* تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `application.config.ts` عبر `roleUniversalIdentifier`. هذا هو الدور الافتراضي الذي تستخدمه الوظائف المنطقية في تطبيقك.
* يمكن للتطبيقات تعريف أدوار لاتباع مبدأ أقل الامتياز. امنح فقط الأذونات التي تحتاجها وظائفك، ثم وجّه `roleUniversalIdentifier` إلى المعرّف الشامل لذلك الدور.
* تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `application-config.ts` عبر `defaultRoleUniversalIdentifier`. هذا هو الدور الافتراضي الذي تستخدمه الوظائف المنطقية في تطبيقك.
* يمكن للتطبيقات تعريف أدوار لاتباع مبدأ أقل الامتياز. امنح فقط الأذونات التي تحتاجها وظائفك، ثم وجّه `defaultRoleUniversalIdentifier` إلى المعرّف الشامل لذلك الدور.
### مثال Hello World
استكشف مثالًا بسيطًا شاملًا من البداية إلى النهاية يوضح الكائنات والوظائف ومشغلات متعددة [هنا](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
استكشف مثالًا بسيطًا شاملًا من البداية إلى النهاية يوضح الكائنات والوظائف المنطقية والمكوّنات الأمامية ومشغّلات متعددة [هنا](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## إعداد يدوي (بدون المهيئ)
بينما نوصي باستخدام `create-twenty-app` للحصول على أفضل تجربة للبدء، يمكنك أيضًا إعداد مشروع يدويًا. لا تثبّت CLI عالميًا. بدل ذلك، أضف `twenty-sdk` كاعتماد محلي ووصل السكربتات في ملف package.json لديك:
بينما نوصي باستخدام `create-twenty-app` للحصول على أفضل تجربة للبدء، يمكنك أيضًا إعداد مشروع يدويًا. لا تثبّت CLI عالميًا. بدل ذلك، أضف `twenty-sdk` كاعتماد محلي واربط سكربتًا واحدًا في ملف package.json لديك:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
ثم أضف نصوصًا مثل هذه:
ثم أضف سكربتًا باسم `twenty`:
```json filename="package.json"
{
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
"twenty": "twenty"
}
}
```
يمكنك الآن تشغيل الأوامر نفسها عبر Yarn، مثل `yarn app:dev` و`yarn app:generate`، إلخ.
الآن يمكنك تشغيل جميع الأوامر عبر `yarn twenty <command>`، مثلًا: `yarn twenty app:dev`، `yarn twenty help`، إلخ.
## استكشاف الأخطاء وإصلاحها
* أخطاء المصادقة: شغّل `yarn auth:login` وتأكد من أن مفتاح واجهة برمجة التطبيقات لديك يمتلك الأذونات المطلوبة.
* أخطاء المصادقة: شغّل `yarn twenty auth:login` وتأكد من أن مفتاح واجهة برمجة التطبيقات لديك يمتلك الأذونات المطلوبة.
* يتعذّر الاتصال بالخادم: تحقق من عنوان URL لواجهة البرمجة وأن خادم Twenty قابل للوصول.
* الأنواع أو العميل مفقود/قديم: شغّل `yarn app:generate`.
* وضع التطوير لا يزامن: تأكد من أن `yarn app:dev` قيد التشغيل وأن التغييرات ليست متجاهلة من بيئتك.
* الأنواع أو العميل مفقود/قديم: أعد تشغيل `yarn twenty app:dev` — فهو ينشئ العميل مضبوط الأنواع بشكل تلقائي.
* وضع التطوير لا يزامن: تأكد من أن `yarn twenty app:dev` قيد التشغيل وأن التغييرات ليست متجاهلة من بيئتك.
قناة المساعدة على Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -17,10 +17,6 @@ Aplikace vám umožňují vytvářet a spravovat přizpůsobení Twenty **jako k
* Vytvářejte logické funkce s vlastními spouštěči
* Nasazujte stejnou aplikaci do více pracovních prostorů
**Již brzy:**
* Vlastní rozvržení a komponenty uživatelského rozhraní
## Předpoklady
* Node.js 24+ a Yarn 4
@@ -31,7 +27,7 @@ Aplikace vám umožňují vytvářet a spravovat přizpůsobení Twenty **jako k
Vytvořte novou aplikaci pomocí oficiálního scaffolderu, poté se ověřte a začněte vyvíjet:
```bash filename="Terminal"
# Vygenerujte kostru nové aplikace
# Vygenerujte kostru nové aplikace (ve výchozím nastavení zahrnuje všechny příklady)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
@@ -40,32 +36,45 @@ corepack enable
yarn install
# Přihlaste se pomocí svého API klíče (budete vyzváni)
yarn auth:login
yarn twenty auth:login
# Spusťte vývojový režim: automaticky synchronizuje místní změny s vaším pracovním prostorem
yarn app:dev
yarn twenty app:dev
```
Nástroj pro generování kostry podporuje tři režimy pro řízení toho, které ukázkové soubory jsou zahrnuty:
```bash filename="Terminal"
# Výchozí (úplný): všechny příklady (objekt, pole, logická funkce, front-endová komponenta, zobrazení, položka navigační nabídky)
npx create-twenty-app@latest my-app
# Minimální: pouze základní soubory (application-config.ts a default-role.ts)
npx create-twenty-app@latest my-app --minimal
# Interaktivní: vyberte, které příklady zahrnout
npx create-twenty-app@latest my-app --interactive
```
Odtud můžete:
```bash filename="Terminal"
# Přidejte do vaší aplikace novou entitu (s průvodcem)
yarn entity:add
# Vygenerujte typovaného klienta Twenty a typy entit pracovního prostoru
yarn app:generate
yarn twenty entity:add
# Sledujte logy funkcí vaší aplikace
yarn function:logs
yarn twenty function:logs
# Spusťte funkci podle názvu
yarn function:execute -n my-function -p '{"name": "test"}'
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Spusťte postinstalační funkci
yarn twenty function:execute --postInstall
# Odinstalujte aplikaci z aktuálního pracovního prostoru
yarn app:uninstall
yarn twenty app:uninstall
# Zobrazte nápovědu k příkazům
yarn help
yarn twenty help
```
Viz také: referenční stránky CLI pro [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) a [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@@ -77,9 +86,9 @@ Když spustíte `npx create-twenty-app@latest my-twenty-app`, scaffolder:
* Zkopíruje minimální základní aplikaci do `my-twenty-app/`
* Přidá lokální závislost `twenty-sdk` a konfiguraci pro Yarn 4
* Vytvoří konfigurační soubory a skripty napojené na `twenty` CLI
* Vygeneruje výchozí konfiguraci aplikace a výchozí roli funkcí
* Vygeneruje základní soubory (konfigurace aplikace, výchozí role funkcí, postinstalační funkce) a k nim ukázkové soubory podle zvoleného režimu generování kostry
Čerstvě vytvořená aplikace vypadá takto:
Čerstvě vygenerovaná aplikace s výchozím režimem `--exhaustive` vypadá takto:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -95,90 +104,76 @@ my-twenty-app/
README.md
public/ # Složka s veřejnými prostředky (obrázky, písma apod.)
src/
application.config.ts # Povinné hlavní konfigurace aplikace
default-function.role.ts # Výchozí role pro bezserverové funkce
hello-world.function.ts # Ukázková bezserverová funkce
hello-world.front-component.tsx # Ukázková front-endová komponenta
// vaše entity (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
├── application-config.ts # Povinné hlavní konfigurace aplikace
├── roles/
└── default-role.ts # Výchozí role pro logické funkce
├── objects/
│ └── example-object.ts # Ukázková definice vlastního objektu
├── fields/
│ └── example-field.ts # Ukázková samostatná definice pole
├── logic-functions/
│ ├── hello-world.ts # Ukázková logická funkce
│ └── post-install.ts # Postinstalační logická funkce
├── front-components/
│ └── hello-world.tsx # Ukázková front-endová komponenta
├── views/
│ └── example-view.ts # Ukázková definice uloženého zobrazení
└── navigation-menu-items/
└── example-navigation-menu-item.ts # Ukázkový odkaz postranní navigace
```
### Konvence před konfigurací
Aplikace používají přístup **konvence před konfigurací**, kde jsou entity detekovány podle přípony souboru. To umožňuje flexibilní organizaci ve složce `src/app/`:
| Přípona souboru | Typ entity |
| ----------------------- | -------------------------------- |
| `*.object.ts` | Definice vlastních objektů |
| `*.function.ts` | Definice serverless funkcí |
| `*.front-component.tsx` | Definice frontendových komponent |
| `*.role.ts` | Definice rolí |
### Podporované uspořádání složek
Entity můžete uspořádat podle některého z těchto vzorů:
**Tradiční (podle typu):**
```text
src/
├── application.config.ts
├── objects/
│ └── postCard.object.ts
├── functions/
│ └── createPostCard.function.ts
├── components/
│ └── card.front-component.tsx
└── roles/
└── admin.role.ts
```
**Podle funkcí:**
```text
src/
├── application.config.ts
└── post-card/
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── postCardAdmin.role.ts
```
**Plochá:**
```text
src/
├── application.config.ts
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── admin.role.ts
```
S volbou `--minimal` se vytvoří pouze základní soubory (`application-config.ts`, `roles/default-role.ts` a `logic-functions/post-install.ts`). S volbou `--interactive` si vyberete, které ukázkové soubory chcete zahrnout.
V kostce:
* **package.json**: Deklaruje název aplikace, verzi, engines (Node 24+, Yarn 4) a přidává `twenty-sdk` plus skripty jako `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` a `auth:login`, které delegují na lokální `twenty` CLI.
* **package.json**: Deklaruje název aplikace, verzi, engines (Node 24+, Yarn 4) a přidává `twenty-sdk` plus skript `twenty`, který deleguje na lokální `twenty` CLI. Spusťte `yarn twenty help` pro výpis všech dostupných příkazů.
* **.gitignore**: Ignoruje běžné artefakty jako `node_modules`, `.yarn`, `generated/` (typovaný klient), `dist/`, `build/`, složky s coverage, logy a soubory `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Zamykají a konfigurují nástrojový řetězec Yarn 4 používaný projektem.
* **.nvmrc**: Fixuje verzi Node.js požadovanou projektem.
* **eslint.config.mjs** a **tsconfig.json**: Poskytují lintování a konfiguraci TypeScriptu pro zdrojové soubory vaší aplikace v TypeScriptu.
* **README.md**: Krátké README v kořeni aplikace se základními pokyny.
* **public/**: Složka pro ukládání veřejných prostředků (obrázky, písma, statické soubory), které bude vaše aplikace poskytovat. Soubory umístěné zde se během synchronizace nahrají a jsou za běhu dostupné.
* **src/**: Hlavní místo, kde definujete svou aplikaci jako kód:
* `application.config.ts`: Globální konfigurace vaší aplikace (metadata a napojení za běhu). Viz „Konfigurace aplikace“ níže.
* `*.role.ts`: Definice rolí používané vašimi logickými funkcemi. Viz „Výchozí role funkce“ níže.
* `*.object.ts`: Definice vlastních objektů.
* `*.function.ts`: Definice logických funkcí.
* `*.front-component.tsx`: Definice frontových komponent.
* **src/**: Hlavní místo, kde definujete svou aplikaci jako kód
### Detekce entit
SDK detekuje entity analýzou vašich souborů TypeScript a hledá volání **`export default define<Entity>({...})`**. Každý typ entity má odpovídající pomocnou funkci exportovanou z `twenty-sdk`:
| Pomocná funkce | Typ entity |
| ---------------------------- | ------------------------------------- |
| `defineObject()` | Definice vlastních objektů |
| `defineLogicFunction()` | Definice logických funkcí |
| `defineFrontComponent()` | Definice frontendových komponent |
| `defineRole()` | Definice rolí |
| `defineField()` | Rozšíření polí u existujících objektů |
| `defineView()` | Definice uložených zobrazení |
| `defineNavigationMenuItem()` | Definice položek navigační nabídky |
<Note>
**Pojmenování souborů je flexibilní.** Detekce entit je založená na AST — SDK prochází vaše zdrojové soubory a hledá vzor `export default define<Entity>({...})`. Soubory a složky můžete organizovat, jak chcete. Seskupování podle typu entity (např. `logic-functions/`, `roles/`) je pouze konvence pro organizaci kódu, nikoli požadavek.
</Note>
Příklad detekované entity:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
Pozdější příkazy přidají další soubory a složky:
* `yarn app:generate` vytvoří složku `generated/` (typovaný klient Twenty + typy pracovního prostoru).
* `yarn entity:add` přidá soubory s definicemi entit do `src/` pro vaše vlastní objekty, funkce, frontové komponenty nebo role.
* `yarn twenty app:dev` automaticky vygeneruje typovaného klienta API v `node_modules/twenty-sdk/generated` (typovaný klient Twenty + typy pracovního prostoru).
* `yarn twenty entity:add` přidá soubory s definicemi entit do `src/` pro vaše vlastní objekty, funkce, frontové komponenty nebo role.
## Ověření
Při prvním spuštění `yarn auth:login` budete vyzváni k zadání:
Při prvním spuštění `yarn twenty auth:login` budete vyzváni k zadání:
* URL API (výchozí je http://localhost:3000 nebo váš aktuální profil pracovního prostoru)
* Klíč API
@@ -189,25 +184,25 @@ Vaše přihlašovací údaje se ukládají pro jednotlivé uživatele do `~/.twe
```bash filename="Terminal"
# Login interactively (recommended)
yarn auth:login
yarn twenty auth:login
# Login to a specific workspace profile
yarn auth:login --workspace my-custom-workspace
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn auth:list
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn auth:switch
yarn twenty auth:switch
# Switch to a specific workspace
yarn auth:switch production
yarn twenty auth:switch production
# Check current authentication status
yarn auth:status
yarn twenty auth:status
```
Jakmile přepnete pracovní prostor pomocí `auth:switch`, všechny následující příkazy budou tento pracovní prostor používat jako výchozí. Můžete jej stále dočasně přepsat pomocí `--workspace <name>`.
Jakmile přepnete pracovní prostor pomocí `yarn twenty auth:switch`, všechny následující příkazy budou tento pracovní prostor používat jako výchozí. Můžete jej stále dočasně přepsat pomocí `--workspace <name>`.
## Používejte zdroje SDK (typy a konfiguraci)
@@ -215,16 +210,20 @@ twenty-sdk poskytuje typované stavební bloky a pomocné funkce, které použí
### Pomocné funkce
SDK poskytuje čtyři pomocné funkce s vestavěnou validací pro definování entit vaší aplikace:
SDK poskytuje pomocné funkce pro definování entit vaší aplikace. Jak je popsáno v [Detekce entit](#entity-detection), musíte použít `export default define<Entity>({...})`, aby byly vaše entity detekovány:
| Funkce | Účel |
| --------------------- | ------------------------------------------------ |
| `defineApplication()` | Konfigurace metadat aplikace |
| `defineObject()` | Definice vlastních objektů s poli |
| `defineFunction()` | Definice logických funkcí s obslužnými funkcemi |
| `defineRole()` | Konfigurace oprávnění rolí a přístupu k objektům |
| Funkce | Účel |
| ---------------------------- | ----------------------------------------------------------------- |
| `defineApplication()` | Nakonfigurujte metadata aplikace (povinné, jedno na aplikaci) |
| `defineObject()` | Definice vlastních objektů s poli |
| `defineLogicFunction()` | Definice logických funkcí s obslužnými funkcemi |
| `defineFrontComponent()` | Definujte frontendové komponenty pro vlastní uživatelské rozhraní |
| `defineRole()` | Konfigurace oprávnění rolí a přístupu k objektům |
| `defineField()` | Rozšiřte existující objekty o další pole |
| `defineView()` | Definujte uložená zobrazení pro objekty |
| `defineNavigationMenuItem()` | Definujte odkazy postranní navigace |
Tyto funkce validují vaši konfiguraci za běhu a poskytují lepší automatické doplňování v IDE a lepší typovou bezpečnost.
Tyto funkce validují vaši konfiguraci v době sestavení a poskytují automatické doplňování v IDE a typovou bezpečnost.
### Definování objektů
@@ -305,26 +304,32 @@ Hlavní body:
* Hodnota `universalIdentifier` musí být jedinečná a stabilní napříč nasazeními.
* Každé pole vyžaduje `name`, `type`, `label` a svůj vlastní stabilní `universalIdentifier`.
* Pole `fields` je volitelné — objekty můžete definovat i bez vlastních polí.
* Nové objekty můžete vygenerovat pomocí `yarn entity:add`, který vás provede pojmenováním, poli a vztahy.
* Nové objekty můžete vygenerovat pomocí `yarn twenty entity:add`, který vás provede pojmenováním, poli a vztahy.
<Note>
**Základní pole jsou vytvořena automaticky.** Když definujete vlastní objekt, Twenty automaticky přidá standardní pole jako `name`, `createdAt`, `updatedAt`, `createdBy`, `position` a `deletedAt`. Nemusíte je definovat v poli `fields` — přidejte pouze svá vlastní pole.
**Základní pole jsou vytvořena automaticky.** Když definujete vlastní objekt, Twenty automaticky přidá standardní pole
jako `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` a `deletedAt`.
Nemusíte je definovat v poli `fields` — přidejte pouze svá vlastní pole.
Výchozí pole můžete přepsat definováním pole se stejným názvem v poli `fields`,
ale to se nedoporučuje.
</Note>
### Konfigurace aplikace (application.config.ts)
### Konfigurace aplikace (application-config.ts)
Každá aplikace má jeden soubor `application.config.ts`, který popisuje:
Každá aplikace má jeden soubor `application-config.ts`, který popisuje:
* **Identitu aplikace**: identifikátory, zobrazovaný název a popis.
* **Jak běží její funkce**: kterou roli používají pro oprávnění.
* **(Volitelné) proměnné**: dvojice klíč–hodnota zpřístupněné vašim funkcím jako proměnné prostředí.
* **(Volitelná) postinstalační funkce**: logická funkce, která se spouští po instalaci aplikace.
Use `defineApplication()` to define your application configuration:
```typescript
// src/app/application.config.ts
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -339,7 +344,8 @@ export default defineApplication({
isSecret: false,
},
},
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -347,11 +353,12 @@ Poznámky:
* Pole `universalIdentifier` jsou deterministická ID, která vlastníte; vygenerujte je jednou a udržujte je stabilní napříč synchronizacemi.
* `applicationVariables` se stanou proměnnými prostředí pro vaše funkce (například `DEFAULT_RECIPIENT_NAME` je dostupné jako `process.env.DEFAULT_RECIPIENT_NAME`).
* `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
* `defaultRoleUniversalIdentifier` se musí shodovat se souborem role (viz níže).
* `postInstallLogicFunctionUniversalIdentifier` (volitelné) odkazuje na logickou funkci, která se automaticky spustí po instalaci aplikace. Viz [Postinstalační funkce](#post-install-functions).
#### Role a oprávnění
Aplikace mohou definovat role, které zapouzdřují oprávnění k objektům a akcím ve vašem pracovním prostoru. The field `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
Aplikace mohou definovat role, které zapouzdřují oprávnění k objektům a akcím ve vašem pracovním prostoru. Pole `defaultRoleUniversalIdentifier` v `application-config.ts` určuje výchozí roli používanou logickými funkcemi vaší aplikace.
* Běhový klíč API vložený jako `TWENTY_API_KEY` je odvozen z této výchozí role funkcí.
* Typovaný klient bude omezen oprávněními udělenými této roli.
@@ -362,7 +369,7 @@ Aplikace mohou definovat role, které zapouzdřují oprávnění k objektům a a
Když vygenerujete novou aplikaci, CLI také vytvoří výchozí soubor role. K definování rolí s vestavěnou validací použijte `defineRole()`:
```typescript
// src/app/default-function.role.ts
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
@@ -401,10 +408,10 @@ export default defineRole({
});
```
Na `universalIdentifier` této role se poté odkazuje v `application.config.ts` jako na `roleUniversalIdentifier`. Jinými slovy:
Na `universalIdentifier` této role se poté odkazuje v `application-config.ts` jako na `defaultRoleUniversalIdentifier`. Jinými slovy:
* **\*.role.ts** definuje, co může výchozí role funkce dělat.
* **application.config.ts** ukazuje na tuto roli, aby vaše funkce zdědily její oprávnění.
* **application-config.ts** ukazuje na tuto roli, aby vaše funkce zdědily její oprávnění.
Poznámky:
@@ -415,11 +422,11 @@ Poznámky:
### Konfigurace logických funkcí a vstupní bod
Každý soubor funkce používá `defineFunction()` k exportu konfigurace s obslužnou funkcí (handlerem) a volitelnými spouštěči. Pro automatickou detekci použijte příponu souboru `*.function.ts`.
Každý soubor funkce používá `defineLogicFunction()` k exportu konfigurace s obslužnou funkcí (handlerem) a volitelnými spouštěči.
```typescript
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
@@ -439,7 +446,7 @@ const handler = async (params: RoutePayload) => {
return result;
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
@@ -486,6 +493,55 @@ Poznámky:
* Pole `triggers` je volitelné. Funkce bez spouštěčů lze použít jako pomocné funkce volané jinými funkcemi.
* V jedné funkci můžete kombinovat více typů spouštěčů.
### Postinstalační funkce
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.
Když vygenerujete kostru nové aplikace pomocí `create-twenty-app`, vytvoří se pro vás postinstalační funkce v `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
Funkce je připojena do vaší aplikace odkazem na její univerzální identifikátor v `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Postinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Hlavní body:
* Postinstalační funkce jsou standardní logické funkce — používají `defineLogicFunction()` stejně jako jakákoli jiná funkce.
* Pole `postInstallLogicFunctionUniversalIdentifier` v `defineApplication()` je volitelné. Pokud je vynecháno, po instalaci se nespustí žádná funkce.
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší úlohy nastavení, jako je naplnění daty.
* Postinstalační funkce nepotřebují spouštěče — jsou spouštěny platformou během instalace nebo ručně pomocí `function:execute --postInstall`.
### Payload spouštěče trasy
<Warning>
@@ -515,7 +571,7 @@ Poznámky:
Když spouštěč trasy vyvolá vaši logickou funkci, ta obdrží objekt `RoutePayload`, který odpovídá formátu AWS HTTP API v2. Importujte typ z `twenty-sdk`:
```typescript
import { defineFunction, type RoutePayload } from 'twenty-sdk';
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
@@ -545,7 +601,7 @@ Typ `RoutePayload` má následující strukturu:
Ve výchozím nastavení se záhlaví HTTP z příchozích požadavků z bezpečnostních důvodů do vaší logické funkce **ne** předávají. Chcete-li zpřístupnit konkrétní záhlaví, výslovně je uveďte v poli `forwardedRequestHeaders`:
```typescript
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
@@ -580,12 +636,114 @@ const handler = async (event: RoutePayload) => {
Nové funkce můžete vytvářet dvěma způsoby:
* **Vygenerované**: Spusťte `yarn entity:add` a zvolte možnost přidat novou funkci. Tím se vygeneruje startovací soubor s obslužnou funkcí a konfigurací.
* **Ruční**: Vytvořte nový soubor `*.function.ts` a použijte `defineFunction()` podle stejného vzoru.
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou logickou funkci. Tím se vygeneruje startovací soubor s obslužnou funkcí a konfigurací.
* **Ruční**: Vytvořte nový soubor `*.logic-function.ts` a použijte `defineLogicFunction()` podle stejného vzoru.
### Označení logické funkce jako nástroje
Logické funkce lze zpřístupnit jako **nástroje** pro agenty AI a pracovní postupy. Když je funkce označena jako nástroj, stane se dohledatelnou funkcemi AI produktu Twenty a lze ji vybrat jako krok v automatizacích pracovních postupů.
Chcete-li označit logickou funkci jako nástroj, nastavte `isTool: true` a poskytněte `toolInputSchema` popisující očekávané vstupní parametry pomocí [JSON Schema](https://json-schema.org/):
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
Hlavní body:
* **`isTool`** (`boolean`, výchozí: `false`): Když je nastaveno na `true`, funkce je zaregistrována jako nástroj a zpřístupní se agentům AI a automatizacím pracovních postupů.
* **`toolInputSchema`** (`object`, volitelné): Objekt JSON Schema, který popisuje parametry, jež vaše funkce přijímá. Agenti AI používají toto schéma k pochopení toho, jaké vstupy nástroj očekává, a k ověřování volání. Pokud je vynecháno, schéma má výchozí podobu `{ type: 'object', properties: {} }` (žádné parametry).
* Funkce s `isTool: false` (nebo není nastaveno) **nejsou** zpřístupněny jako nástroje. Stále je lze spouštět přímo nebo volat z jiných funkcí, ale neobjeví se ve vyhledávání nástrojů.
* **Pojmenování nástrojů**: Když je funkce zpřístupněna jako nástroj, její název se automaticky normalizuje na `logic_function_<name>` (převedeno na malá písmena, nealfanumerické znaky jsou nahrazeny podtržítky). Například `enrich-company` se změní na `logic_function_enrich_company`.
* Můžete kombinovat `isTool` se spouštěči — funkce může být zároveň nástrojem (volatelným agenty AI) i spouštěna událostmi (cron, databázové události, routes).
<Note>
**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>
### Frontendové komponenty
Frontendové komponenty vám umožňují vytvářet vlastní React komponenty, které se vykreslují v rozhraní Twenty. K definování komponent s vestavěnou validací použijte `defineFrontComponent()`:
```typescript
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
Hlavní body:
* Frontendové komponenty jsou React komponenty, které se vykreslují v izolovaných kontextech v rámci Twenty.
* Pro automatickou detekci použijte příponu souboru `*.front-component.tsx`.
* Pole `component` odkazuje na vaši React komponentu.
* Komponenty se během `yarn twenty app:dev` automaticky sestaví a synchronizují.
Nové frontendové komponenty můžete vytvořit dvěma způsoby:
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou frontendovou komponentu.
* **Ruční**: Vytvořte nový soubor `*.front-component.tsx` a použijte `defineFrontComponent()`.
### Generovaný typovaný klient
Spusťte yarn app:generate a vytvořte lokálního typovaného klienta v generated/ na základě schématu vašeho pracovního prostoru. Použijte jej ve svých funkcích:
Typovaný klient je automaticky generován pomocí `yarn twenty app:dev` a ukládá se do `node_modules/twenty-sdk/generated` podle schématu vašeho pracovního prostoru. Použijte jej ve svých funkcích:
```typescript
import Twenty from '~/generated';
@@ -594,7 +752,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
Klient je znovu generován příkazem `yarn app:generate`. Spusťte znovu po změně vašich objektů nebo při připojování k novému pracovnímu prostoru.
Klient se automaticky znovu generuje pomocí `yarn twenty app:dev` kdykoli se změní vaše objekty nebo pole.
#### Běhové přihlašovací údaje v logických funkcích
@@ -606,49 +764,38 @@ Když vaše funkce běží na Twenty, platforma před spuštěním kódu vloží
Poznámky:
* Není nutné předávat URL ani klíč API vygenerovanému klientovi. Za běhu čte `TWENTY_API_URL` a `TWENTY_API_KEY` z process.env.
* Oprávnění API klíče jsou určena rolí odkazovanou v `application.config.ts` prostřednictvím `roleUniversalIdentifier`. Toto je výchozí role používaná logickými funkcemi vaší aplikace.
* Aplikace mohou definovat role podle principu nejmenších oprávnění. Udělte pouze oprávnění, která vaše funkce potřebují, a poté nastavte `roleUniversalIdentifier` na univerzální identifikátor této role.
* Oprávnění klíče API jsou určena rolí odkazovanou ve vašem `application-config.ts` prostřednictvím `defaultRoleUniversalIdentifier`. Toto je výchozí role používaná logickými funkcemi vaší aplikace.
* Aplikace mohou definovat role podle principu nejmenších oprávnění. Udělte pouze oprávnění, která vaše funkce potřebují, a poté nastavte `defaultRoleUniversalIdentifier` na univerzální identifikátor této role.
### Příklad Hello World
Prozkoumejte minimalistický end-to-end příklad, který demonstruje objekty, funkce a více spouštěčů [zde](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
Prozkoumejte minimalistický end-to-end příklad, který demonstruje objekty, logické funkce, frontendové komponenty a více spouštěčů [zde](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Ruční nastavení (bez scaffolderu)
Ačkoli pro nejlepší začátky doporučujeme použít `create-twenty-app`, projekt můžete nastavit i ručně. Neinstalujte CLI globálně. Místo toho přidejte `twenty-sdk` jako lokální závislost a propojte skripty v souboru package.json:
Ačkoli pro nejlepší začátky doporučujeme použít `create-twenty-app`, projekt můžete nastavit i ručně. Neinstalujte CLI globálně. Místo toho přidejte `twenty-sdk` jako lokální závislost a přidejte jeden skript do souboru package.json:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
Poté přidejte skripty jako tyto:
Poté přidejte skript `twenty`:
```json filename="package.json"
{
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
"twenty": "twenty"
}
}
```
Nyní můžete spouštět stejné příkazy přes Yarn, např. `yarn app:dev`, `yarn app:generate` atd.
Nyní můžete spouštět všechny příkazy přes `yarn twenty <command>`, např. `yarn twenty app:dev`, `yarn twenty help` atd.
## Řešení potíží
* Chyby ověření: spusťte `yarn auth:login` a ujistěte se, že váš klíč API má požadovaná oprávnění.
* Chyby ověření: spusťte `yarn twenty auth:login` a ujistěte se, že váš klíč API má požadovaná oprávnění.
* Nelze se připojit k serveru: ověřte URL API a že je server Twenty dosažitelný.
* Typy nebo klient chybí nebo jsou zastaralé: spusťte `yarn app:generate`.
* Režim vývoje nesynchronizuje: ujistěte se, že běží `yarn app:dev` a že vaše prostředí změny neignoruje.
* Types or client missing/outdated: restart `yarn twenty app:dev` — it auto-generates the typed client.
* Režim vývoje se nesynchronizuje: ujistěte se, že běží `yarn twenty app:dev` a že vaše prostředí změny neignoruje.
Kanál podpory na Discordu: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -17,10 +17,6 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
* Logikfunktionen mit benutzerdefinierten Triggern erstellen
* Dieselbe App in mehreren Workspaces bereitstellen
**Bald verfügbar:**
* Benutzerdefinierte UI-Layouts und Komponenten
## Voraussetzungen
* Node.js 24+ und Yarn 4
@@ -31,41 +27,54 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
Erstellen Sie mit dem offiziellen Scaffolder eine neue App, authentifizieren Sie sich und beginnen Sie mit der Entwicklung:
```bash filename="Terminal"
# Scaffold a new app
# Eine neue App erstellen (enthält standardmäßig alle Beispiele)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# If you don't use yarn@4
# Falls du yarn@4 nicht verwendest
corepack enable
yarn install
# Authenticate using your API key (you'll be prompted)
yarn auth:login
# Mit deinem API-Schlüssel authentifizieren (du wirst dazu aufgefordert)
yarn twenty auth:login
# Start dev mode: automatically syncs local changes to your workspace
yarn app:dev
# Dev-Modus starten: synchronisiert lokale Änderungen automatisch mit deinem Arbeitsbereich
yarn twenty app:dev
```
Das Scaffolding-Tool unterstützt drei Modi, um zu steuern, welche Beispieldateien enthalten sind:
```bash filename="Terminal"
# Standard (umfassend): alle Beispiele (Objekt, Feld, Logikfunktion, Frontend-Komponente, View, Navigationsmenüeintrag)
npx create-twenty-app@latest my-app
# Minimal: nur Kerndateien (application-config.ts und default-role.ts)
npx create-twenty-app@latest my-app --minimal
# Interaktiv: wähle aus, welche Beispiele enthalten sein sollen
npx create-twenty-app@latest my-app --interactive
```
Von hier aus können Sie:
```bash filename="Terminal"
# Eine neue Entität zu deiner Anwendung hinzufügen (geführt)
yarn entity:add
# Eine neue Entität zu Ihrer Anwendung hinzufügen (geführt)
yarn twenty entity:add
# Einen typisierten Twenty-Client und Entitätstypen für den Arbeitsbereich generieren
yarn app:generate
# Die Funktionsprotokolle deiner Anwendung überwachen
yarn function:logs
# Die Funktionsprotokolle Ihrer Anwendung überwachen
yarn twenty function:logs
# Eine Funktion anhand ihres Namens ausführen
yarn function:execute -n my-function -p '{"name": "test"}'
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Die Post-Installationsfunktion ausführen
yarn twenty function:execute --postInstall
# Die Anwendung aus dem aktuellen Arbeitsbereich deinstallieren
yarn app:uninstall
yarn twenty app:uninstall
# Hilfe zu Befehlen anzeigen
yarn help
yarn twenty help
```
Siehe auch: die CLI-Referenzseiten für [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) und [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@@ -77,9 +86,9 @@ Wenn Sie `npx create-twenty-app@latest my-twenty-app` ausführen, erledigt der S
* Kopiert eine minimale Basisanwendung nach `my-twenty-app/`
* Fügt eine lokale `twenty-sdk`-Abhängigkeit und die Yarn-4-Konfiguration hinzu
* Erstellt Konfigurationsdateien und Skripte, die an die `twenty`-CLI angebunden sind
* Generiert eine Standard-Anwendungskonfiguration und eine Standard-Funktionsrolle
* Erzeugt Kerndateien (Anwendungskonfiguration, Standardrolle für Logikfunktionen, Post-Installationsfunktion) sowie Beispieldateien entsprechend dem Scaffolding-Modus
Eine frisch erzeugte App sieht so aus:
Eine frisch erstellte App mit dem Standardmodus `--exhaustive` sieht so aus:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -95,90 +104,76 @@ my-twenty-app/
README.md
public/ # Ordner für öffentliche Assets (Bilder, Schriftarten usw.)
src/
application.config.ts # Erforderlich Hauptkonfiguration der Anwendung
default-function.role.ts # Standardrolle für serverlose Funktionen
hello-world.function.ts # Beispiel für eine serverlose Funktion
hello-world.front-component.tsx # Beispiel für eine Frontend-Komponente
// Ihre Entitäten (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
application-config.ts # Erforderlich  Hauptkonfiguration der Anwendung
roles/
 default-role.ts # Standardrolle für Logikfunktionen
objects/
 example-object.ts # Beispiel für eine benutzerdefinierte Objektdefinition
fields/
 example-field.ts # Beispiel für eine eigenständige Felddefinition
logic-functions/
 hello-world.ts # Beispiel für eine Logikfunktion
 post-install.ts # Post-Installations-Logikfunktion
front-components/
 hello-world.tsx # Beispiel für eine Frontend-Komponente
views/
 example-view.ts # Beispiel für eine gespeicherte View-Definition
navigation-menu-items/
example-navigation-menu-item.ts # Beispiel für einen Navigationslink in der Seitenleiste
```
### Konvention vor Konfiguration
Anwendungen verwenden einen Ansatz **Konvention vor Konfiguration**, bei dem Entitäten anhand ihrer Dateiendung erkannt werden. Dies ermöglicht eine flexible Organisation im Ordner `src/app/`:
| Dateiendung | Entitätstyp |
| ----------------------- | ------------------------------------- |
| `*.object.ts` | Benutzerdefinierte Objektdefinitionen |
| `*.function.ts` | Definitionen serverloser Funktionen |
| `*.front-component.tsx` | Definitionen von Frontend-Komponenten |
| `*.role.ts` | Rollendefinitionen |
### Unterstützte Ordnerorganisationen
Sie können Ihre Entitäten nach einem der folgenden Muster organisieren:
**Traditionell (nach Typ):**
```text
src/
├── application.config.ts
├── objects/
│ └── postCard.object.ts
├── functions/
│ └── createPostCard.function.ts
├── components/
│ └── card.front-component.tsx
└── roles/
└── admin.role.ts
```
**Feature-basiert:**
```text
src/
├── application.config.ts
└── post-card/
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── postCardAdmin.role.ts
```
**Flach:**
```text
src/
├── application.config.ts
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── admin.role.ts
```
Mit `--minimal` werden nur die Kerndateien erstellt (`application-config.ts`, `roles/default-role.ts` und `logic-functions/post-install.ts`). Mit `--interactive` wählst du aus, welche Beispieldateien enthalten sein sollen.
Auf hoher Ebene:
* **package.json**: Deklariert den App-Namen, die Version, Engines (Node 24+, Yarn 4) und fügt `twenty-sdk` sowie Skripte wie `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` und `auth:login` hinzu, die an die lokale `twenty`-CLI delegieren.
* **package.json**: Deklariert den App-Namen, die Version und die Engines (Node 24+, Yarn 4) und fügt `twenty-sdk` sowie ein `twenty`-Skript hinzu, das an die lokale `twenty`-CLI delegiert. Führe `yarn twenty help` aus, um alle verfügbaren Befehle aufzulisten.
* **.gitignore**: Ignoriert übliche Artefakte wie `node_modules`, `.yarn`, `generated/` (typisierter Client), `dist/`, `build/`, Coverage-Ordner, Logdateien und `.env*`-Dateien.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Fixieren und konfigurieren die vom Projekt verwendete Yarn-4-Toolchain.
* **.nvmrc**: Legt die vom Projekt erwartete Node.js-Version fest.
* **eslint.config.mjs** und **tsconfig.json**: Stellen Linting und TypeScript-Konfiguration für die TypeScript-Quellen Ihrer App bereit.
* **README.md**: Ein kurzes README im App-Root mit grundlegenden Anweisungen.
* **public/**: Ein Ordner zum Speichern öffentlicher Assets (Bilder, Schriftarten, statische Dateien), die zusammen mit Ihrer Anwendung bereitgestellt werden. Hier abgelegte Dateien werden während der Synchronisierung hochgeladen und sind zur Laufzeit zugänglich.
* **src/**: Der Hauptort, an dem Sie Ihre Anwendung als Code definieren:
* `application.config.ts`: Globale Konfiguration für Ihre App (Metadaten und Laufzeit-Anbindung). Siehe unten „Anwendungskonfiguration“.
* `*.role.ts`: Rollendefinitionen, die von Ihren Logikfunktionen verwendet werden. Siehe unten „Standard-Funktionsrolle“.
* `*.object.ts`: Benutzerdefinierte Objektdefinitionen.
* `*.function.ts`: Definitionen von Logikfunktionen.
* `*.front-component.tsx`: Front-Komponenten-Definitionen.
* **src/**: Der Hauptort, an dem Sie Ihre Anwendung als Code definieren
### Entitätserkennung
Das SDK erkennt Entitäten, indem es Ihre TypeScript-Dateien nach Aufrufen von **`export default define<Entity>({...})`** parst. Für jeden Entitätstyp gibt es eine entsprechende Hilfsfunktion, die aus `twenty-sdk` exportiert wird:
| Hilfsfunktion | Entitätstyp |
| ---------------------------- | ----------------------------------------- |
| `defineObject()` | Benutzerdefinierte Objektdefinitionen |
| `defineLogicFunction()` | Definitionen von Logikfunktionen |
| `defineFrontComponent()` | Definitionen von Frontend-Komponenten |
| `defineRole()` | Rollendefinitionen |
| `defineField()` | Felderweiterungen für bestehende Objekte |
| `defineView()` | Gespeicherte View-Definitionen |
| `defineNavigationMenuItem()` | Definitionen von Navigationsmenüeinträgen |
<Note>
**Dateibenennung ist flexibel.** Die Entitätserkennung ist AST-basiert — das SDK durchsucht Ihre Quelldateien nach dem Muster `export default define<Entity>({...})`. Sie können Ihre Dateien und Ordner nach Belieben organisieren. Die Gruppierung nach Entitätstyp (z. B. `logic-functions/`, `roles/`) ist lediglich eine Konvention zur Codeorganisation, keine Voraussetzung.
</Note>
Beispiel für eine erkannte Entität:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
Spätere Befehle fügen weitere Dateien und Ordner hinzu:
* `yarn app:generate` erstellt einen `generated/`-Ordner (typisierter Twenty-Client + Workspace-Typen).
* `yarn entity:add` fügt unter `src/` Entitätsdefinitionsdateien für Ihre benutzerdefinierten Objekte, Funktionen, Front-Komponenten oder Rollen hinzu.
* `yarn twenty app:dev` generiert automatisch einen typisierten API-Client in `node_modules/twenty-sdk/generated` (typisierter Twenty-Client + Arbeitsbereichs-Typen).
* `yarn twenty entity:add` fügt unter `src/` Entitätsdefinitionsdateien für benutzerdefinierte Objekte, Funktionen, Frontend-Komponenten oder Rollen hinzu.
## Authentifizierung
Wenn Sie `yarn auth:login` zum ersten Mal ausführen, werden Sie nach Folgendem gefragt:
Wenn Sie `yarn twenty auth:login` zum ersten Mal ausführen, werden Sie nach Folgendem gefragt:
* API-URL (standardmäßig http://localhost:3000 oder Ihr aktuelles Workspace-Profil)
* API-Schlüssel
@@ -189,25 +184,25 @@ Ihre Anmeldedaten werden pro Benutzer in `~/.twenty/config.json` gespeichert. Si
```bash filename="Terminal"
# Login interactively (recommended)
yarn auth:login
yarn twenty auth:login
# Login to a specific workspace profile
yarn auth:login --workspace my-custom-workspace
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn auth:list
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn auth:switch
yarn twenty auth:switch
# Switch to a specific workspace
yarn auth:switch production
yarn twenty auth:switch production
# Check current authentication status
yarn auth:status
yarn twenty auth:status
```
Sobald Sie mit `auth:switch` den Arbeitsbereich gewechselt haben, verwenden alle nachfolgenden Befehle standardmäßig diesen Arbeitsbereich. Sie können es weiterhin vorübergehend mit `--workspace <name>` überschreiben.
Sobald Sie mit `yarn twenty auth:switch` den Arbeitsbereich gewechselt haben, verwenden alle nachfolgenden Befehle standardmäßig diesen Arbeitsbereich. Sie können es weiterhin vorübergehend mit `--workspace <name>` überschreiben.
## SDK-Ressourcen verwenden (Typen & Konfiguration)
@@ -215,16 +210,20 @@ Das twenty-sdk stellt typisierte Bausteine und Hilfsfunktionen bereit, die Sie i
### Hilfsfunktionen
Das SDK stellt vier Hilfsfunktionen mit eingebauter Validierung bereit, um Ihre App-Entitäten zu definieren:
Das SDK stellt Hilfsfunktionen bereit, um die Entitäten Ihrer App zu definieren. Wie in [Entitätserkennung](#entity-detection) beschrieben, müssen Sie `export default define<Entity>({...})` verwenden, damit Ihre Entitäten erkannt werden:
| Funktion | Zweck |
| --------------------- | ---------------------------------------------------- |
| `defineApplication()` | Anwendungsmetadaten konfigurieren |
| `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren |
| `defineFunction()` | Logikfunktionen mit Handlern definieren |
| `defineRole()` | Rollenberechtigungen und Objektzugriff konfigurieren |
| Funktion | Zweck |
| ---------------------------- | -------------------------------------------------------------- |
| `defineApplication()` | Anwendungsmetadaten konfigurieren (erforderlich, eine pro App) |
| `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren |
| `defineLogicFunction()` | Logikfunktionen mit Handlern definieren |
| `defineFrontComponent()` | Frontend-Komponenten für benutzerdefinierte UI definieren |
| `defineRole()` | Rollenberechtigungen und Objektzugriff konfigurieren |
| `defineField()` | Bestehende Objekte mit zusätzlichen Feldern erweitern |
| `defineView()` | Gespeicherte Views für Objekte definieren |
| `defineNavigationMenuItem()` | Seitenleisten-Navigationslinks definieren |
Diese Funktionen validieren Ihre Konfiguration zur Laufzeit und bieten bessere IDE-Autovervollständigung sowie Typsicherheit.
Diese Funktionen validieren Ihre Konfiguration zur Build-Zeit und bieten IDE-Autovervollständigung sowie Typsicherheit.
### Objekte definieren
@@ -305,26 +304,32 @@ Hauptpunkte:
* Der `universalIdentifier` muss eindeutig und über Deployments hinweg stabil sein.
* Jedes Feld benötigt `name`, `type`, `label` und einen eigenen stabilen `universalIdentifier`.
* Das Array `fields` ist optional — Sie können Objekte ohne benutzerdefinierte Felder definieren.
* Sie können mit `yarn entity:add` neue Objekte erzeugen; der Assistent führt Sie durch Benennung, Felder und Beziehungen.
* Sie können mit `yarn twenty entity:add` neue Objekte erzeugen; der Assistent führt Sie durch Benennung, Felder und Beziehungen.
<Note>
**Basisfelder werden automatisch erstellt.** Wenn Sie ein benutzerdefiniertes Objekt definieren, fügt Twenty automatisch Standardfelder wie `name`, `createdAt`, `updatedAt`, `createdBy`, `position` und `deletedAt` hinzu. Sie müssen diese nicht in Ihrem `fields`-Array definieren — fügen Sie nur Ihre benutzerdefinierten Felder hinzu.
**Basisfelder werden automatisch erstellt.** Wenn Sie ein benutzerdefiniertes Objekt definieren, fügt Twenty automatisch Standardfelder hinzu
wie `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` und `deletedAt`.
Sie müssen diese nicht in Ihrem `fields`-Array definieren — fügen Sie nur Ihre benutzerdefinierten Felder hinzu.
Sie können Standardfelder überschreiben, indem Sie in Ihrem `fields`-Array ein Feld mit demselben Namen definieren,
dies wird jedoch nicht empfohlen.
</Note>
### Anwendungskonfiguration (application.config.ts)
### Anwendungskonfiguration (application-config.ts)
Jede App hat eine einzelne Datei `application.config.ts`, die Folgendes beschreibt:
Jede App hat eine einzelne Datei `application-config.ts`, die Folgendes beschreibt:
* **Was die App ist**: Bezeichner, Anzeigename und Beschreibung.
* **Wie ihre Funktionen ausgeführt werden**: welche Rolle sie für Berechtigungen verwenden.
* **(Optional) Variablen**: SchlüsselWert-Paare, die Ihren Funktionen als Umgebungsvariablen zur Verfügung gestellt werden.
* **(Optional) Post-Installationsfunktion**: eine Logikfunktion, die nach der Installation der App ausgeführt wird.
Verwenden Sie `defineApplication()`, um Ihre Anwendungskonfiguration zu definieren:
```typescript
// src/app/application.config.ts
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -339,7 +344,8 @@ export default defineApplication({
isSecret: false,
},
},
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -347,11 +353,12 @@ Notizen:
* `universalIdentifier`-Felder sind deterministische IDs, die Sie besitzen; generieren Sie sie einmal und halten Sie sie über Synchronisierungen hinweg stabil.
* `applicationVariables` werden zu Umgebungsvariablen für Ihre Funktionen (zum Beispiel ist `DEFAULT_RECIPIENT_NAME` als `process.env.DEFAULT_RECIPIENT_NAME` verfügbar).
* `roleUniversalIdentifier` muss mit der Rolle übereinstimmen, die Sie in Ihrer `*.role.ts`-Datei definieren (siehe unten).
* `defaultRoleUniversalIdentifier` muss mit der Rollendatei übereinstimmen (siehe unten).
* `postInstallLogicFunctionUniversalIdentifier` (optional) verweist auf eine Logikfunktion, die nach der Installation der App automatisch ausgeführt wird. Siehe [Post-Installationsfunktionen](#post-install-functions).
#### Rollen und Berechtigungen
Anwendungen können Rollen definieren, die Berechtigungen für die Objekte und Aktionen Ihres Workspaces kapseln. Das Feld `roleUniversalIdentifier` in `application.config.ts` legt die Standardrolle fest, die von den Logikfunktionen Ihrer App verwendet wird.
Anwendungen können Rollen definieren, die Berechtigungen für die Objekte und Aktionen Ihres Workspaces kapseln. Das Feld `defaultRoleUniversalIdentifier` in `application-config.ts` legt die Standardrolle fest, die von den Logikfunktionen Ihrer App verwendet wird.
* Der zur Laufzeit als `TWENTY_API_KEY` injizierte API-Schlüssel wird von dieser Standard-Funktionsrolle abgeleitet.
* Der typisierte Client ist auf die dieser Rolle gewährten Berechtigungen beschränkt.
@@ -362,7 +369,7 @@ Anwendungen können Rollen definieren, die Berechtigungen für die Objekte und A
Wenn Sie eine neue App erzeugen, erstellt die CLI auch eine Standard-Rolldatei. Verwenden Sie `defineRole()`, um Rollen mit eingebauter Validierung zu definieren:
```typescript
// src/app/default-function.role.ts
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
@@ -401,10 +408,10 @@ export default defineRole({
});
```
Der `universalIdentifier` dieser Rolle wird anschließend in `application.config.ts` als `roleUniversalIdentifier` referenziert. Anders ausgedrückt:
Der `universalIdentifier` dieser Rolle wird anschließend in `application-config.ts` als `defaultRoleUniversalIdentifier` referenziert. Anders ausgedrückt:
* **\*.role.ts** definiert, was die Standard-Funktionsrolle darf.
* **application.config.ts** verweist auf diese Rolle, sodass Ihre Funktionen deren Berechtigungen erben.
* **application-config.ts** verweist auf diese Rolle, sodass Ihre Funktionen deren Berechtigungen erben.
Notizen:
@@ -415,11 +422,11 @@ Notizen:
### Konfiguration von Logikfunktionen und Einstiegspunkt
Jede Funktionsdatei verwendet `defineFunction()`, um eine Konfiguration mit einem Handler und optionalen Triggern zu exportieren. Verwenden Sie die Dateiendung `*.function.ts` für die automatische Erkennung.
Jede Funktionsdatei verwendet `defineLogicFunction()`, um eine Konfiguration mit einem Handler und optionalen Triggern zu exportieren.
```typescript
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
@@ -439,7 +446,7 @@ const handler = async (params: RoutePayload) => {
return result;
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
@@ -486,6 +493,55 @@ Notizen:
* Das Array `triggers` ist optional. Funktionen ohne Trigger können als von anderen Funktionen aufgerufene Utility-Funktionen verwendet werden.
* Sie können mehrere Trigger-Typen in einer Funktion kombinieren.
### Post-Installationsfunktionen
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.
Wenn du mit `create-twenty-app` eine neue App erstellst, wird für dich eine Post-Installationsfunktion unter `src/logic-functions/post-install.ts` erzeugt:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
Die Funktion wird in deine App eingebunden, indem ihr universeller Bezeichner in `application-config.ts` referenziert wird:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Du kannst die Post-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Hauptpunkte:
* Post-Installationsfunktionen sind Standard-Logikfunktionen — sie verwenden `defineLogicFunction()` wie jede andere Funktion.
* Das Feld `postInstallLogicFunctionUniversalIdentifier` in `defineApplication()` ist optional. Wenn es weggelassen wird, wird nach der Installation keine Funktion ausgeführt.
* Das standardmäßige Timeout ist auf 300 Sekunden (5 Minuten) festgelegt, um längere Einrichtungsvorgänge wie Daten-Seeding zu ermöglichen.
* Post-Installationsfunktionen benötigen keine Trigger — sie werden von der Plattform während der Installation oder manuell über `function:execute --postInstall` aufgerufen.
### Routen-Trigger-Payload
<Warning>
@@ -515,7 +571,7 @@ Notizen:
Wenn ein Routen-Trigger Ihre Logikfunktion aufruft, erhält sie ein `RoutePayload`-Objekt, das dem AWS HTTP API v2-Format entspricht. Importieren Sie den Typ aus `twenty-sdk`:
```typescript
import { defineFunction, type RoutePayload } from 'twenty-sdk';
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
@@ -545,7 +601,7 @@ Der Typ `RoutePayload` hat die folgende Struktur:
Standardmäßig werden HTTP-Header von eingehenden Anfragen aus Sicherheitsgründen nicht an Ihre Logikfunktion weitergegeben. Um auf bestimmte Header zuzugreifen, listen Sie diese explizit im Array `forwardedRequestHeaders` auf:
```typescript
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
@@ -580,12 +636,114 @@ const handler = async (event: RoutePayload) => {
Sie können neue Funktionen auf zwei Arten erstellen:
* **Generiert**: Führen Sie `yarn entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Funktion. Dadurch wird eine Starterdatei mit Handler und Konfiguration erzeugt.
* **Manuell**: Erstellen Sie eine neue `*.function.ts`-Datei und verwenden Sie `defineFunction()` nach demselben Muster.
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Logikfunktion. Dadurch wird eine Starterdatei mit Handler und Konfiguration erzeugt.
* **Manuell**: Erstellen Sie eine neue `*.logic-function.ts`-Datei und verwenden Sie `defineLogicFunction()` nach demselben Muster.
### Eine Logikfunktion als Tool markieren
Logikfunktionen können als **Tools** für KI-Agenten und Workflows verfügbar gemacht werden. Wenn eine Funktion als Tool markiert ist, wird sie von den KI-Funktionen von Twenty auffindbar und kann als Schritt in Workflow-Automatisierungen ausgewählt werden.
Um eine Logikfunktion als Tool zu markieren, setzen Sie `isTool: true` und geben Sie ein `toolInputSchema` an, das die erwarteten Eingabeparameter mithilfe von [JSON Schema](https://json-schema.org/) beschreibt:
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
Hauptpunkte:
* **`isTool`** (`boolean`, Standard: `false`): Wenn auf `true` gesetzt, wird die Funktion als Tool registriert und steht KI-Agenten und Workflow-Automatisierungen zur Verfügung.
* **`toolInputSchema`** (`object`, optional): Ein JSON-Schema-Objekt, das die Parameter beschreibt, die Ihre Funktion akzeptiert. KI-Agenten verwenden dieses Schema, um zu verstehen, welche Eingaben das Tool erwartet, und um Aufrufe zu validieren. Falls weggelassen, lautet der Standardwert für das Schema `{ type: 'object', properties: {} }` (keine Parameter).
* Funktionen mit `isTool: false` (oder nicht gesetzt) werden **nicht** als Tools bereitgestellt. Sie können weiterhin direkt ausgeführt oder von anderen Funktionen aufgerufen werden, erscheinen jedoch nicht in der Tool-Erkennung.
* **Tool-Benennung**: Wenn als Tool bereitgestellt, wird der Funktionsname automatisch zu `logic_function_<name>` normalisiert (in Kleinbuchstaben umgewandelt, nicht alphanumerische Zeichen durch Unterstriche ersetzt). Beispielsweise wird `enrich-company` zu `logic_function_enrich_company`.
* Sie können `isTool` mit Triggern kombinieren — eine Funktion kann gleichzeitig sowohl ein Tool (von KI-Agenten aufrufbar) als auch durch Ereignisse (Cron, Datenbankereignisse, Routen) ausgelöst werden.
<Note>
**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>
### Frontend-Komponenten
Frontend-Komponenten ermöglichen es Ihnen, benutzerdefinierte React-Komponenten zu erstellen, die innerhalb der Twenty-UI gerendert werden. Verwenden Sie `defineFrontComponent()`, um Komponenten mit eingebauter Validierung zu definieren:
```typescript
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
Hauptpunkte:
* Frontend-Komponenten sind React-Komponenten, die in isolierten Kontexten innerhalb von Twenty gerendert werden.
* Verwenden Sie die Dateiendung `*.front-component.tsx` für die automatische Erkennung.
* Das Feld `component` verweist auf Ihre React-Komponente.
* Komponenten werden während `yarn twenty app:dev` automatisch gebaut und synchronisiert.
Sie können neue Frontend-Komponenten auf zwei Arten erstellen:
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Frontend-Komponente.
* **Manuell**: Erstellen Sie eine neue `*.front-component.tsx`-Datei und verwenden Sie `defineFrontComponent()`.
### Generierter typisierter Client
Führen Sie yarn app:generate aus, um einen lokalen typisierten Client in generated/ basierend auf Ihrem Workspace-Schema zu erstellen. Verwenden Sie ihn in Ihren Funktionen:
Der typisierte Client wird von `yarn twenty app:dev` automatisch generiert und basierend auf Ihrem Arbeitsbereichs-Schema in `node_modules/twenty-sdk/generated` gespeichert. Verwenden Sie ihn in Ihren Funktionen:
```typescript
import Twenty from '~/generated';
@@ -594,7 +752,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
Der Client wird durch `yarn app:generate` erneut generiert. Führen Sie ihn nach Änderungen an Ihren Objekten oder beim Onboarding in einen neuen Workspace erneut aus.
Der Client wird von `yarn twenty app:dev` automatisch neu generiert, sobald sich Ihre Objekte oder Felder ändern.
#### Laufzeit-Anmeldedaten in Logikfunktionen
@@ -606,49 +764,38 @@ Wenn Ihre Funktion auf Twenty läuft, injiziert die Plattform vor der Ausführun
Notizen:
* Sie müssen dem generierten Client weder URL noch API-Schlüssel übergeben. Er liest `TWENTY_API_URL` und `TWENTY_API_KEY` zur Laufzeit aus process.env.
* Die Berechtigungen des API-Schlüssels werden durch die Rolle bestimmt, auf die in Ihrer `application.config.ts` über `roleUniversalIdentifier` verwiesen wird. Dies ist die Standardrolle, die von den Logikfunktionen Ihrer Anwendung verwendet wird.
* Anwendungen können Rollen definieren, um das Least-Privilege-Prinzip einzuhalten. Gewähren Sie nur die Berechtigungen, die Ihre Funktionen benötigen, und verweisen Sie dann mit `roleUniversalIdentifier` auf den universellen Bezeichner dieser Rolle.
* Die Berechtigungen des API-Schlüssels werden durch die Rolle bestimmt, auf die in Ihrer `application-config.ts` über `defaultRoleUniversalIdentifier` verwiesen wird. Dies ist die Standardrolle, die von den Logikfunktionen Ihrer Anwendung verwendet wird.
* Anwendungen können Rollen definieren, um das Least-Privilege-Prinzip einzuhalten. Gewähren Sie nur die Berechtigungen, die Ihre Funktionen benötigen, und verweisen Sie dann mit `defaultRoleUniversalIdentifier` auf den universellen Bezeichner dieser Rolle.
### Hello-World-Beispiel
Ein minimales End-to-End-Beispiel, das Objekte, Funktionen und mehrere Trigger demonstriert, finden Sie [hier](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
Ein minimales End-to-End-Beispiel, das Objekte, Logikfunktionen, Frontend-Komponenten und mehrere Trigger demonstriert, finden Sie [hier](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Manuelle Einrichtung (ohne Scaffolder)
Wir empfehlen zwar `create-twenty-app` für das beste Einstiegserlebnis, Sie können ein Projekt aber auch manuell einrichten. Installieren Sie die CLI nicht global. Fügen Sie stattdessen `twenty-sdk` als lokale Abhängigkeit hinzu und binden Sie Skripte in Ihrer package.json ein:
Wir empfehlen zwar `create-twenty-app` für das beste Einstiegserlebnis, Sie können ein Projekt aber auch manuell einrichten. Installieren Sie die CLI nicht global. Fügen Sie stattdessen `twenty-sdk` als lokale Abhängigkeit hinzu und binden Sie ein einzelnes Skript in Ihrer package.json ein:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
Fügen Sie dann Skripte wie diese hinzu:
Fügen Sie dann ein `twenty`-Skript hinzu:
```json filename="package.json"
{
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
"twenty": "twenty"
}
}
```
Jetzt können Sie dieselben Befehle über Yarn ausführen, z. B. `yarn app:dev`, `yarn app:generate` usw.
Jetzt können Sie alle Befehle über `yarn twenty <command>` ausführen, z. B. `yarn twenty app:dev`, `yarn twenty help` usw.
## Fehlerbehebung
* Authentifizierungsfehler: Führen Sie `yarn auth:login` aus und stellen Sie sicher, dass Ihr API-Schlüssel die erforderlichen Berechtigungen hat.
* Authentifizierungsfehler: Führen Sie `yarn twenty auth:login` aus und stellen Sie sicher, dass Ihr API-Schlüssel die erforderlichen Berechtigungen hat.
* Verbindung zum Server nicht möglich: Überprüfen Sie die API-URL und dass der Twenty-Server erreichbar ist.
* Typen oder Client fehlen/veraltet: Führen Sie `yarn app:generate` aus.
* Dev-Modus synchronisiert nicht: Stellen Sie sicher, dass `yarn app:dev` läuft und dass Änderungen von Ihrer Umgebung nicht ignoriert werden.
* Typen oder Client fehlen/veraltet: Starten Sie `yarn twenty app:dev` neu — der typisierte Client wird automatisch generiert.
* Dev-Modus synchronisiert nicht: Stellen Sie sicher, dass `yarn twenty app:dev` läuft und dass Änderungen von Ihrer Umgebung nicht ignoriert werden.
Discord-Hilfekanal: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -52,9 +52,6 @@ Desde aquí usted puede:
# Añade una nueva entidad a tu aplicación (guiado)
yarn entity:add
# Genera un cliente tipado de Twenty y tipos de entidad del espacio de trabajo
yarn app:generate
# Supervisa los registros de funciones de tu aplicación
yarn function:logs
@@ -157,7 +154,7 @@ src/
A grandes rasgos:
* **package.json**: Declara el nombre de la aplicación, la versión, los entornos (Node 24+, Yarn 4) y agrega `twenty-sdk` además de scripts como `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` y `auth:login` que delegan en la CLI local `twenty`.
* **package.json**: Declara el nombre de la aplicación, la versión, los entornos (Node 24+, Yarn 4) y agrega `twenty-sdk` además de scripts como `app:dev`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` y `auth:login` que delegan en la CLI local `twenty`.
* **.gitignore**: Ignora artefactos comunes como `node_modules`, `.yarn`, `generated/` (cliente tipado), `dist/`, `build/`, carpetas de cobertura, archivos de registro y archivos `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Bloquean y configuran la cadena de herramientas Yarn 4 utilizada por el proyecto.
* **.nvmrc**: Fija la versión de Node.js esperada por el proyecto.
@@ -173,7 +170,7 @@ A grandes rasgos:
Comandos posteriores añadirán más archivos y carpetas:
* `yarn app:generate` creará una carpeta `generated/` (cliente tipado de Twenty + tipos del espacio de trabajo).
* `yarn app:dev` genera automáticamente el cliente Twenty tipado en `node_modules/twenty-sdk/generated`.
* `yarn entity:add` añadirá archivos de definición de entidades en `src/` para tus objetos, funciones, componentes de interfaz o roles personalizados.
## Autenticación
@@ -585,7 +582,7 @@ Puedes crear funciones nuevas de dos maneras:
### Cliente tipado generado
Ejecuta yarn app:generate para crear un cliente tipado local en generated/ basado en el esquema de tu espacio de trabajo. Úsalo en tus funciones:
`yarn app:dev` genera automáticamente el cliente Twenty tipado en `node_modules/twenty-sdk/generated`. Úsalo en tus funciones:
```typescript
import Twenty from '~/generated';
@@ -594,7 +591,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
El cliente se vuelve a generar con `yarn app:generate`. Vuelve a ejecutarlo después de cambiar tus objetos o al incorporarte a un nuevo espacio de trabajo.
El cliente se regenera automáticamente durante la ejecución de `app:dev`. Reinicia `app:dev` después de cambiar tus objetos o al incorporarte a un nuevo espacio de trabajo.
#### Credenciales en tiempo de ejecución en funciones de lógica
@@ -632,7 +629,6 @@ Luego agrega scripts como estos:
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
@@ -642,13 +638,13 @@ Luego agrega scripts como estos:
}
```
Ahora puedes ejecutar los mismos comandos mediante Yarn, p. ej., `yarn app:dev`, `yarn app:generate`, etc.
Ahora puedes ejecutar los mismos comandos mediante Yarn, p. ej., `yarn app:dev`, etc.
## Solución de problemas
* Errores de autenticación: ejecuta `yarn auth:login` y asegúrate de que tu clave de API tenga los permisos necesarios.
* No se puede conectar al servidor: verifica la URL de la API y que el servidor de Twenty sea accesible.
* Tipos o cliente faltantes/obsoletos: ejecuta `yarn app:generate`.
* Tipos o cliente faltantes/obsoletos: reinicia `yarn app:dev`.
* El modo de desarrollo no sincroniza: asegúrate de que `yarn app:dev` esté ejecutándose y de que los cambios no sean ignorados por tu entorno.
Canal de ayuda en Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -52,9 +52,6 @@ yarn app:dev
# Ajouter une nouvelle entité à votre application (assisté)
yarn entity:add
# Générer un client Twenty typé et les types d'entité de l'espace de travail
yarn app:generate
# Surveiller les journaux des fonctions de votre application
yarn function:logs
@@ -157,7 +154,7 @@ src/
Dans les grandes lignes :
* **package.json** : Déclare le nom de lapplication, la version, les moteurs (Node 24+, Yarn 4), et ajoute `twenty-sdk` ainsi que des scripts comme `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` et `auth:login` qui délèguent à la CLI locale `twenty`.
* **package.json** : Déclare le nom de lapplication, la version, les moteurs (Node 24+, Yarn 4), et ajoute `twenty-sdk` ainsi que des scripts comme `app:dev`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` et `auth:login` qui délèguent à la CLI locale `twenty`.
* **.gitignore** : Ignore les artefacts courants tels que `node_modules`, `.yarn`, `generated/` (client typé), `dist/`, `build/`, les dossiers de couverture, les fichiers journaux et les fichiers `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/** : Verrouillent et configurent la chaîne doutils Yarn 4 utilisée par le projet.
* **.nvmrc** : Fige la version de Node.js attendue par le projet.
@@ -173,7 +170,7 @@ Dans les grandes lignes :
Des commandes ultérieures ajouteront dautres fichiers et dossiers :
* `yarn app:generate` créera un dossier `generated/` (client Twenty typé + types de lespace de travail).
* `yarn app:dev` génère automatiquement le client Twenty typé dans `node_modules/twenty-sdk/generated`.
* `yarn entity:add` ajoutera des fichiers de définition dentité sous `src/` pour vos objets, fonctions, composants front-end ou rôles personnalisés.
## Authentification
@@ -585,7 +582,7 @@ Vous pouvez créer de nouvelles fonctions de deux façons :
### Client typé généré
Exécutez yarn app:generate pour créer un client typé local dans generated/ basé sur le schéma de votre espace de travail. Utilisez-le dans vos fonctions :
`yarn app:dev` génère automatiquement le client Twenty typé dans `node_modules/twenty-sdk/generated`. Utilisez-le dans vos fonctions :
```typescript
import Twenty from '~/generated';
@@ -594,7 +591,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
Le client est régénéré par `yarn app:generate`. Relancez après avoir modifié vos objets ou lors de lintégration à un nouvel espace de travail.
Le client est régénéré automatiquement pendant l'exécution de `app:dev`. Redémarrez `app:dev` après avoir modifié vos objets ou lors de lintégration à un nouvel espace de travail.
#### Identifiants dexécution dans les fonctions logiques
@@ -632,7 +629,6 @@ Ajoutez ensuite des scripts comme ceux-ci :
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
@@ -642,13 +638,13 @@ Ajoutez ensuite des scripts comme ceux-ci :
}
```
Vous pouvez désormais exécuter les mêmes commandes via Yarn, par exemple `yarn app:dev`, `yarn app:generate`, etc.
Vous pouvez désormais exécuter les mêmes commandes via Yarn, par exemple `yarn app:dev`, etc.
## Résolution des problèmes
* Erreurs dauthentification : exécutez `yarn auth:login` et assurez-vous que votre clé API dispose des autorisations requises.
* Impossible de se connecter au serveur : vérifiez lURL de lAPI et que le serveur Twenty est accessible.
* Types ou client manquants/obsolètes : exécutez `yarn app:generate`.
* Types ou client manquants/obsolètes : redémarrez `yarn app:dev`.
* Le mode dev ne se synchronise pas : assurez-vous que `yarn app:dev` est en cours dexécution et que les modifications ne sont pas ignorées par votre environnement.
Canal daide Discord : https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -17,10 +17,6 @@ Le app ti consentono di creare e gestire le personalizzazioni di Twenty **come c
* Crea funzioni logiche con trigger personalizzati
* Distribuisci la stessa app su più spazi di lavoro
**In arrivo:**
* Layout e componenti UI personalizzati
## Prerequisiti
* Node.js 24+ e Yarn 4
@@ -31,7 +27,7 @@ Le app ti consentono di creare e gestire le personalizzazioni di Twenty **come c
Crea una nuova app utilizzando lo scaffolder ufficiale, quindi autenticati e inizia a sviluppare:
```bash filename="Terminal"
# Crea lo scaffold di una nuova app
# Crea lo scaffold di una nuova app (include tutti gli esempi per impostazione predefinita)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
@@ -40,32 +36,45 @@ corepack enable
yarn install
# Autenticati usando la tua API key (ti verrà richiesto)
yarn auth:login
yarn twenty auth:login
# Avvia la modalità di sviluppo: sincronizza automaticamente le modifiche locali con il tuo workspace
yarn app:dev
yarn twenty app:dev
```
Lo strumento di scaffolding supporta tre modalità per controllare quali file di esempio vengono inclusi:
```bash filename="Terminal"
# Predefinita (esaustiva): tutti gli esempi (oggetto, campo, funzione logica, componente front-end, vista, voce del menu di navigazione)
npx create-twenty-app@latest my-app
# Minimale: solo i file principali (application-config.ts e default-role.ts)
npx create-twenty-app@latest my-app --minimal
# Interattiva: seleziona quali esempi includere
npx create-twenty-app@latest my-app --interactive
```
Da qui puoi:
```bash filename="Terminal"
# Aggiungi una nuova entità alla tua applicazione (guidata)
yarn entity:add
# Genera un client Twenty tipizzato e i tipi di entità dell'area di lavoro
yarn app:generate
yarn twenty entity:add
# Monitora i log delle funzioni della tua applicazione
yarn function:logs
yarn twenty function:logs
# Esegui una funzione per nome
yarn function:execute -n my-function -p '{"name": "test"}'
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Esegui la funzione post-installazione
yarn twenty function:execute --postInstall
# Disinstalla l'applicazione dallo spazio di lavoro corrente
yarn app:uninstall
yarn twenty app:uninstall
# Mostra l'aiuto dei comandi
yarn help
yarn twenty help
```
Vedi anche: le pagine di riferimento della CLI per [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) e [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@@ -77,9 +86,9 @@ Quando esegui `npx create-twenty-app@latest my-twenty-app`, lo scaffolder:
* Copia un'applicazione base minimale in `my-twenty-app/`
* Aggiunge una dipendenza locale `twenty-sdk` e la configurazione di Yarn 4
* Crea file di configurazione e script collegati alla CLI `twenty`
* Genera una configurazione applicativa predefinita e un ruolo funzione predefinito
* Genera i file principali (configurazione dell'applicazione, ruolo predefinito per le funzioni logiche, funzione di post-installazione) più i file di esempio in base alla modalità di scaffolding
Un'app appena generata dallo scaffolder si presenta così:
Un'app appena creata con la modalità predefinita `--exhaustive` si presenta così:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -95,90 +104,76 @@ my-twenty-app/
README.md
public/ # Cartella delle risorse pubbliche (immagini, font, ecc.)
src/
application.config.ts # Obbligatorio - configurazione principale dell'applicazione
default-function.role.ts # Ruolo predefinito per le funzioni serverless
hello-world.function.ts # Funzione serverless di esempio
hello-world.front-component.tsx # Componente front-end di esempio
// le tue entità (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
├── application-config.ts # Obbligatorio - configurazione principale dell'applicazione
├── roles/
└── default-role.ts # Ruolo predefinito per le funzioni logiche
├── objects/
│ └── example-object.ts # Definizione di oggetto personalizzato di esempio
├── fields/
│ └── example-field.ts # Definizione di campo autonomo di esempio
├── logic-functions/
│ ├── hello-world.ts # Funzione logica di esempio
│ └── post-install.ts # Funzione logica post-installazione
├── front-components/
│ └── hello-world.tsx # Componente front-end di esempio
├── views/
│ └── example-view.ts # Definizione di vista salvata di esempio
└── navigation-menu-items/
└── example-navigation-menu-item.ts # Link di navigazione della barra laterale di esempio
```
### Convenzioni invece della configurazione
Le applicazioni usano un approccio basato sulle **convenzioni invece della configurazione** in cui le entità vengono rilevate in base al suffisso del file. Questo consente un'organizzazione flessibile all'interno della cartella `src/app/`:
| Suffisso del file | Tipo di entità |
| ----------------------- | ------------------------------------- |
| `*.object.ts` | Definizioni di oggetti personalizzati |
| `*.function.ts` | Definizioni di funzioni serverless |
| `*.front-component.tsx` | Definizioni dei componenti front-end |
| `*.role.ts` | Definizioni di ruoli |
### Organizzazioni di cartelle supportate
Puoi organizzare le tue entità in uno qualsiasi di questi modelli:
**Tradizionale (per tipo):**
```text
src/
├── application.config.ts
├── objects/
│ └── postCard.object.ts
├── functions/
│ └── createPostCard.function.ts
├── components/
│ └── card.front-component.tsx
└── roles/
└── admin.role.ts
```
**Per funzionalità:**
```text
src/
├── application.config.ts
└── post-card/
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── postCardAdmin.role.ts
```
**Struttura piatta:**
```text
src/
├── application.config.ts
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── admin.role.ts
```
Con `--minimal`, vengono creati solo i file principali (`application-config.ts`, `roles/default-role.ts` e `logic-functions/post-install.ts`). Con `--interactive`, scegli quali file di esempio includere.
A livello generale:
* **package.json**: Dichiara il nome dell'app, la versione, i motori (Node 24+, Yarn 4) e aggiunge `twenty-sdk`, oltre a script come `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` e `auth:login` che delegano alla CLI locale `twenty`.
* **package.json**: Dichiara il nome dell'app, la versione, i motori (Node 24+, Yarn 4) e aggiunge `twenty-sdk` più uno script `twenty` che delega alla CLI locale `twenty`. Esegui `yarn twenty help` per elencare tutti i comandi disponibili.
* **.gitignore**: Ignora i file generati comuni come `node_modules`, `.yarn`, `generated/` (client tipizzato), `dist/`, `build/`, cartelle di coverage, file di log e file `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Bloccano e configurano la toolchain Yarn 4 utilizzata dal progetto.
* **.nvmrc**: Fissa la versione di Node.js prevista dal progetto.
* **eslint.config.mjs** e **tsconfig.json**: Forniscono linting e configurazione TypeScript per i sorgenti TypeScript della tua app.
* **README.md**: Un breve README nella radice dell'app con istruzioni di base.
* **public/**: Una cartella per archiviare risorse pubbliche (immagini, font, file statici) che saranno servite con la tua applicazione. I file collocati qui vengono caricati durante la sincronizzazione e sono accessibili in fase di esecuzione.
* **src/**: Il luogo principale in cui definisci la tua applicazione come codice:
* `application.config.ts`: Configurazione globale della tua app (metadati e collegamenti di runtime). Vedi "Configurazione dell'applicazione" qui sotto.
* `*.role.ts`: Definizioni di ruoli usate dalle tue funzioni logiche. Vedi "Ruolo funzione predefinito" qui sotto.
* `*.object.ts`: Definizioni di oggetti personalizzati.
* `*.function.ts`: Definizioni di funzioni logiche.
* `*.front-component.tsx`: Definizioni di componenti front-end.
* **src/**: Il luogo principale in cui definisci la tua applicazione come codice
### Rilevamento delle entità
L'SDK rileva le entità analizzando i tuoi file TypeScript alla ricerca di chiamate **`export default define<Entity>({...})`**. Ogni tipo di entità ha una corrispondente funzione helper esportata da `twenty-sdk`:
| Funzione helper | Tipo di entità |
| ---------------------------- | ---------------------------------------------- |
| `defineObject()` | Definizioni di oggetti personalizzati |
| `defineLogicFunction()` | Definizioni di funzioni logiche |
| `defineFrontComponent()` | Definizioni dei componenti front-end |
| `defineRole()` | Definizioni di ruoli |
| `defineField()` | Estensioni di campo per oggetti esistenti |
| `defineView()` | Definizioni di viste salvate |
| `defineNavigationMenuItem()` | Definizioni delle voci del menu di navigazione |
<Note>
**La denominazione dei file è flessibile.** Il rilevamento delle entità è basato sull'AST — l'SDK esegue la scansione dei file sorgente alla ricerca del pattern `export default define<Entity>({...})`. Puoi organizzare file e cartelle come preferisci. Raggruppare per tipo di entità (ad es., `logic-functions/`, `roles/`) è solo una convenzione per l'organizzazione del codice, non un requisito.
</Note>
Esempio di entità rilevata:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
Comandi successivi aggiungeranno altri file e cartelle:
* `yarn app:generate` creerà una cartella `generated/` (client Twenty tipizzato + tipi dello spazio di lavoro).
* `yarn entity:add` aggiungerà file di definizione delle entità sotto `src/` per i tuoi oggetti, funzioni, componenti front-end o ruoli personalizzati.
* `yarn twenty app:dev` genererà automaticamente un client API tipizzato in `node_modules/twenty-sdk/generated` (client Twenty tipizzato + tipi dell'area di lavoro).
* `yarn twenty entity:add` aggiungerà file di definizione delle entità sotto `src/` per i tuoi oggetti, funzioni, componenti front-end o ruoli personalizzati.
## Autenticazione
La prima volta che esegui `yarn auth:login`, ti verranno richiesti:
La prima volta che esegui `yarn twenty auth:login`, ti verranno richiesti:
* URL dell'API (predefinito a http://localhost:3000 o al profilo dello spazio di lavoro corrente)
* Chiave API
@@ -189,25 +184,25 @@ Le tue credenziali sono archiviate per utente in `~/.twenty/config.json`. Puoi m
```bash filename="Terminal"
# Login interactively (recommended)
yarn auth:login
yarn twenty auth:login
# Login to a specific workspace profile
yarn auth:login --workspace my-custom-workspace
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn auth:list
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn auth:switch
yarn twenty auth:switch
# Switch to a specific workspace
yarn auth:switch production
yarn twenty auth:switch production
# Check current authentication status
yarn auth:status
yarn twenty auth:status
```
Una volta che hai cambiato area di lavoro con `auth:switch`, tutti i comandi successivi utilizzeranno quell'area di lavoro per impostazione predefinita. Puoi comunque sovrascriverla temporaneamente con `--workspace <name>`.
Una volta che hai cambiato area di lavoro con `yarn twenty auth:switch`, tutti i comandi successivi utilizzeranno quell'area di lavoro per impostazione predefinita. Puoi comunque sovrascriverla temporaneamente con `--workspace <name>`.
## Usa le risorse dell'SDK (tipi e configurazione)
@@ -215,16 +210,20 @@ Il pacchetto twenty-sdk fornisce blocchi tipizzati e funzioni helper da usare ne
### Funzioni helper
L'SDK fornisce quattro funzioni helper con convalida integrata per definire le entità della tua app:
L'SDK fornisce funzioni helper per definire le entità della tua app. Come descritto in [Rilevamento delle entità](#entity-detection), devi usare `export default define<Entity>({...})` affinché le tue entità vengano rilevate:
| Funzione | Scopo |
| --------------------- | ------------------------------------------------------- |
| `defineApplication()` | Configura i metadati dell'applicazione |
| `defineObject()` | Definisci oggetti personalizzati con campi |
| `defineFunction()` | Definisci funzioni logiche con handler |
| `defineRole()` | Configura i permessi dei ruoli e l'accesso agli oggetti |
| Funzione | Scopo |
| ---------------------------- | ----------------------------------------------------------------------- |
| `defineApplication()` | Configura i metadati dell'applicazione (obbligatorio, uno per app) |
| `defineObject()` | Definisci oggetti personalizzati con campi |
| `defineLogicFunction()` | Definisci funzioni logiche con handler |
| `defineFrontComponent()` | Definisci componenti front-end per un'interfaccia utente personalizzata |
| `defineRole()` | Configura i permessi dei ruoli e l'accesso agli oggetti |
| `defineField()` | Estendi gli oggetti esistenti con campi aggiuntivi |
| `defineView()` | Definisce viste salvate per gli oggetti |
| `defineNavigationMenuItem()` | Definisce i link di navigazione della barra laterale |
Queste funzioni convalidano la configurazione a runtime e offrono un migliore completamento automatico nell'IDE e una maggiore sicurezza dei tipi.
Queste funzioni convalidano la configurazione in fase di build e offrono il completamento automatico nell'IDE e la sicurezza dei tipi.
### Definizione degli oggetti
@@ -305,26 +304,32 @@ Punti chiave:
* Il `universalIdentifier` deve essere univoco e stabile tra i deployment.
* Ogni campo richiede un `name`, `type`, `label` e il proprio `universalIdentifier` stabile.
* L'array `fields` è facoltativo: puoi definire oggetti senza campi personalizzati.
* Puoi generare nuovi oggetti con `yarn entity:add`, che ti guida nella denominazione, nei campi e nelle relazioni.
* Puoi generare nuovi oggetti con `yarn twenty entity:add`, che ti guida nella denominazione, nei campi e nelle relazioni.
<Note>
**I campi base vengono creati automaticamente.** Quando definisci un oggetto personalizzato, Twenty aggiunge automaticamente i campi standard come `name`, `createdAt`, `updatedAt`, `createdBy`, `position` e `deletedAt`. Non è necessario definirli nel tuo array `fields` — aggiungi solo i tuoi campi personalizzati.
**I campi base vengono creati automaticamente.** Quando definisci un oggetto personalizzato, Twenty aggiunge automaticamente i campi standard
come `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` e `deletedAt`.
Non è necessario definirli nel tuo array `fields` — aggiungi solo i tuoi campi personalizzati.
Puoi sovrascrivere i campi predefiniti definendo un campo con lo stesso nome nel tuo array `fields`,
ma non è consigliato.
</Note>
### Configurazione dell'applicazione (application.config.ts)
### Configurazione dell'applicazione (application-config.ts)
Ogni app ha un singolo file `application.config.ts` che descrive:
Ogni app ha un singolo file `application-config.ts` che descrive:
* **Identità dell'app**: identificatori, nome visualizzato e descrizione.
* **Come vengono eseguite le sue funzioni**: quale ruolo usano per i permessi.
* **Variabili (opzionali)**: coppie chiavevalore esposte alle funzioni come variabili d'ambiente.
* **(Opzionale) funzione post-installazione**: una funzione logica che viene eseguita dopo l'installazione dell'app.
Usa `defineApplication()` per definire la configurazione della tua applicazione:
```typescript
// src/app/application.config.ts
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -339,7 +344,8 @@ export default defineApplication({
isSecret: false,
},
},
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -347,11 +353,12 @@ Note:
* I campi `universalIdentifier` sono ID deterministici sotto il tuo controllo; generali una volta e mantienili stabili tra le sincronizzazioni.
* `applicationVariables` diventano variabili d'ambiente per le tue funzioni (ad esempio, `DEFAULT_RECIPIENT_NAME` è disponibile come `process.env.DEFAULT_RECIPIENT_NAME`).
* `roleUniversalIdentifier` deve corrispondere al ruolo che definisci nel tuo file `*.role.ts` (vedi sotto).
* `defaultRoleUniversalIdentifier` deve corrispondere al file del ruolo (vedi sotto).
* `postInstallLogicFunctionUniversalIdentifier` (opzionale) fa riferimento a una funzione logica che viene eseguita automaticamente dopo l'installazione dell'app. Vedi [Funzioni post-installazione](#post-install-functions).
#### Ruoli e permessi
Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro. Il campo `roleUniversalIdentifier` in `application.config.ts` designa il ruolo predefinito utilizzato dalle funzioni logiche della tua app.
Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro. Il campo `defaultRoleUniversalIdentifier` in `application-config.ts` indica il ruolo predefinito utilizzato dalle funzioni logiche della tua app.
* La chiave API di runtime iniettata come `TWENTY_API_KEY` è derivata da questo ruolo funzione predefinito.
* Il client tipizzato sarà limitato ai permessi concessi a quel ruolo.
@@ -362,7 +369,7 @@ Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti
Quando generi una nuova app con lo scaffolder, la CLI crea anche un file di ruolo predefinito. Usa `defineRole()` per definire ruoli con convalida integrata:
```typescript
// src/app/default-function.role.ts
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
@@ -401,10 +408,10 @@ export default defineRole({
});
```
L'`universalIdentifier` di questo ruolo viene quindi referenziato in `application.config.ts` come `roleUniversalIdentifier`. In altre parole:
L'`universalIdentifier` di questo ruolo viene quindi referenziato in `application-config.ts` come `defaultRoleUniversalIdentifier`. In altre parole:
* **\*.role.ts** definisce ciò che il ruolo funzione predefinito può fare.
* **application.config.ts** punta a quel ruolo in modo che le tue funzioni ne ereditino i permessi.
* **application-config.ts** punta a quel ruolo in modo che le tue funzioni ne ereditino i permessi.
Note:
@@ -415,11 +422,11 @@ Note:
### Configurazione e punto di ingresso della funzione logica
Ogni file di funzione usa `defineFunction()` per esportare una configurazione con un handler e trigger opzionali. Usa il suffisso di file `*.function.ts` per il rilevamento automatico.
Ogni file di funzione usa `defineLogicFunction()` per esportare una configurazione con un handler e trigger opzionali.
```typescript
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
@@ -439,7 +446,7 @@ const handler = async (params: RoutePayload) => {
return result;
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
@@ -486,6 +493,55 @@ Note:
* L'array `triggers` è facoltativo. Le funzioni senza trigger possono essere utilizzate come funzioni di utilità richiamate da altre funzioni.
* Puoi combinare più tipi di trigger in un'unica funzione.
### Funzioni post-installazione
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.
Quando esegui lo scaffolding di una nuova app con `create-twenty-app`, viene generata automaticamente una funzione di post-installazione in `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
La funzione viene collegata alla tua app facendo riferimento al suo identificatore universale in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Puoi anche eseguire manualmente la funzione di post-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Punti chiave:
* Le funzioni di post-installazione sono funzioni logiche standard — usano `defineLogicFunction()` come qualsiasi altra funzione.
* Il campo `postInstallLogicFunctionUniversalIdentifier` in `defineApplication()` è facoltativo. Se omesso, nessuna funzione viene eseguita dopo l'installazione.
* Il timeout predefinito è impostato a 300 secondi (5 minuti) per consentire attività di configurazione più lunghe, come il popolamento dei dati.
* Le funzioni di post-installazione non necessitano di trigger — vengono invocate dalla piattaforma durante l'installazione o manualmente tramite `function:execute --postInstall`.
### Payload del trigger di route
<Warning>
@@ -515,7 +571,7 @@ Note:
Quando un trigger di route invoca la tua funzione logica, questa riceve un oggetto `RoutePayload` che segue il formato AWS HTTP API v2. Importa il tipo da `twenty-sdk`:
```typescript
import { defineFunction, type RoutePayload } from 'twenty-sdk';
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
@@ -545,7 +601,7 @@ Il tipo `RoutePayload` ha la seguente struttura:
Per impostazione predefinita, le intestazioni HTTP delle richieste in ingresso **non** vengono passate alla tua funzione logica per motivi di sicurezza. Per accedere a intestazioni specifiche, elencale esplicitamente nell'array `forwardedRequestHeaders`:
```typescript
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
@@ -580,12 +636,114 @@ const handler = async (event: RoutePayload) => {
Puoi creare nuove funzioni in due modi:
* **Generata dallo scaffolder**: Esegui `yarn entity:add` e scegli l'opzione per aggiungere una nuova funzione. Questo genera un file iniziale con un handler e una configurazione.
* **Manuale**: Crea un nuovo file `*.function.ts` e usa `defineFunction()`, seguendo lo stesso schema.
* **Generata dallo scaffolder**: Esegui `yarn twenty entity:add` e scegli l'opzione per aggiungere una nuova funzione logica. Questo genera un file iniziale con un handler e una configurazione.
* **Manuale**: Crea un nuovo file `*.logic-function.ts` e usa `defineLogicFunction()`, seguendo lo stesso schema.
### Contrassegnare una funzione logica come strumento
Le funzioni logiche possono essere esposte come **strumenti** per gli agenti di IA e i flussi di lavoro. Quando una funzione è contrassegnata come strumento, diventa individuabile dalle funzionalità di IA di Twenty e può essere selezionata come passaggio nelle automazioni dei flussi di lavoro.
Per contrassegnare una funzione logica come strumento, imposta `isTool: true` e fornisci un `toolInputSchema` che descriva i parametri di input attesi utilizzando [JSON Schema](https://json-schema.org/):
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
Punti chiave:
* **`isTool`** (`boolean`, predefinito: `false`): Quando impostato su `true`, la funzione viene registrata come strumento e diventa disponibile per gli agenti IA e le automazioni dei flussi di lavoro.
* **`toolInputSchema`** (`object`, opzionale): Un oggetto JSON Schema che descrive i parametri accettati dalla funzione. Gli agenti IA utilizzano questo schema per capire quali input si aspetta lo strumento e per convalidare le chiamate. Se omesso, lo schema assume il valore predefinito `{ type: 'object', properties: {} }` (nessun parametro).
* Le funzioni con `isTool: false` (o non impostato) **non** vengono esposte come strumenti. Possono comunque essere eseguite direttamente o chiamate da altre funzioni, ma non compariranno nell'individuazione degli strumenti.
* **Denominazione dello strumento**: Quando esposta come strumento, il nome della funzione viene normalizzato automaticamente in `logic_function_<name>` (in minuscolo, i caratteri non alfanumerici vengono sostituiti da trattini bassi). Ad esempio, `enrich-company` diventa `logic_function_enrich_company`.
* È possibile combinare `isTool` con i trigger — una funzione può essere sia uno strumento (invocabile dagli agenti IA) sia attivata da eventi (cron, eventi del database, routes) contemporaneamente.
<Note>
**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>
### Componenti front-end
I componenti front-end ti consentono di creare componenti React personalizzati che vengono renderizzati all'interno dell'interfaccia di Twenty. Usa `defineFrontComponent()` per definire componenti con convalida integrata:
```typescript
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
Punti chiave:
* I componenti front-end sono componenti React che eseguono il rendering in contesti isolati all'interno di Twenty.
* Usa il suffisso di file `*.front-component.tsx` per il rilevamento automatico.
* Il campo `component` fa riferimento al tuo componente React.
* I componenti vengono compilati e sincronizzati automaticamente durante `yarn twenty app:dev`.
Puoi creare nuovi componenti front-end in due modi:
* **Generata dallo scaffolder**: Esegui `yarn twenty entity:add` e scegli l'opzione per aggiungere un nuovo componente front-end.
* **Manuale**: Crea un nuovo file `*.front-component.tsx` e usa `defineFrontComponent()`.
### Client tipizzato generato
Esegui yarn app:generate per creare un client tipizzato locale in generated/ basato sullo schema del tuo spazio di lavoro. Usalo nelle tue funzioni:
Il client tipizzato è generato automaticamente da `yarn twenty app:dev` e salvato in `node_modules/twenty-sdk/generated` in base allo schema della tua area di lavoro. Usalo nelle tue funzioni:
```typescript
import Twenty from '~/generated';
@@ -594,7 +752,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
Il client viene rigenerato da `yarn app:generate`. Eseguilo nuovamente dopo aver modificato i tuoi oggetti oppure quando effettui l'onboarding su un nuovo spazio di lavoro.
Il client viene rigenerato automaticamente da `yarn twenty app:dev` ogni volta che i tuoi oggetti o campi cambiano.
#### Credenziali di runtime nelle funzioni logiche
@@ -606,49 +764,38 @@ Quando la tua funzione viene eseguita su Twenty, la piattaforma inietta le crede
Note:
* Non è necessario passare URL o chiave API al client generato. Legge `TWENTY_API_URL` e `TWENTY_API_KEY` da process.env in fase di esecuzione.
* I permessi della chiave API sono determinati dal ruolo referenziato in `application.config.ts` tramite `roleUniversalIdentifier`. Questo è il ruolo predefinito utilizzato dalle funzioni logiche della tua applicazione.
* Le applicazioni possono definire ruoli per seguire il principio del privilegio minimo. Concedi solo i permessi necessari alle tue funzioni, quindi punta `roleUniversalIdentifier` all'identificatore universale di quel ruolo.
* I permessi della chiave API sono determinati dal ruolo referenziato nel tuo `application-config.ts` tramite `defaultRoleUniversalIdentifier`. Questo è il ruolo predefinito utilizzato dalle funzioni logiche della tua applicazione.
* Le applicazioni possono definire ruoli per seguire il principio del privilegio minimo. Concedi solo i permessi necessari alle tue funzioni, quindi punta `defaultRoleUniversalIdentifier` all'identificatore universale di quel ruolo.
### Esempio Hello World
Esplora un esempio minimale endtoend che dimostra oggetti, funzioni e trigger multipli [qui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
Esplora un esempio minimale end-to-end che dimostra oggetti, funzioni logiche, componenti front-end e trigger multipli [qui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Configurazione manuale (senza lo scaffolder)
Sebbene consigliamo di utilizzare `create-twenty-app` per la migliore esperienza iniziale, puoi anche configurare un progetto manualmente. Non installare la CLI globalmente. Invece, aggiungi `twenty-sdk` come dipendenza locale e collega gli script nel tuo package.json:
Sebbene consigliamo di utilizzare `create-twenty-app` per la migliore esperienza iniziale, puoi anche configurare un progetto manualmente. Non installare la CLI globalmente. Invece, aggiungi `twenty-sdk` come dipendenza locale e collega un unico script nel tuo package.json:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
Quindi aggiungi script come questi:
Quindi aggiungi uno script `twenty`:
```json filename="package.json"
{
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
"twenty": "twenty"
}
}
```
Ora puoi eseguire gli stessi comandi tramite Yarn, ad esempio `yarn app:dev`, `yarn app:generate`, ecc.
Ora puoi eseguire tutti i comandi tramite `yarn twenty <command>`, ad es. `yarn twenty app:dev`, `yarn twenty help`, ecc.
## Risoluzione dei problemi
* Errori di autenticazione: esegui `yarn auth:login` e assicurati che la tua chiave API abbia i permessi richiesti.
* Errori di autenticazione: esegui `yarn twenty auth:login` e assicurati che la tua chiave API abbia i permessi richiesti.
* Impossibile connettersi al server: verifica l'URL dell'API e che il server Twenty sia raggiungibile.
* Tipi o client mancanti/obsoleti: esegui `yarn app:generate`.
* Modalità di sviluppo non in sincronizzazione: assicurati che `yarn app:dev` sia in esecuzione e che le modifiche non vengano ignorate dal tuo ambiente.
* Types or client missing/outdated: restart `yarn twenty app:dev` — it auto-generates the typed client.
* Modalità di sviluppo non sincronizzata: assicurati che `yarn twenty app:dev` sia in esecuzione e che le modifiche non vengano ignorate dal tuo ambiente.
Canale di supporto su Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -52,9 +52,6 @@ yarn app:dev
# アプリケーションに新しいエンティティを追加(ガイド付き)
yarn entity:add
# 型付きの Twenty クライアントとワークスペースのエンティティ型を生成
yarn app:generate
# アプリケーションの関数のログを監視
yarn function:logs
@@ -156,7 +153,7 @@ src/
概要:
* **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and `auth:login` that delegate to the local `twenty` CLI.
* **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and `auth:login` that delegate to the local `twenty` CLI.
* **.gitignore**: `node_modules`、`.yarn`、`generated/`(型付きクライアント)、`dist/`、`build/`、カバレッジ用フォルダー、ログファイル、`.env*` ファイルなどの一般的な生成物を無視します。
* **yarn.lock**、**.yarnrc.yml**、**.yarn/**: プロジェクトで使用する Yarn 4 ツールチェーンをロックおよび構成します。
* **.nvmrc**: プロジェクトで想定する Node.js バージョンを固定します。
@@ -171,7 +168,7 @@ src/
後続のコマンドにより、さらにファイルやフォルダーが追加されます:
* `yarn app:generate` は `generated/` フォルダー(型付き Twenty クライアント + ワークスペースの型)を作成します。
* `yarn app:dev` は `node_modules/twenty-sdk/generated` 型付き Twenty クライアントを自動生成します。
* `yarn entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
## 認証
@@ -583,7 +580,7 @@ const handler = async (event: RoutePayload) => {
### 生成された型付きクライアント
ワークスペースのスキーマに基づき、generated/ローカルの型付きクライアントを作成するには yarn app:generate を実行します。 関数内で使用します:
`yarn app:dev` は `node_modules/twenty-sdk/generated`型付き Twenty クライアントを自動生成します。 関数内で使用します:
```typescript
import Twenty from '~/generated';
@@ -592,7 +589,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
このクライアントは `yarn app:generate` によって再生成されます。 Re-run after changing your objects or when onboarding to a new workspace.
このクライアントは `app:dev` 実行中に自動的に再生成されます。 オブジェクトを変更した後、または新しいワークスペースにオンボーディングする際は、`app:dev` を再起動してください。
#### Runtime credentials in logic functions
@@ -630,7 +627,6 @@ yarn add -D twenty-sdk
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
@@ -640,13 +636,13 @@ yarn add -D twenty-sdk
}
```
Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:generate`, etc.
Now you can run the same commands via Yarn, e.g. `yarn app:dev`, etc.
## トラブルシューティング
* 認証エラー: `yarn auth:login` を実行し、API キーに必要な権限があることを確認してください。
* サーバーに接続できません: API URL と、Twenty サーバーに到達可能であることを確認してください。
* Types or client missing/outdated: run `yarn app:generate`.
* Types or client missing/outdated: restart `yarn app:dev`.
* 開発モードで同期されない: `yarn app:dev` が実行中であり、環境によって変更が無視されていないことを確認してください。
Discord ヘルプチャンネル: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -52,9 +52,6 @@ yarn app:dev
# Add a new entity to your application (guided)
yarn entity:add
# Generate a typed Twenty client and workspace entity types
yarn app:generate
# Watch your application's function logs
yarn function:logs
@@ -157,7 +154,7 @@ src/
개요:
* **package.json**: 앱 이름, 버전, 엔진(Node 24+, Yarn 4)을 선언하고, `twenty-sdk`와 함께 `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, `auth:login` 같은 스크립트를 추가합니다. 이 스크립트들은 로컬 `twenty` CLI에 위임됩니다.
* **package.json**: 앱 이름, 버전, 엔진(Node 24+, Yarn 4)을 선언하고, `twenty-sdk`와 함께 `app:dev`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, `auth:login` 같은 스크립트를 추가합니다. 이 스크립트들은 로컬 `twenty` CLI에 위임됩니다.
* **.gitignore**: `node_modules`, `.yarn`, `generated/`(타입드 클라이언트), `dist/`, `build/`, 커버리지 폴더, 로그 파일, `.env*` 파일 등의 일반 산출물을 무시합니다.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: 프로젝트에서 사용하는 Yarn 4 툴체인을 고정하고 구성합니다.
* **.nvmrc**: 프로젝트에서 예상하는 Node.js 버전을 고정합니다.
@@ -173,7 +170,7 @@ src/
이후 명령을 실행하면 더 많은 파일과 폴더가 추가됩니다:
* `yarn app:generate`는 `generated/` 폴더를 생성합니다(타입드 Twenty 클라이언트 + 워크스페이스 타입).
* `yarn app:dev`는 `node_modules/twenty-sdk/generated`에 타입드 Twenty 클라이언트를 자동으로 생성합니다.
* `yarn entity:add`는 사용자 정의 객체, 함수, 프런트 컴포넌트 또는 역할에 대한 엔티티 정의 파일을 `src/` 아래에 추가합니다.
## 인증
@@ -585,7 +582,7 @@ const handler = async (event: RoutePayload) => {
### 생성된 타입드 클라이언트
워크스페이스 스키마를 기반으로 generated/ 로컬 타입드 클라이언트를 생성하려면 yarn app:generate를 실행하세요. 함수에서 사용하세요:
`yarn app:dev`는 `node_modules/twenty-sdk/generated`에 타입드 Twenty 클라이언트를 자동으로 생성합니다. 함수에서 사용하세요:
```typescript
import Twenty from '~/generated';
@@ -594,7 +591,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
클라이언트는 `yarn app:generate`로 다시 생성됩니다. 객체를 변경한 후 또는 새 워크스페이스에 온보딩할 때 다시 실행하세요.
클라이언트는 `app:dev` 실행 중 자동으로 다시 생성됩니다. 객체를 변경한 후 또는 새 워크스페이스에 온보딩할 때 `app:dev`를 다시 시작하세요.
#### 로직 함수의 런타임 자격 증명
@@ -632,7 +629,6 @@ yarn add -D twenty-sdk
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
@@ -642,13 +638,13 @@ yarn add -D twenty-sdk
}
```
이제 Yarn을 통해 동일한 명령을 실행할 수 있습니다. 예: `yarn app:dev`, `yarn app:generate` 등.
이제 Yarn을 통해 동일한 명령을 실행할 수 있습니다. 예: `yarn app:dev` 등.
## 문제 해결
* 인증 오류: `yarn auth:login`를 실행하고 API 키에 필요한 권한이 있는지 확인하세요.
* 서버에 연결할 수 없음: API URL과 Twenty 서버에 접근 가능한지 확인하세요.
* 타입 또는 클라이언트가 없거나 오래된 경우: `yarn app:generate`를 실행하세요.
* 타입 또는 클라이언트가 없거나 오래된 경우: `yarn app:dev`를 다시 시작하세요.
* 개발 모드가 동기화되지 않음: `yarn app:dev`가 실행 중인지, 환경에서 변경 사항을 무시하지 않는지 확인하세요.
Discord 도움말 채널: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -17,10 +17,6 @@ Os aplicativos permitem criar e gerenciar personalizações do Twenty **como có
* Crie funções de lógica com gatilhos personalizados
* Implemente o mesmo aplicativo em vários espaços de trabalho
**Em breve:**
* Layouts e componentes de UI personalizados
## Pré-requisitos
* Node.js 24+ e Yarn 4
@@ -31,7 +27,7 @@ Os aplicativos permitem criar e gerenciar personalizações do Twenty **como có
Crie um novo aplicativo usando o gerador oficial, depois autentique-se e comece a desenvolver:
```bash filename="Terminal"
# Criar a estrutura de um novo app
# Criar a estrutura de um novo app (inclui todos os exemplos por padrão)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
@@ -40,32 +36,45 @@ corepack enable
yarn install
# Autentique-se usando sua chave de API (você será solicitado)
yarn auth:login
yarn twenty auth:login
# Iniciar modo de desenvolvimento: sincroniza automaticamente as alterações locais com seu workspace
yarn app:dev
yarn twenty app:dev
```
O gerador de estrutura oferece suporte a três modos para controlar quais arquivos de exemplo são incluídos:
```bash filename="Terminal"
# Padrão (exhaustivo): todos os exemplos (objeto, campo, função de lógica, componente de front-end, visualização, item do menu de navegação)
npx create-twenty-app@latest my-app
# Mínimo: apenas arquivos principais (application-config.ts e default-role.ts)
npx create-twenty-app@latest my-app --minimal
# Interativo: selecione quais exemplos incluir
npx create-twenty-app@latest my-app --interactive
```
A partir daqui você pode:
```bash filename="Terminal"
# Adicionar uma nova entidade à sua aplicação (assistido)
yarn entity:add
# Gerar um cliente Twenty tipado e tipos de entidades do espaço de trabalho
yarn app:generate
yarn twenty entity:add
# Acompanhar os logs das funções da sua aplicação
yarn function:logs
yarn twenty function:logs
# Executar uma função pelo nome
yarn function:execute -n my-function -p '{\"name\": \"test\"}'
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
# Executar a função de pós-instalação
yarn twenty function:execute --postInstall
# Desinstalar a aplicação do espaço de trabalho atual
yarn app:uninstall
yarn twenty app:uninstall
# Exibir a ajuda dos comandos
yarn help
yarn twenty help
```
Veja também: as páginas de referência da CLI para [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) e [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@@ -77,9 +86,9 @@ Ao executar `npx create-twenty-app@latest my-twenty-app`, o gerador:
* Copia um aplicativo base mínimo para `my-twenty-app/`
* Adiciona uma dependência local `twenty-sdk` e a configuração do Yarn 4
* Cria arquivos de configuração e scripts conectados à CLI `twenty`
* Gera uma configuração de aplicativo padrão e um papel padrão para as funções
* Gera arquivos principais (configuração da aplicação, papel padrão para funções de lógica, função de pós-instalação) além de arquivos de exemplo com base no modo de geração de estrutura
Um aplicativo recém-criado pelo scaffold fica assim:
Um app recém-criado com o modo padrão `--exhaustive` fica assim:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -95,90 +104,76 @@ my-twenty-app/
README.md
public/ # Pasta de recursos públicos (imagens, fontes, etc.)
src/
application.config.ts # Obrigatório - configuração principal da aplicação
default-function.role.ts # Papel padrão para funções serverless
hello-world.function.ts # Exemplo de função serverless
hello-world.front-component.tsx # Exemplo de componente de front-end
// suas entidades (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
├── application-config.ts # Obrigatório - configuração principal da aplicação
├── roles/
│ └── default-role.ts # Papel padrão para funções de lógica
├── objects/
│ └── example-object.ts # Exemplo de definição de objeto personalizado
├── fields/
│ └── example-field.ts # Exemplo de definição de campo independente
├── logic-functions/
│ ├── hello-world.ts # Exemplo de função de lógica
│ └── post-install.ts # Função de lógica de pós-instalação
├── front-components/
│ └── hello-world.tsx # Exemplo de componente de front-end
├── views/
│ └── example-view.ts # Exemplo de definição de visualização salva
└── navigation-menu-items/
└── example-navigation-menu-item.ts # Exemplo de link de navegação da barra lateral
```
### Convenção sobre configuração
Os aplicativos usam uma abordagem de **convenção sobre configuração** em que as entidades são detectadas pelo sufixo do arquivo. Isso permite organização flexível dentro da pasta `src/app/`:
| Sufixo de arquivo | Tipo de entidade |
| ----------------------- | -------------------------------------- |
| `*.object.ts` | Definições de objetos personalizados |
| `*.function.ts` | Definições de funções serverless |
| `*.front-component.tsx` | Definições de componentes de front-end |
| `*.role.ts` | Definições de papéis |
### Organizações de pastas suportadas
Você pode organizar suas entidades em qualquer um destes padrões:
**Tradicional (por tipo):**
```text
src/
├── application.config.ts
├── objects/
│ └── postCard.object.ts
├── functions/
│ └── createPostCard.function.ts
├── components/
│ └── card.front-component.tsx
└── roles/
└── admin.role.ts
```
**Baseada em funcionalidades:**
```text
src/
├── application.config.ts
└── post-card/
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── postCardAdmin.role.ts
```
**Plana:**
```text
src/
├── application.config.ts
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── admin.role.ts
```
Com `--minimal`, apenas os arquivos principais são criados (`application-config.ts`, `roles/default-role.ts` e `logic-functions/post-install.ts`). Com `--interactive`, você escolhe quais arquivos de exemplo incluir.
Em alto nível:
* **package.json**: Declara o nome do app, versão, engines (Node 24+, Yarn 4) e adiciona `twenty-sdk`, além de scripts como `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` e `auth:login` que delegam para a CLI local `twenty`.
* **package.json**: Declara o nome do app, versão, engines (Node 24+, Yarn 4), e adiciona `twenty-sdk` além de um script `twenty` que delega para a CLI `twenty` local. Execute `yarn twenty help` para listar todos os comandos disponíveis.
* **.gitignore**: Ignora artefatos comuns como `node_modules`, `.yarn`, `generated/` (cliente tipado), `dist/`, `build/`, pastas de cobertura, arquivos de log e arquivos `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Bloqueiam e configuram a ferramenta Yarn 4 usada pelo projeto.
* **.nvmrc**: Fixa a versão do Node.js esperada pelo projeto.
* **eslint.config.mjs** e **tsconfig.json**: Fornecem lint e configuração do TypeScript para os fontes TypeScript do seu aplicativo.
* **README.md**: Um README curto na raiz do aplicativo com instruções básicas.
* **public/**: Uma pasta para armazenar recursos públicos (imagens, fontes, arquivos estáticos) que serão servidos com sua aplicação. Os arquivos colocados aqui são enviados durante a sincronização e ficam acessíveis em tempo de execução.
* **src/**: O local principal onde você define seu aplicativo como código:
* `application.config.ts`: Configuração global do seu aplicativo (metadados e conexões de execução). Veja "Configuração do aplicativo" abaixo.
* `*.role.ts`: Definições de papéis usadas pelas suas funções de lógica. Veja "Papel de função padrão" abaixo.
* `*.object.ts`: Definições de objetos personalizados.
* `*.function.ts`: Definições de funções de lógica.
* `*.front-component.tsx`: Definições de componentes de front-end.
* **src/**: O local principal onde você define seu aplicativo como código
### Detecção de entidades
O SDK detecta entidades analisando seus arquivos TypeScript em busca de chamadas **`export default define<Entity>({...})`**. Cada tipo de entidade tem uma função utilitária correspondente exportada de `twenty-sdk`:
| Função utilitária | Tipo de entidade |
| ---------------------------- | ------------------------------------------- |
| `defineObject()` | Definições de objetos personalizados |
| `defineLogicFunction()` | Definições de funções de lógica |
| `defineFrontComponent()` | Definições de componentes de front-end |
| `defineRole()` | Definições de papéis |
| `defineField()` | Extensões de campos para objetos existentes |
| `defineView()` | Definições de visualizações salvas |
| `defineNavigationMenuItem()` | Definições de itens do menu de navegação |
<Note>
**A nomeação de arquivos é flexível.** A detecção de entidades é baseada em AST — o SDK varre seus arquivos fonte em busca do padrão `export default define<Entity>({...})`. Você pode organizar seus arquivos e pastas como quiser. Agrupar por tipo de entidade (por exemplo, `logic-functions/`, `roles/`) é apenas uma convenção para organização do código, não um requisito.
</Note>
Exemplo de uma entidade detectada:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
Comandos posteriores adicionarão mais arquivos e pastas:
* `yarn app:generate` criará uma pasta `generated/` (cliente tipado do Twenty + tipos do workspace).
* `yarn entity:add` adicionará arquivos de definição de entidade em `src/` para seus objetos, funções, componentes de front-end ou papéis personalizados.
* `yarn twenty app:dev` vai gerar automaticamente um cliente de API tipado em `node_modules/twenty-sdk/generated` (cliente Twenty tipado + tipos do espaço de trabalho).
* `yarn twenty entity:add` adicionará arquivos de definição de entidade em `src/` para seus objetos, funções, componentes de front-end ou papéis personalizados.
## Autenticação
Na primeira vez que você executar `yarn auth:login`, será solicitado o seguinte:
Na primeira vez que você executar `yarn twenty auth:login`, será solicitado o seguinte:
* URL da API (padrão: http://localhost:3000 ou o perfil do seu espaço de trabalho atual)
* Chave de API
@@ -189,25 +184,25 @@ Suas credenciais são armazenadas por usuário em `~/.twenty/config.json`. Você
```bash filename="Terminal"
# Fazer login interativamente (recomendado)
yarn auth:login
yarn twenty auth:login
# Fazer login em um perfil de espaço de trabalho específico
yarn auth:login --workspace my-custom-workspace
yarn twenty auth:login --workspace my-custom-workspace
# Listar todos os espaços de trabalho configurados
yarn auth:list
yarn twenty auth:list
# Alterar o espaço de trabalho padrão (interativo)
yarn auth:switch
yarn twenty auth:switch
# Alternar para um espaço de trabalho específico
yarn auth:switch production
yarn twenty auth:switch production
# Verificar o status atual da autenticação
yarn auth:status
yarn twenty auth:status
```
Depois que você alternar os espaços de trabalho com `auth:switch`, todos os comandos subsequentes usarão esse espaço de trabalho por padrão. Você ainda pode substituí-lo temporariamente com `--workspace <name>`.
Depois que você alternar os espaços de trabalho com `yarn twenty auth:switch`, todos os comandos subsequentes usarão esse espaço de trabalho por padrão. Você ainda pode substituí-lo temporariamente com `--workspace <name>`.
## Use os recursos do SDK (tipos e configuração)
@@ -215,16 +210,20 @@ O twenty-sdk fornece blocos de construção tipados e funções utilitárias que
### Funções utilitárias
O SDK fornece quatro funções utilitárias com validação integrada para definir as entidades do seu aplicativo:
O SDK fornece funções utilitárias para definir as entidades do seu app. Conforme descrito em [Detecção de entidades](#entity-detection), você deve usar `export default define<Entity>({...})` para que suas entidades sejam detectadas:
| Função | Finalidade |
| --------------------- | ------------------------------------------------- |
| `defineApplication()` | Configura os metadados do aplicativo |
| `defineObject()` | Define objetos personalizados com campos |
| `defineFunction()` | Defina funções de lógica com handlers |
| `defineRole()` | Configura permissões de papéis e acesso a objetos |
| Função | Finalidade |
| ---------------------------- | ------------------------------------------------------------ |
| `defineApplication()` | Configurar metadados do aplicativo (obrigatório, um por app) |
| `defineObject()` | Define objetos personalizados com campos |
| `defineLogicFunction()` | Defina funções de lógica com handlers |
| `defineFrontComponent()` | Definir componentes de front-end para UI personalizada |
| `defineRole()` | Configura permissões de papéis e acesso a objetos |
| `defineField()` | Estender objetos existentes com campos adicionais |
| `defineView()` | Define visualizações salvas para objetos |
| `defineNavigationMenuItem()` | Define links de navegação da barra lateral |
Essas funções validam sua configuração em tempo de execução e oferecem melhor autocompletar na IDE e segurança de tipos.
Essas funções validam sua configuração em tempo de compilação e oferecem autocompletar na IDE e segurança de tipos.
### Definindo objetos
@@ -305,26 +304,32 @@ Pontos-chave:
* O `universalIdentifier` deve ser exclusivo e estável entre implantações.
* Cada campo requer `name`, `type`, `label` e seu próprio `universalIdentifier` estável.
* O array `fields` é opcional — você pode definir objetos sem campos personalizados.
* Você pode criar novos objetos usando `yarn entity:add`, que orienta você sobre nomeação, campos e relacionamentos.
* Você pode criar novos objetos usando `yarn twenty entity:add`, que orienta você sobre nomeação, campos e relacionamentos.
<Note>
**Os campos base são criados automaticamente.** Quando você define um objeto personalizado, o Twenty adiciona automaticamente campos padrão como `name`, `createdAt`, `updatedAt`, `createdBy`, `position` e `deletedAt`. Você não precisa definir esses no seu array `fields` — adicione apenas seus campos personalizados.
**Os campos base são criados automaticamente.** Quando você define um objeto personalizado, o Twenty adiciona automaticamente campos padrão
como `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` e `deletedAt`.
Você não precisa definir esses no seu array `fields` — adicione apenas seus campos personalizados.
Você pode substituir os campos padrão definindo um campo com o mesmo nome no seu array `fields`,
mas isso não é recomendado.
</Note>
### Configuração do aplicativo (application.config.ts)
### Configuração do aplicativo (application-config.ts)
Todo aplicativo tem um único arquivo `application.config.ts` que descreve:
Todo aplicativo tem um único arquivo `application-config.ts` que descreve:
* **O que é o aplicativo**: identificadores, nome de exibição e descrição.
* **Como suas funções são executadas**: qual papel usam para permissões.
* **Variáveis (opcional)**: pares chavevalor expostos às suas funções como variáveis de ambiente.
* **(Opcional) função de pós-instalação**: uma função de lógica que é executada após a instalação da aplicação.
Use `defineApplication()` to define your application configuration:
```typescript
// src/app/application.config.ts
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -339,7 +344,8 @@ export default defineApplication({
isSecret: false,
},
},
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -347,11 +353,12 @@ Notas:
* `universalIdentifier` são IDs determinísticos que você controla; gere-os uma vez e mantenha-os estáveis entre sincronizações.
* `applicationVariables` tornam-se variáveis de ambiente para suas funções (por exemplo, `DEFAULT_RECIPIENT_NAME` fica disponível como `process.env.DEFAULT_RECIPIENT_NAME`).
* `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
* `defaultRoleUniversalIdentifier` deve corresponder ao arquivo do papel (veja abaixo).
* `postInstallLogicFunctionUniversalIdentifier` (opcional) aponta para uma função de lógica que é executada automaticamente após a instalação da aplicação. Consulte [Funções de pós-instalação](#post-install-functions).
#### Papéis e permissões
Os aplicativos podem definir papéis que encapsulam permissões sobre os objetos e ações do seu espaço de trabalho. The field `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
Os aplicativos podem definir papéis que encapsulam permissões sobre os objetos e ações do seu espaço de trabalho. O campo `defaultRoleUniversalIdentifier` em `application-config.ts` designa o papel padrão usado pelas funções de lógica do seu app.
* A chave de API em tempo de execução, injetada como `TWENTY_API_KEY`, é derivada desse papel padrão de função.
* O cliente tipado ficará restrito às permissões concedidas a esse papel.
@@ -362,7 +369,7 @@ Os aplicativos podem definir papéis que encapsulam permissões sobre os objetos
Ao criar um novo aplicativo com o scaffold, a CLI também cria um arquivo de papel padrão. Use `defineRole()` para definir papéis com validação integrada:
```typescript
// src/app/default-function.role.ts
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
@@ -401,10 +408,10 @@ export default defineRole({
});
```
O `universalIdentifier` desse papel é então referenciado em `application.config.ts` como `roleUniversalIdentifier`. Em outras palavras:
O `universalIdentifier` desse papel é então referenciado em `application-config.ts` como `defaultRoleUniversalIdentifier`. Em outras palavras:
* **\*.role.ts** define o que o papel de função padrão pode fazer.
* **application.config.ts** aponta para esse papel para que suas funções herdem suas permissões.
* **application-config.ts** aponta para esse papel para que suas funções herdem suas permissões.
Notas:
@@ -415,11 +422,11 @@ Notas:
### Configuração de função de lógica e ponto de entrada
Cada arquivo de função usa `defineFunction()` para exportar uma configuração com um handler e gatilhos opcionais. Use o sufixo de arquivo `*.function.ts` para detecção automática.
Cada arquivo de função usa `defineLogicFunction()` para exportar uma configuração com um handler e gatilhos opcionais.
```typescript
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
@@ -439,7 +446,7 @@ const handler = async (params: RoutePayload) => {
return result;
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
@@ -486,6 +493,55 @@ Notas:
* O array `triggers` é opcional. Funções sem gatilhos podem ser usadas como funções utilitárias chamadas por outras funções.
* Você pode misturar vários tipos de gatilho em uma única função.
### Funções de pós-instalação
Uma função de pós-instalação é uma função de lógica que é executada automaticamente após a sua aplicação ser instalada em um espaço de trabalho. Isso é útil para tarefas de configuração únicas, como preencher dados padrão, criar registros iniciais ou configurar as configurações do espaço de trabalho.
Ao criar a estrutura de um novo app com `create-twenty-app`, uma função de pós-instalação é gerada para você em `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
A função é conectada ao seu app referenciando seu identificador universal em `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Você também pode executar manualmente a função de pós-instalação a qualquer momento usando a CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Pontos-chave:
* As funções de pós-instalação são funções de lógica padrão — elas usam `defineLogicFunction()` como qualquer outra função.
* O campo `postInstallLogicFunctionUniversalIdentifier` em `defineApplication()` é opcional. Se omitido, nenhuma função é executada após a instalação.
* 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.
* As funções de pós-instalação não precisam de gatilhos — elas são invocadas pela plataforma durante a instalação ou manualmente via `function:execute --postInstall`.
### Payload de gatilho de rota
<Warning>
@@ -515,7 +571,7 @@ Notas:
Quando um gatilho de rota invoca sua função de lógica, ela recebe um objeto `RoutePayload` que segue o formato do AWS HTTP API v2. Importe o tipo de `twenty-sdk`:
```typescript
import { defineFunction, type RoutePayload } from 'twenty-sdk';
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
@@ -545,7 +601,7 @@ O tipo `RoutePayload` tem a seguinte estrutura:
Por padrão, os cabeçalhos HTTP das requisições recebidas **não** são repassados para sua função de lógica por motivos de segurança. Para acessar cabeçalhos específicos, liste-os explicitamente no array `forwardedRequestHeaders`:
```typescript
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
@@ -580,12 +636,114 @@ const handler = async (event: RoutePayload) => {
Você pode criar novas funções de duas formas:
* **Gerado automaticamente**: Execute `yarn entity:add` e escolha a opção para adicionar uma nova função. Isso gera um arquivo inicial com um handler e configuração.
* **Manual**: Crie um novo arquivo `*.function.ts` e use `defineFunction()`, seguindo o mesmo padrão.
* **Gerado automaticamente**: Execute `yarn twenty entity:add` e escolha a opção para adicionar uma nova função de lógica. Isso gera um arquivo inicial com um handler e configuração.
* **Manual**: Crie um novo arquivo `*.logic-function.ts` e use `defineLogicFunction()`, seguindo o mesmo padrão.
### Marcar uma função lógica como ferramenta
Funções lógicas podem ser expostas como **ferramentas** para agentes de IA e fluxos de trabalho. Quando uma função é marcada como ferramenta, ela fica disponível para os recursos de IA do Twenty e pode ser selecionada como uma etapa em automações de fluxos de trabalho.
Para marcar uma função lógica como ferramenta, defina `isTool: true` e forneça um `toolInputSchema` descrevendo os parâmetros de entrada esperados usando [JSON Schema](https://json-schema.org/):
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
Pontos-chave:
* **`isTool`** (`boolean`, padrão: `false`): Quando definido como `true`, a função é registrada como uma ferramenta e fica disponível para agentes de IA e automações de fluxos de trabalho.
* **`toolInputSchema`** (`object`, opcional): Um objeto JSON Schema que descreve os parâmetros que sua função aceita. Os agentes de IA usam esse esquema para entender quais entradas a ferramenta espera e para validar as chamadas. Se omitido, o esquema tem como padrão `{ type: 'object', properties: {} }` (sem parâmetros).
* Funções com `isTool: false` (ou não definido) **não** são expostas como ferramentas. Elas ainda podem ser executadas diretamente ou chamadas por outras funções, mas não aparecerão na descoberta de ferramentas.
* **Nomenclatura de ferramentas**: Quando exposta como uma ferramenta, o nome da função é automaticamente normalizado para `logic_function_<name>` (em minúsculas, caracteres não alfanuméricos substituídos por sublinhados). Por exemplo, `enrich-company` torna-se `logic_function_enrich_company`.
* Você pode combinar `isTool` com gatilhos — uma função pode ser ao mesmo tempo uma ferramenta (chamável por agentes de IA) e acionada por eventos (cron, eventos de banco de dados, rotas) simultaneamente.
<Note>
**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>
### Componentes de front-end
Componentes de front-end permitem criar componentes React personalizados que são renderizados na UI do Twenty. Use `defineFrontComponent()` para definir componentes com validação integrada:
```typescript
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
Pontos-chave:
* Componentes de front-end são componentes React que renderizam em contextos isolados dentro do Twenty.
* Use o sufixo de arquivo `*.front-component.tsx` para detecção automática.
* O campo `component` faz referência ao seu componente React.
* Os componentes são compilados e sincronizados automaticamente durante `yarn twenty app:dev`.
Você pode criar novos componentes de front-end de duas formas:
* **Gerado automaticamente**: Execute `yarn twenty entity:add` e escolha a opção para adicionar um novo componente de front-end.
* **Manual**: Crie um novo arquivo `*.front-component.tsx` e use `defineFrontComponent()`.
### Cliente tipado gerado
Execute yarn app:generate para criar um cliente tipado local em generated/ com base no esquema do seu workspace. Use-o em suas funções:
O cliente tipado é gerado automaticamente pelo `yarn twenty app:dev` e armazenado em `node_modules/twenty-sdk/generated` com base no esquema do seu espaço de trabalho. Use-o em suas funções:
```typescript
import Twenty from '~/generated';
@@ -594,7 +752,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
O cliente é regenerado pelo `yarn app:generate`. Execute novamente após alterar seus objetos ou ao ingressar em um novo workspace.
O cliente é regenerado automaticamente pelo `yarn twenty app:dev` sempre que seus objetos ou campos forem alterados.
#### Credenciais em tempo de execução em funções de lógica
@@ -606,49 +764,38 @@ Quando sua função é executada no Twenty, a plataforma injeta credenciais como
Notas:
* Você não precisa passar a URL ou a chave de API para o cliente gerado. Ele lê `TWENTY_API_URL` e `TWENTY_API_KEY` de process.env em tempo de execução.
* As permissões da chave de API são determinadas pelo papel referenciado no seu `application.config.ts` via `roleUniversalIdentifier`. Este é o papel padrão usado pelas funções de lógica do seu app.
* Os aplicativos podem definir papéis para seguir o princípio do menor privilégio. Conceda apenas as permissões de que suas funções precisam e, em seguida, aponte `roleUniversalIdentifier` para o identificador universal desse papel.
* As permissões da chave de API são determinadas pelo papel referenciado no seu `application-config.ts` via `defaultRoleUniversalIdentifier`. Este é o papel padrão usado pelas funções de lógica do seu app.
* Os aplicativos podem definir papéis para seguir o princípio do menor privilégio. Conceda apenas as permissões de que suas funções precisam e, em seguida, aponte `defaultRoleUniversalIdentifier` para o identificador universal desse papel.
### Exemplo Hello World
Explore um exemplo mínimo de ponta a ponta que demonstra objetos, funções e vários gatilhos [aqui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
Explore um exemplo mínimo de ponta a ponta que demonstra objetos, funções de lógica, componentes de front-end e vários gatilhos [aqui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Configuração manual (sem o gerador)
Embora recomendemos usar `create-twenty-app` para a melhor experiência inicial, você também pode configurar um projeto manualmente. Não instale a CLI globalmente. Em vez disso, adicione `twenty-sdk` como uma dependência local e conecte scripts no seu package.json:
Embora recomendemos usar `create-twenty-app` para a melhor experiência inicial, você também pode configurar um projeto manualmente. Não instale a CLI globalmente. Em vez disso, adicione `twenty-sdk` como uma dependência local e configure um único script no seu package.json:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
Em seguida, adicione scripts como estes:
Em seguida, adicione um script `twenty`:
```json filename="package.json"
{
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
"twenty": "twenty"
}
}
```
Agora você pode executar os mesmos comandos via Yarn, por exemplo, `yarn app:dev`, `yarn app:generate`, etc.
Agora você pode executar todos os comandos via `yarn twenty <command>`, por exemplo, `yarn twenty app:dev`, `yarn twenty help`, etc.
## Resolução de Problemas
* Erros de autenticação: execute `yarn auth:login` e certifique-se de que sua chave de API tenha as permissões necessárias.
* Erros de autenticação: execute `yarn twenty auth:login` e certifique-se de que sua chave de API tenha as permissões necessárias.
* Não é possível conectar ao servidor: verifique a URL da API e se o servidor do Twenty está acessível.
* Tipos ou cliente ausentes/desatualizados: execute `yarn app:generate`.
* Modo de desenvolvimento não sincronizando: certifique-se de que `yarn app:dev` esteja em execução e de que as alterações não estejam sendo ignoradas pelo seu ambiente.
* Tipos ou cliente ausentes/desatualizados: reinicie `yarn twenty app:dev` — ele gera automaticamente o cliente tipado.
* Modo de desenvolvimento não sincronizando: certifique-se de que `yarn twenty app:dev` esteja em execução e de que as alterações não estejam sendo ignoradas pelo seu ambiente.
Canal de ajuda no Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -17,10 +17,6 @@ Aplicațiile vă permit să construiți și să gestionați personalizările Twe
* Creați funcții de logică cu declanșatoare personalizate
* Implementați aceeași aplicație în mai multe spații de lucru
**În curând:**
* Dispuneri și componente UI personalizate
## Cerințe
* Node.js 24+ și Yarn 4
@@ -31,7 +27,7 @@ Aplicațiile vă permit să construiți și să gestionați personalizările Twe
Creați o aplicație nouă folosind generatorul oficial, apoi autentificați-vă și începeți să dezvoltați:
```bash filename="Terminal"
# Creează scheletul unei aplicații noi
# Creează scheletul unei aplicații noi (include toate exemplele în mod implicit)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
@@ -40,32 +36,45 @@ corepack enable
yarn install
# Autentifică-te folosind cheia ta API (ți se va solicita)
yarn auth:login
yarn twenty auth:login
# Pornește modul de dezvoltare: sincronizează automat modificările locale cu spațiul tău de lucru
yarn app:dev
yarn twenty app:dev
```
Generatorul de schelet acceptă trei moduri pentru a controla ce fișiere de exemplu sunt incluse:
```bash filename="Terminal"
# Implicit (exhaustiv): toate exemplele (obiect, câmp, funcție logică, componentă de interfață, vizualizare, element de meniu de navigare)
npx create-twenty-app@latest my-app
# Minimal: doar fișierele de bază (application-config.ts și default-role.ts)
npx create-twenty-app@latest my-app --minimal
# Interactiv: selectezi ce exemple să incluzi
npx create-twenty-app@latest my-app --interactive
```
De aici puteți:
```bash filename="Terminal"
# Adaugă o entitate nouă în aplicația ta (ghidat)
yarn entity:add
# Generează un client Twenty tipat și tipurile de entități ale spațiului de lucru
yarn app:generate
yarn twenty entity:add
# Urmărește jurnalele funcțiilor aplicației tale
yarn function:logs
yarn twenty function:logs
# Execută o funcție după nume
yarn function:execute -n my-function -p '{"name": "test"}'
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execută funcția post-instalare
yarn twenty function:execute --postInstall
# Dezinstalează aplicația din spațiul de lucru curent
yarn app:uninstall
yarn twenty app:uninstall
# Afișează ajutorul pentru comenzi
yarn help},{
yarn twenty help
```
Consultați și: paginile de referință CLI pentru [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) și [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@@ -77,9 +86,9 @@ Când rulați `npx create-twenty-app@latest my-twenty-app`, generatorul:
* Copiază o aplicație de bază minimală în `my-twenty-app/`
* Adaugă o dependență locală `twenty-sdk` și configurația Yarn 4
* Creează fișiere de configurare și scripturi conectate la CLI-ul `twenty`
* Generează o configurație implicită a aplicației și un rol implicit pentru funcții
* Generează fișierele de bază (configurația aplicației, rolul implicit al funcțiilor, funcția post-instalare) plus fișiere de exemplu în funcție de modul de generare a scheletului
O aplicație nou generată arată astfel:
O aplicație proaspăt generată cu modul implicit `--exhaustive` arată astfel:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -95,90 +104,76 @@ my-twenty-app/
README.md
public/ # Director pentru resurse publice (imagini, fonturi etc.)
src/
application.config.ts # Obligatoriu - configurația principală a aplicației
default-function.role.ts # Rolul implicit pentru funcțiile serverless
hello-world.function.ts # Exemplu de funcție serverless
hello-world.front-component.tsx # Exemplu de componentă de interfață
// entitățile tale (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
├── application-config.ts # Obligatoriu - configurația principală a aplicației
├── roles/
└── default-role.ts # Rol implicit pentru funcțiile logice
├── objects/
│ └── example-object.ts # Exemplu de definiție a unui obiect personalizat
├── fields/
│ └── example-field.ts # Exemplu de definiție de câmp independent
├── logic-functions/
│ ├── hello-world.ts # Exemplu de funcție logică
│ └── post-install.ts # Funcție logică post-instalare
├── front-components/
│ └── hello-world.tsx # Exemplu de componentă de interfață
├── views/
│ └── example-view.ts # Exemplu de definiție a unei vizualizări salvate
└── navigation-menu-items/
└── example-navigation-menu-item.ts # Exemplu de link de navigare în bara laterală
```
### Convenție în locul configurării
Aplicațiile folosesc o abordare bazată pe convenție în locul configurării, în care entitățile sunt detectate după sufixul fișierului. Aceasta permite o organizare flexibilă în folderul `src/app/`:
| Sufixul fișierului | Tipul entității |
| ----------------------- | ---------------------------------------- |
| `*.object.ts` | Definiții de obiecte personalizate |
| `*.function.ts` | Definiții de funcții serverless |
| `*.front-component.tsx` | Definiții ale componentelor de interfață |
| `*.role.ts` | Definiții de rol |
### Structuri de foldere acceptate
Vă puteți organiza entitățile în oricare dintre aceste modele:
**Tradițional (după tip):**
```text
src/
├── application.config.ts
├── objects/
│ └── postCard.object.ts
├── functions/
│ └── createPostCard.function.ts
├── components/
│ └── card.front-component.tsx
└── roles/
└── admin.role.ts
```
**Bazat pe funcționalități:**
```text
src/
├── application.config.ts
└── post-card/
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── postCardAdmin.role.ts
```
**Plat:**
```text
src/
├── application.config.ts
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── admin.role.ts
```
Cu `--minimal`, sunt create doar fișierele de bază (`application-config.ts`, `roles/default-role.ts` și `logic-functions/post-install.ts`). Cu `--interactive`, alegi ce fișiere de exemplu să incluzi.
Pe scurt:
* **package.json**: Declară numele aplicației, versiunea, motoarele (Node 24+, Yarn 4) și adaugă `twenty-sdk` plus scripturi precum `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` și `auth:login` care deleagă către CLI-ul local `twenty`.
* **package.json**: Declară numele aplicației, versiunea, motoarele (Node 24+, Yarn 4) și adaugă `twenty-sdk` plus un script `twenty` care deleagă către CLI-ul local `twenty`. Rulează `yarn twenty help` pentru a lista toate comenzile disponibile.
* **.gitignore**: Ignoră artefacte comune precum `node_modules`, `.yarn`, `generated/` (client tipizat), `dist/`, `build/`, foldere de coverage, fișiere jurnal și fișiere `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Blochează și configurează lanțul de instrumente Yarn 4 folosit de proiect.
* **.nvmrc**: Fixează versiunea Node.js așteptată de proiect.
* **eslint.config.mjs** și **tsconfig.json**: Oferă linting și configurație TypeScript pentru fișierele TypeScript ale aplicației.
* **README.md**: Un README scurt în rădăcina aplicației, cu instrucțiuni de bază.
* **public/**: Un folder pentru stocarea resurselor publice (imagini, fonturi, fișiere statice) care vor fi servite împreună cu aplicația ta. Fișierele plasate aici sunt încărcate în timpul sincronizării și sunt accesibile la rulare.
* **src/**: Locul principal unde vă definiți aplicația sub formă de cod:
* `application.config.ts`: Configurație globală pentru aplicație (metadate și conectare la runtime). Vezi "Configurația aplicației" mai jos.
* `*.role.ts`: Definiții de rol folosite de funcțiile dvs. de logică. Vezi "Rol implicit pentru funcții" mai jos.
* `*.object.ts`: Definiții de obiecte personalizate.
* `*.function.ts`: Definiții de funcții de logică.
* `*.front-component.tsx`: Definiții de componente front-end.
* **src/**: Locul principal unde vă definiți aplicația sub formă de cod
### Detectarea entităților
SDK-ul detectează entitățile analizând fișierele TypeScript pentru apeluri **`export default define<Entity>({...})`**. Fiecare tip de entitate are o funcție ajutătoare corespunzătoare, exportată din `twenty-sdk`:
| Funcție ajutătoare | Tipul entității |
| ---------------------------- | ---------------------------------------------- |
| `defineObject()` | Definiții de obiecte personalizate |
| `defineLogicFunction()` | Definiții de funcții de logică |
| `defineFrontComponent()` | Definiții ale componentelor de interfață |
| `defineRole()` | Definiții de rol |
| `defineField()` | Extensii de câmp pentru obiectele existente |
| `defineView()` | Definiții pentru vizualizări salvate |
| `defineNavigationMenuItem()` | Definiții pentru elemente de meniu de navigare |
<Note>
**Denumirea fișierelor este flexibilă.** Detectarea entităților se bazează pe AST — SDK-ul scanează fișierele sursă pentru tiparul `export default define<Entity>({...})`. Puteți organiza fișierele și folderele cum doriți. Gruparea după tipul de entitate (de exemplu, `logic-functions/`, `roles/`) este doar o convenție pentru organizarea codului, nu o cerință.
</Note>
Exemplu de entitate detectată:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
Comenzile ulterioare vor adăuga mai multe fișiere și foldere:
* `yarn app:generate` va crea un folder `generated/` (client Twenty tipizat + tipuri pentru spațiul de lucru).
* `yarn entity:add` va adăuga fișiere de definire a entităților în `src/` pentru obiectele, funcțiile, componentele front-end sau rolurile personalizate.
* `yarn twenty app:dev` va genera automat un client API tipizat în `node_modules/twenty-sdk/generated` (client Twenty tipizat + tipuri ale spațiului de lucru).
* `yarn twenty entity:add` va adăuga fișiere de definire a entităților în `src/` pentru obiectele, funcțiile, componentele front-end sau rolurile personalizate.
## Autentificare
Prima dată când rulați `yarn auth:login`, vi se vor solicita:
Prima dată când rulați `yarn twenty auth:login`, vi se vor solicita:
* URL-ul API (implicit http://localhost:3000 sau profilul spațiului de lucru curent)
* Cheie API
@@ -189,25 +184,25 @@ Acreditările dvs. sunt stocate per utilizator în `~/.twenty/config.json`. Pute
```bash filename="Terminal"
# Login interactively (recommended)
yarn auth:login
yarn twenty auth:login
# Login to a specific workspace profile
yarn auth:login --workspace my-custom-workspace
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn auth:list
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn auth:switch
yarn twenty auth:switch
# Switch to a specific workspace
yarn auth:switch production
yarn twenty auth:switch production
# Check current authentication status
yarn auth:status
yarn twenty auth:status
```
După ce ați schimbat spațiul de lucru cu `auth:switch`, toate comenzile ulterioare vor folosi implicit acel spațiu de lucru. Îl puteți totuși suprascrie temporar cu `--workspace <name>`.
După ce ați schimbat spațiul de lucru cu `yarn twenty auth:switch`, toate comenzile ulterioare vor folosi implicit acel spațiu de lucru. Îl puteți totuși suprascrie temporar cu `--workspace <name>`.
## Utilizați resursele SDK (tipuri și configurare)
@@ -215,16 +210,20 @@ Biblioteca twenty-sdk oferă blocuri de bază tipizate și funcții ajutătoare
### Funcții ajutătoare
SDK-ul oferă patru funcții ajutătoare cu validare încorporată pentru definirea entităților aplicației:
SDK-ul oferă funcții ajutătoare pentru definirea entităților aplicației. După cum este descris în [Detectarea entităților](#entity-detection), trebuie să folosiți `export default define<Entity>({...})` pentru ca entitățile să fie detectate:
| Funcție | Scop |
| --------------------- | -------------------------------------------------------- |
| `defineApplication()` | Configurați metadatele aplicației |
| `defineObject()` | Definiți obiecte personalizate cu câmpuri |
| `defineFunction()` | Definiți funcții de logică cu handleri |
| `defineRole()` | Configurați permisiunile rolurilor și accesul la obiecte |
| Funcție | Scop |
| ---------------------------- | ---------------------------------------------------------------------- |
| `defineApplication()` | Configurați metadatele aplicației (obligatoriu, una per aplicație) |
| `defineObject()` | Definiți obiecte personalizate cu câmpuri |
| `defineLogicFunction()` | Definiți funcții de logică cu handleri |
| `defineFrontComponent()` | Definiți componente Front pentru interfața de utilizator personalizată |
| `defineRole()` | Configurați permisiunile rolurilor și accesul la obiecte |
| `defineField()` | Extindeți obiectele existente cu câmpuri suplimentare |
| `defineView()` | Definește vizualizări salvate pentru obiecte |
| `defineNavigationMenuItem()` | Definește linkuri de navigare în bara laterală |
Aceste funcții validează configurația în timpul execuției și oferă o completare automată mai bună în IDE și siguranța tipurilor.
Aceste funcții validează configurația în timpul build-ului și oferă completare automată în IDE și siguranța tipurilor.
### Definirea obiectelor
@@ -305,26 +304,32 @@ Puncte cheie:
* `universalIdentifier` trebuie să fie unic și stabil între implementări.
* Fiecare câmp necesită un `name`, un `type`, un `label` și propriul `universalIdentifier` stabil.
* Matricea `fields` este opțională — puteți defini obiecte fără câmpuri personalizate.
* Puteți genera obiecte noi folosind `yarn entity:add`, care vă ghidează prin denumire, câmpuri și relații.
* Puteți genera obiecte noi folosind `yarn twenty entity:add`, care vă ghidează prin denumire, câmpuri și relații.
<Note>
**Câmpurile de bază sunt create automat.** Când definiți un obiect personalizat, Twenty adaugă automat câmpuri standard precum `name`, `createdAt`, `updatedAt`, `createdBy`, `position` și `deletedAt`. Nu trebuie să le definiți în tabloul `fields` — adăugați doar câmpurile personalizate proprii.
**Câmpurile de bază sunt create automat.** Când definiți un obiect personalizat, Twenty adaugă automat câmpuri standard
precum `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` și `deletedAt`.
Nu trebuie să le definiți în tabloul `fields` — adăugați doar câmpurile personalizate proprii.
Puteți suprascrie câmpurile implicite definind un câmp cu același nume în tabloul `fields`,
dar acest lucru nu este recomandat.
</Note>
### Configurația aplicației (application.config.ts)
### Configurația aplicației (application-config.ts)
Fiecare aplicație are un singur fișier `application.config.ts` care descrie:
Fiecare aplicație are un singur fișier `application-config.ts` care descrie:
* **Cine este aplicația**: identificatori, nume de afișare și descriere.
* **Cum rulează funcțiile**: ce rol folosesc pentru permisiuni.
* **(Opțional) variabile**: perechi cheievaloare expuse funcțiilor ca variabile de mediu.
* **(Opțional) funcție post-instalare**: o funcție logică care rulează după instalarea aplicației.
Folosiți `defineApplication()` pentru a defini configurația aplicației:
```typescript
// src/app/application.config.ts
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -339,7 +344,8 @@ export default defineApplication({
isSecret: false,
},
},
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -347,11 +353,12 @@ Notițe:
* Câmpurile `universalIdentifier` sunt ID-uri deterministe pe care le dețineți; generați-le o singură dată și păstrați-le stabile între sincronizări.
* `applicationVariables` devin variabile de mediu pentru funcțiile dvs. (de exemplu, `DEFAULT_RECIPIENT_NAME` este disponibil ca `process.env.DEFAULT_RECIPIENT_NAME`).
* `roleUniversalIdentifier` trebuie să corespundă rolului pe care îl definiți în fișierul `*.role.ts` (vedeți mai jos).
* `defaultRoleUniversalIdentifier` trebuie să corespundă fișierului de rol (vedeți mai jos).
* `postInstallLogicFunctionUniversalIdentifier` (opțional) indică o funcție logică care rulează automat după instalarea aplicației. Vezi [Funcții post-instalare](#post-install-functions).
#### Roluri și permisiuni
Aplicațiile pot defini roluri care încapsulează permisiuni asupra obiectelor și acțiunilor din spațiul dvs. de lucru. Câmpul `roleUniversalIdentifier` din `application.config.ts` desemnează rolul implicit folosit de funcțiile de logică ale aplicației.
Aplicațiile pot defini roluri care încapsulează permisiuni asupra obiectelor și acțiunilor din spațiul dvs. de lucru. Câmpul `defaultRoleUniversalIdentifier` din `application-config.ts` desemnează rolul implicit utilizat de funcțiile de logică ale aplicației.
* Cheia API de runtime injectată ca `TWENTY_API_KEY` este derivată din acest rol implicit pentru funcții.
* Clientul tipizat va fi restricționat la permisiunile acordate acelui rol.
@@ -362,7 +369,7 @@ Aplicațiile pot defini roluri care încapsulează permisiuni asupra obiectelor
Când generați o aplicație nouă, CLI creează și un fișier de rol implicit. Folosiți `defineRole()` pentru a defini roluri cu validare încorporată:
```typescript
// src/app/default-function.role.ts
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
@@ -401,10 +408,10 @@ export default defineRole({
});
```
`universalIdentifier` al acestui rol este apoi referențiat în `application.config.ts` ca `roleUniversalIdentifier`. Cu alte cuvinte:
`universalIdentifier` al acestui rol este apoi referențiat în `application-config.ts` ca `defaultRoleUniversalIdentifier`. Cu alte cuvinte:
* **\*.role.ts** definește ce poate face rolul implicit pentru funcții.
* **application.config.ts** indică acel rol, astfel încât funcțiile moștenesc permisiunile lui.
* **application-config.ts** indică acel rol, astfel încât funcțiile moștenesc permisiunile lui.
Notițe:
@@ -415,11 +422,11 @@ Notițe:
### Configurația funcției de logică și punctul de intrare
Fiecare fișier de funcție folosește `defineFunction()` pentru a exporta o configurație cu un handler și declanșatoare opționale. Folosiți sufixul de fișier `*.function.ts` pentru detectare automată.
Fiecare fișier de funcție folosește `defineLogicFunction()` pentru a exporta o configurație cu un handler și declanșatoare opționale.
```typescript
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
@@ -439,7 +446,7 @@ const handler = async (params: RoutePayload) => {
return result;
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
@@ -486,6 +493,55 @@ Notițe:
* Matricea `triggers` este opțională. Funcțiile fără declanșatoare pot fi folosite ca funcții utilitare apelate de alte funcții.
* Puteți combina mai multe tipuri de declanșatoare într-o singură funcție.
### Funcții post-instalare
O funcție post-instalare este o funcție logică care rulează automat după instalarea aplicației într-un spațiu de lucru. Aceasta este utilă pentru sarcini de configurare unice, cum ar fi popularea cu date implicite, crearea înregistrărilor inițiale sau configurarea setărilor spațiului de lucru.
Când creezi scheletul unei aplicații noi cu `create-twenty-app`, este generată o funcție post-instalare la `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
Funcția este integrată în aplicația ta prin referirea la identificatorul său universal în `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Poți, de asemenea, să execuți manual funcția post-instalare oricând folosind CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Puncte cheie:
* Funcțiile post-instalare sunt funcții logice standard — folosesc `defineLogicFunction()` la fel ca orice altă funcție.
* Câmpul `postInstallLogicFunctionUniversalIdentifier` din `defineApplication()` este opțional. Dacă este omis, nu rulează nicio funcție după instalare.
* Timpul de expirare implicit este setat la 300 de secunde (5 minute) pentru a permite sarcini de configurare mai lungi, cum ar fi popularea datelor.
* Funcțiile post-instalare nu au nevoie de declanșatoare — sunt invocate de platformă în timpul instalării sau manual prin `function:execute --postInstall`.
### Payload-ul declanșatorului de rută
<Warning>
@@ -515,7 +571,7 @@ Notițe:
Când un declanșator de rută apelează funcția dvs. de logică, aceasta primește un obiect `RoutePayload` care urmează formatul AWS HTTP API v2. Importă tipul din `twenty-sdk`:
```typescript
import { defineFunction, type RoutePayload } from 'twenty-sdk';
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
@@ -545,7 +601,7 @@ Tipul `RoutePayload` are următoarea structură:
În mod implicit, anteturile HTTP din cererile de intrare **nu** sunt transmise funcției dvs. de logică din motive de securitate. Pentru a accesa anumite anteturi, listează-le explicit în array-ul `forwardedRequestHeaders`:
```typescript
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
@@ -580,12 +636,114 @@ const handler = async (event: RoutePayload) => {
Puteți crea funcții noi în două moduri:
* **Generat**: Rulați `yarn entity:add` și alegeți opțiunea de a adăuga o funcție nouă. Aceasta generează un fișier inițial cu un handler și o configurație.
* **Manual**: Creați un fișier nou `*.function.ts` și folosiți `defineFunction()`, urmând același model.
* **Generat**: Rulați `yarn twenty entity:add` și alegeți opțiunea de a adăuga o funcție logică nouă. Aceasta generează un fișier inițial cu un handler și o configurație.
* **Manual**: Creați un fișier nou `*.logic-function.ts` și folosiți `defineLogicFunction()`, urmând același model.
### Marcarea unei funcții logice drept instrument
Funcțiile logice pot fi expuse ca **instrumente** pentru agenți de IA și fluxuri de lucru. Când o funcție este marcată ca instrument, poate fi descoperită de funcționalitățile de IA ale Twenty și poate fi selectată ca pas în automatizări ale fluxurilor de lucru.
Pentru a marca o funcție logică drept instrument, setați `isTool: true` și furnizați un `toolInputSchema` care descrie parametrii de intrare așteptați folosind [JSON Schema](https://json-schema.org/):
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
Puncte cheie:
* **`isTool`** (`boolean`, implicit: `false`): Când este setat la `true`, funcția este înregistrată ca instrument și devine disponibilă pentru agenții AI și automatizările de fluxuri de lucru.
* **`toolInputSchema`** (`object`, opțional): Un obiect JSON Schema care descrie parametrii pe care îi acceptă funcția dvs. Agenții AI folosesc această schemă pentru a înțelege ce intrări așteaptă instrumentul și pentru a valida apelurile. Dacă este omisă, schema are implicit valoarea `{ type: 'object', properties: {} }` (fără parametri).
* Funcțiile cu `isTool: false` (sau nedefinit) **nu** sunt expuse ca instrumente. Pot totuși fi executate direct sau apelate de alte funcții, dar nu vor apărea în descoperirea instrumentelor.
* **Denumierea instrumentelor**: Când este expusă ca instrument, denumirea funcției este normalizată automat la `logic_function_<name>` (convertită la litere mici, iar caracterele non-alfanumerice sunt înlocuite cu caractere de subliniere). De exemplu, `enrich-company` devine `logic_function_enrich_company`.
* Puteți combina `isTool` cu declanșatoare — o funcție poate fi atât un instrument (apelabilă de agenții AI), cât și declanșată de evenimente (cron, evenimente de bază de date, rute) în același timp.
<Note>
**Scrieți o `description` bună.** Agenții AI se bazează pe câmpul `description` al funcției pentru a decide când să folosească instrumentul. Fiți specifici cu privire la ceea ce face instrumentul și când ar trebui apelat.
</Note>
### Componente Front
Componentele Front vă permit să construiți componente React personalizate care sunt randate în interfața Twenty. Utilizați `defineFrontComponent()` pentru a defini componente cu validare încorporată:
```typescript
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
Puncte cheie:
* Componentele Front sunt componente React care sunt randate în contexte izolate în cadrul Twenty.
* Folosiți sufixul de fișier `*.front-component.tsx` pentru detectare automată.
* Câmpul `component` face referire la componenta React.
* Componentele sunt construite și sincronizate automat în timpul `yarn twenty app:dev`.
Puteți crea componente Front noi în două moduri:
* **Generat**: Rulați `yarn twenty entity:add` și alegeți opțiunea de a adăuga o componentă frontend nouă.
* **Manual**: Creați un fișier nou `*.front-component.tsx` și folosiți `defineFrontComponent()`.
### Client tipizat generat
Rulați yarn app:generate pentru a crea un client tipizat local în generated/, pe baza schemei spațiului de lucru. Folosiți-l în funcțiile dvs.:
Clientul tipizat este generat automat de `yarn twenty app:dev` și stocat în `node_modules/twenty-sdk/generated`, pe baza schemei spațiului tău de lucru. Folosiți-l în funcțiile dvs.:
```typescript
import Twenty from '~/generated';
@@ -594,7 +752,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
Clientul este regenerat de `yarn app:generate`. Rulați din nou după ce vă modificați obiectele sau când vă integrați într-un spațiu de lucru nou.
Clientul este regenerat automat de `yarn twenty app:dev` ori de câte ori obiectele sau câmpurile tale se schimbă.
#### Acreditări la runtime în funcțiile de logică
@@ -606,49 +764,38 @@ Când funcția rulează pe Twenty, platforma injectează acreditări ca variabil
Notițe:
* Nu trebuie să transmiteți URL-ul sau cheia API către clientul generat. Acesta citește `TWENTY_API_URL` și `TWENTY_API_KEY` din process.env la runtime.
* Permisiunile cheii API sunt determinate de rolul referențiat în `application.config.ts` prin `roleUniversalIdentifier`. Acesta este rolul implicit folosit de funcțiile de logică ale aplicației.
* Aplicațiile pot defini roluri pentru a urma principiul celui mai mic privilegiu. Acordați doar permisiunile de care au nevoie funcțiile, apoi setați `roleUniversalIdentifier` la identificatorul universal al acelui rol.
* Permisiunile cheii API sunt determinate de rolul referențiat în `application-config.ts` prin `defaultRoleUniversalIdentifier`. Acesta este rolul implicit folosit de funcțiile de logică ale aplicației.
* Aplicațiile pot defini roluri pentru a urma principiul celui mai mic privilegiu. Acordați doar permisiunile de care au nevoie funcțiile, apoi setați `defaultRoleUniversalIdentifier` la identificatorul universal al acelui rol.
### Exemplu Hello World
Explorați un exemplu minim, caplacap, care demonstrează obiecte, funcții și declanșatoare multiple [aici](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
Explorați un exemplu minim, cap la cap, care demonstrează obiecte, funcții de logică, componente Front și declanșatoare multiple [aici](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Configurare manuală (fără generator)
Deși recomandăm utilizarea `create-twenty-app` pentru cea mai bună experiență de început, puteți configura și un proiect manual. Nu instalați CLI-ul global. În schimb, adăugați `twenty-sdk` ca dependență locală și conectați scripturile în package.json-ul dvs.:
Deși recomandăm utilizarea `create-twenty-app` pentru cea mai bună experiență de început, puteți configura și un proiect manual. Nu instalați CLI-ul global. În schimb, adăugați `twenty-sdk` ca dependență locală și conectați un singur script în package.json-ul dvs.:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
Apoi adăugați scripturi ca acestea:
Apoi adăugați un script `twenty`:
```json filename="package.json"
{
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
"twenty": "twenty"
}
}
```
Acum puteți rula aceleași comenzi prin Yarn, de ex. `yarn app:dev`, `yarn app:generate`, etc.
Acum poți rula toate comenzile prin `yarn twenty <command>`, de ex. `yarn twenty app:dev`, `yarn twenty help`, etc.
## Depanare
* Erori de autentificare: rulați `yarn auth:login` și asigurați-vă că cheia API are permisiunile necesare.
* Erori de autentificare: rulați `yarn twenty auth:login` și asigurați-vă că cheia API are permisiunile necesare.
* Nu se poate conecta la server: verificați URL-ul API și că serverul Twenty este accesibil.
* Tipuri sau client lipsă/învechite: rulați `yarn app:generate`.
* Modul dev nu sincronizează: asigurați-vă că `yarn app:dev` rulează și că modificările nu sunt ignorate de mediul dvs.
* Types or client missing/outdated: restart `yarn twenty app:dev` — it auto-generates the typed client.
* Modul dev nu sincronizează: asigurați-vă că `yarn twenty app:dev` rulează și că modificările nu sunt ignorate de mediul dvs.
Canal de ajutor pe Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -17,10 +17,6 @@ description: Создавайте и управляйте настройками
* Создавайте логические функции с пользовательскими триггерами
* Развёртывайте одно и то же приложение в нескольких рабочих пространствах
**Скоро:**
* Пользовательские макеты и компоненты интерфейса
## Требования
* Node.js 24+ и Yarn 4
@@ -31,41 +27,54 @@ description: Создавайте и управляйте настройками
Создайте новое приложение с помощью официального генератора, затем выполните аутентификацию и начните разработку:
```bash filename="Terminal"
# Scaffold a new app
# Создать каркас нового приложения (по умолчанию включает все примеры)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# If you don't use yarn@4
# Если вы не используете yarn@4
corepack enable
yarn install
# Authenticate using your API key (you'll be prompted)
yarn auth:login
# Аутентифицироваться с помощью вашего API-ключа (вам будет предложено)
yarn twenty auth:login
# Start dev mode: automatically syncs local changes to your workspace
yarn app:dev
# Запустить режим разработки: автоматически синхронизирует локальные изменения с вашим рабочим пространством
yarn twenty app:dev
```
Генератор каркаса поддерживает три режима для управления тем, какие примерные файлы включаются:
```bash filename="Terminal"
# По умолчанию (полный набор): все примеры (объект, поле, логическая функция, фронтенд-компонент, представление, пункт меню навигации)
npx create-twenty-app@latest my-app
# Минимальный: только основные файлы (application-config.ts и default-role.ts)
npx create-twenty-app@latest my-app --minimal
# Интерактивный: выбрать, какие примеры включить
npx create-twenty-app@latest my-app --interactive
```
Отсюда вы можете:
```bash filename="Terminal"
# Добавить новую сущность в ваше приложение (с мастером)
yarn entity:add
# Сгенерировать типизированный клиент Twenty и типы сущностей рабочего пространства
yarn app:generate
yarn twenty entity:add
# Просматривать логи функций вашего приложения
yarn function:logs
yarn twenty function:logs
# Выполнить функцию по имени
yarn function:execute -n my-function -p '{"name": "test"}'
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
# Выполнить послеустановочную функцию
yarn twenty function:execute --postInstall
# Удалить приложение из текущего рабочего пространства
yarn app:uninstall
yarn twenty app:uninstall
# Показать справку по командам
yarn help
yarn twenty help
```
Смотрите также: страницы справки CLI для [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) и [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@@ -77,9 +86,9 @@ yarn help
* Копирует минимальное базовое приложение в `my-twenty-app/`
* Добавляет локальную зависимость `twenty-sdk` и конфигурацию Yarn 4
* Создаёт файлы конфигурации и скрипты, подключённые к CLI `twenty`
* Генерирует конфигурацию приложения по умолчанию и роль функции по умолчанию
* Генерирует основные файлы (конфигурацию приложения, роль функций по умолчанию, постустановочную функцию), а также примерные файлы в зависимости от выбранного режима создания каркаса
Свежесгенерированное приложение выглядит так:
Сгенерированное с помощью каркаса приложение с режимом по умолчанию `--exhaustive` выглядит так:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -93,92 +102,78 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Папка общедоступных ресурсов (изображения, шрифты и т. п.)
public/ # Папка публичных ресурсов (изображения, шрифты и т. д.)
src/
application.config.ts # Обязательный — основная конфигурация приложения
default-function.role.ts # Роль по умолчанию для бессерверных функций
hello-world.function.ts # Пример бессерверной функции
hello-world.front-component.tsx # Пример фронтенд-компонента
// ваши сущности (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
├── application-config.ts # Обязательный — основная конфигурация приложения
├── roles/
└── default-role.ts # Роль по умолчанию для логических функций
├── objects/
│ └── example-object.ts # Пример определения пользовательского объекта
├── fields/
│ └── example-field.ts # Пример определения отдельного поля
├── logic-functions/
│ ├── hello-world.ts # Пример логической функции
│ └── post-install.ts # Постустановочная логическая функция
├── front-components/
│ └── hello-world.tsx # Пример фронтенд-компонента
├── views/
│ └── example-view.ts # Пример определения сохранённого представления
└── navigation-menu-items/
└── example-navigation-menu-item.ts # Пример ссылки боковой панели навигации
```
### Соглашения важнее конфигурации
Приложения используют подход **соглашения важнее конфигурации**, при котором сущности определяются по суффиксу файла. Это позволяет гибко организовать структуру в папке `src/app/`:
| Суффикс файла | Тип сущности |
| ----------------------- | ------------------------------------- |
| `*.object.ts` | Определения пользовательских объектов |
| `*.function.ts` | Определения бессерверных функций |
| `*.front-component.tsx` | Определения компонентов фронтенда |
| `*.role.ts` | Определения ролей |
### Поддерживаемые способы организации папок
Вы можете организовать свои сущности по любому из этих шаблонов:
**Традиционный (по типам):**
```text
src/
├── application.config.ts
├── objects/
│ └── postCard.object.ts
├── functions/
│ └── createPostCard.function.ts
├── components/
│ └── card.front-component.tsx
└── roles/
└── admin.role.ts
```
**По функциональности:**
```text
src/
├── application.config.ts
└── post-card/
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── postCardAdmin.role.ts
```
**Плоский:**
```text
src/
├── application.config.ts
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── admin.role.ts
```
С `--minimal` создаются только основные файлы (`application-config.ts`, `roles/default-role.ts` и `logic-functions/post-install.ts`). С `--interactive` вы выбираете, какие примерные файлы включить.
В общих чертах:
* **package.json**: Объявляет имя приложения, версию, движки (Node 24+, Yarn 4) и добавляет `twenty-sdk`, а также скрипты вроде `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` и `auth:login`, которые делегируют выполнение локальному CLI `twenty`.
* **package.json**: Объявляет имя приложения, версию, движки (Node 24+, Yarn 4) и добавляет `twenty-sdk`, а также скрипт `twenty`, который делегирует выполнение локальному CLI `twenty`. Выполните `yarn twenty help`, чтобы вывести список всех доступных команд.
* **.gitignore**: Игнорирует распространённые артефакты, такие как `node_modules`, `.yarn`, `generated/` (типизированный клиент), `dist/`, `build/`, каталоги coverage, файлы журналов и файлы `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Фиксируют и настраивают используемый в проекте инструментарий Yarn 4.
* **.nvmrc**: Фиксирует версию Node.js, ожидаемую проектом.
* **eslint.config.mjs** и **tsconfig.json**: Обеспечивают линтинг и конфигурацию TypeScript для исходников вашего приложения на TypeScript.
* **README.md**: Короткий README в корне приложения с базовыми инструкциями.
* **public/**: Папка для хранения общедоступных ресурсов (изображений, шрифтов, статических файлов), которые будут отдаваться вашим приложением. Файлы, размещённые здесь, загружаются во время синхронизации и доступны во время выполнения.
* **src/**: Основное место, где вы определяете приложение как код:
* `application.config.ts`: Глобальная конфигурация вашего приложения (метаданные и параметры выполнения). См. раздел «Конфигурация приложения» ниже.
* `*.role.ts`: Определения ролей, используемых вашими логическими функциями. См. раздел «Роль функции по умолчанию» ниже.
* `*.object.ts`: Определения пользовательских объектов.
* `*.function.ts`: Определения логических функций.
* `*.front-component.tsx`: Определения фронтенд-компонентов.
* **src/**: Основное место, где вы определяете приложение как код
### Обнаружение сущностей
SDK обнаруживает сущности, разбирая ваши файлы TypeScript в поисках вызовов **`export default define<Entity>({...})`**. Для каждого типа сущности существует соответствующая вспомогательная функция, экспортируемая из `twenty-sdk`:
| Вспомогательная функция | Тип сущности |
| ---------------------------- | ------------------------------------------ |
| `defineObject()` | Определения пользовательских объектов |
| `defineLogicFunction()` | Определения логических функций |
| `defineFrontComponent()` | Определения компонентов фронтенда |
| `defineRole()` | Определения ролей |
| `defineField()` | Расширения полей для существующих объектов |
| `defineView()` | Определения сохранённых представлений |
| `defineNavigationMenuItem()` | Определения пунктов меню навигации |
<Note>
**Имена файлов заданы гибко.** Обнаружение сущностей основано на AST — SDK сканирует ваши исходные файлы в поисках шаблона `export default define<Entity>({...})`. Вы можете организовывать файлы и папки как угодно. Группировка по типу сущности (например, `logic-functions/`, `roles/`) — это лишь соглашение для организации кода, а не требование.
</Note>
Пример обнаруженной сущности:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
Позднее команды добавят больше файлов и папок:
* `yarn app:generate` создаст папку `generated/` (типизированный клиент Twenty + типы рабочего пространства).
* `yarn entity:add` добавит файлы определений сущностей в `src/` для ваших пользовательских объектов, функций, фронтенд-компонентов или ролей.
* `yarn twenty app:dev` автоматически сгенерирует типизированный клиент API в `node_modules/twenty-sdk/generated` (типизированный клиент Twenty + типы рабочего пространства).
* `yarn twenty entity:add` добавит файлы определений сущностей в `src/` для ваших пользовательских объектов, функций, фронтенд-компонентов или ролей.
## Аутентификация
При первом запуске `yarn auth:login` вам будет предложено указать:
При первом запуске `yarn twenty auth:login` вам будет предложено указать:
* URL API (по умолчанию http://localhost:3000 или текущий профиль рабочего пространства)
* Ключ API
@@ -189,25 +184,25 @@ src/
```bash filename="Terminal"
# Войти в интерактивном режиме (рекомендуется)
yarn auth:login
yarn twenty auth:login
# Войти в профиль конкретного рабочего пространства
yarn auth:login --workspace my-custom-workspace
yarn twenty auth:login --workspace my-custom-workspace
# Показать список всех настроенных рабочих пространств
yarn auth:list
yarn twenty auth:list
# Переключить рабочее пространство по умолчанию (в интерактивном режиме)
yarn auth:switch
yarn twenty auth:switch
# Переключиться на определённое рабочее пространство
yarn auth:switch production
yarn twenty auth:switch production
# Проверить текущий статус аутентификации
yarn auth:status
yarn twenty auth:status
```
После переключения рабочего пространства с помощью `auth:switch` все последующие команды по умолчанию будут использовать это рабочее пространство. Вы по-прежнему можете временно переопределить это с помощью `--workspace <name>`.
После переключения рабочего пространства с помощью `yarn twenty auth:switch` все последующие команды по умолчанию будут использовать это рабочее пространство. Вы по-прежнему можете временно переопределить это с помощью `--workspace <name>`.
## Используйте ресурсы SDK (типы и конфигурация)
@@ -215,16 +210,20 @@ yarn auth:status
### Вспомогательные функции
SDK предоставляет четыре вспомогательных функции с встроенной валидацией для определения сущностей вашего приложения:
SDK предоставляет вспомогательные функции для определения сущностей вашего приложения. Как описано в [Обнаружение сущностей](#entity-detection), вы должны использовать `export default define<Entity>({...})`, чтобы ваши сущности были обнаружены:
| Функция | Назначение |
| --------------------- | ---------------------------------------------- |
| `defineApplication()` | Настраивает метаданные приложения |
| `defineObject()` | Определяет пользовательские объекты с полями |
| `defineFunction()` | Определение логических функций с обработчиками |
| `defineRole()` | Настраивает права роли и доступ к объектам |
| Функция | Назначение |
| ---------------------------- | ---------------------------------------------------------------------- |
| `defineApplication()` | Настройка метаданных приложения (обязательно, по одному на приложение) |
| `defineObject()` | Определяет пользовательские объекты с полями |
| `defineLogicFunction()` | Определение логических функций с обработчиками |
| `defineFrontComponent()` | Определение фронт-компонентов для настраиваемого интерфейса |
| `defineRole()` | Настраивает права роли и доступ к объектам |
| `defineField()` | Расширение существующих объектов дополнительными полями |
| `defineView()` | Определяйте сохранённые представления для объектов |
| `defineNavigationMenuItem()` | Определяйте ссылки боковой панели навигации |
Эти функции проверяют вашу конфигурацию во время выполнения и обеспечивают лучшую автодополняемость в IDE и безопасность типов.
Эти функции проверяют вашу конфигурацию на этапе сборки и обеспечивают автодополнение в IDE и безопасность типов.
### Определение объектов
@@ -305,26 +304,32 @@ export default defineObject({
* `universalIdentifier` должен быть уникальным и стабильным между развёртываниями.
* Каждому полю требуются `name`, `type`, `label` и собственный стабильный `universalIdentifier`.
* Массив `fields` необязателен — вы можете определять объекты без пользовательских полей.
* Вы можете сгенерировать новые объекты с помощью `yarn entity:add`, который проведёт вас через выбор именования, полей и связей.
* Вы можете сгенерировать новые объекты с помощью `yarn twenty entity:add`, который проведёт вас через настройку имени, полей и связей.
<Note>
**Базовые поля создаются автоматически.** Когда вы определяете пользовательский объект, Twenty автоматически добавляет стандартные поля, такие как `name`, `createdAt`, `updatedAt`, `createdBy`, `position` и `deletedAt`. Вам не нужно определять их в массиве `fields` — добавляйте только свои пользовательские поля.
**Базовые поля создаются автоматически.** Когда вы определяете пользовательский объект, Twenty автоматически добавляет стандартные поля,
такие как `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` и `deletedAt`.
Вам не нужно определять их в массиве `fields` — добавляйте только свои пользовательские поля.
Вы можете переопределить поля по умолчанию, определив поле с тем же именем в массиве `fields`,
но это не рекомендуется.
</Note>
### Конфигурация приложения (application.config.ts)
### Конфигурация приложения (application-config.ts)
У каждого приложения есть единственный файл `application.config.ts`, который описывает:
У каждого приложения есть единственный файл `application-config.ts`, который описывает:
* **Что это за приложение**: идентификаторы, отображаемое имя и описание.
* **Как запускаются его функции**: какую роль они используют для прав доступа.
* **(Необязательно) переменные**: пары ключ-значение, предоставляемые вашим функциям как переменные окружения.
* **(Необязательно) послеустановочная функция**: функция логики, которая запускается после установки приложения.
Используйте `defineApplication()` для определения конфигурации вашего приложения:
```typescript
// src/app/application.config.ts
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -339,7 +344,8 @@ export default defineApplication({
isSecret: false,
},
},
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -347,11 +353,12 @@ export default defineApplication({
* `universalIdentifier` — это детерминированные идентификаторы, которыми вы управляете; сгенерируйте их один раз и сохраняйте стабильными между синхронизациями.
* `applicationVariables` становятся переменными окружения для ваших функций (например, `DEFAULT_RECIPIENT_NAME` доступна как `process.env.DEFAULT_RECIPIENT_NAME`).
* `roleUniversalIdentifier` должен соответствовать роли, которую вы определяете в файле `*.role.ts` (см. ниже).
* `defaultRoleUniversalIdentifier` должен соответствовать файлу роли (см. ниже).
* `postInstallLogicFunctionUniversalIdentifier` (необязательно) указывает на логическую функцию, которая автоматически выполняется после установки приложения. См. [Послеустановочные функции](#post-install-functions).
#### Роли и разрешения
Приложения могут определять роли, инкапсулирующие права на объекты и действия в вашем рабочем пространстве. Поле `roleUniversalIdentifier` в `application.config.ts` обозначает роль по умолчанию, используемую логическими функциями вашего приложения.
Приложения могут определять роли, инкапсулирующие права на объекты и действия в вашем рабочем пространстве. Поле `defaultRoleUniversalIdentifier` в `application-config.ts` обозначает роль по умолчанию, используемую логическими функциями вашего приложения.
* Ключ API во время выполнения, подставляемый как `TWENTY_API_KEY`, получается из этой роли функции по умолчанию.
* Типизированный клиент будет ограничен правами, предоставленными этой ролью.
@@ -362,7 +369,7 @@ export default defineApplication({
Когда вы генерируете новое приложение, CLI также создаёт файл роли по умолчанию. Используйте `defineRole()` для определения ролей со встроенной валидацией:
```typescript
// src/app/default-function.role.ts
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
@@ -401,10 +408,10 @@ export default defineRole({
});
```
`universalIdentifier` этой роли затем указывается в `application.config.ts` как `roleUniversalIdentifier`. Иными словами:
Значение `universalIdentifier` этой роли затем указывается в `application-config.ts` как `defaultRoleUniversalIdentifier`. Иными словами:
* **\*.role.ts** определяет, что может делать роль функции по умолчанию.
* **application.config.ts** указывает на эту роль, чтобы ваши функции наследовали её права.
* **application-config.ts** указывает на эту роль, чтобы ваши функции наследовали её права.
Заметки:
@@ -415,11 +422,11 @@ export default defineRole({
### Конфигурация логической функции и точка входа
Каждый файл функции использует `defineFunction()` для экспорта конфигурации с обработчиком и необязательными триггерами. Используйте суффикс файла `*.function.ts` для автоматического обнаружения.
Каждый файл функции использует `defineLogicFunction()` для экспорта конфигурации с обработчиком и необязательными триггерами.
```typescript
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
@@ -439,7 +446,7 @@ const handler = async (params: RoutePayload) => {
return result;
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
@@ -486,6 +493,55 @@ export default defineFunction({
* Массив `triggers` необязателен. Функции без триггеров можно использовать как вспомогательные, вызываемые другими функциями.
* Вы можете сочетать несколько типов триггеров в одной функции.
### Послеустановочные функции
Послеустановочная функция — это функция логики, которая автоматически выполняется после установки вашего приложения в рабочем пространстве. Это полезно для одноразовых задач настройки, таких как инициализация данных по умолчанию, создание начальных записей или настройка параметров рабочего пространства.
Когда вы создаёте каркас нового приложения с помощью `create-twenty-app`, для вас генерируется постустановочная функция по пути `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
Функция подключается к вашему приложению посредством ссылки на её универсальный идентификатор в `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Вы также можете вручную выполнить постустановочную функцию в любое время с помощью CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Основные моменты:
* Постустановочные функции — это стандартные логические функции: они используют `defineLogicFunction()` как и любые другие функции.
* Поле `postInstallLogicFunctionUniversalIdentifier` в `defineApplication()` является необязательным. Если его опустить, после установки никакая функция выполняться не будет.
* Тайм-аут по умолчанию установлен на 300 секунд (5 минут), чтобы позволить выполнять более длительные задачи настройки, такие как инициализация данных.
* Постустановочным функциям не нужны триггеры — платформа вызывает их во время установки или вручную через `function:execute --postInstall`.
### Полезная нагрузка триггера маршрута
<Warning>
@@ -515,7 +571,7 @@ export default defineFunction({
Когда триггер маршрута вызывает вашу логическую функцию, она получает объект `RoutePayload`, соответствующий формату AWS HTTP API v2. Импортируйте тип из `twenty-sdk`:
```typescript
import { defineFunction, type RoutePayload } from 'twenty-sdk';
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
@@ -545,7 +601,7 @@ const handler = async (event: RoutePayload) => {
По умолчанию HTTP-заголовки из входящих запросов **не** передаются в вашу логическую функцию по соображениям безопасности. Чтобы получить доступ к определённым заголовкам, явно перечислите их в массиве `forwardedRequestHeaders`:
```typescript
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
@@ -580,12 +636,114 @@ const handler = async (event: RoutePayload) => {
Вы можете создать новые функции двумя способами:
* **Сгенерировано**: Запустите `yarn entity:add` и выберите опцию добавления новой функции. Это создаёт стартовый файл с обработчиком и конфигурацией.
* **Вручную**: Создайте новый файл `*.function.ts` и используйте `defineFunction()`, следуя тому же шаблону.
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления новой функции логики. Это создаёт стартовый файл с обработчиком и конфигурацией.
* **Вручную**: Создайте новый файл `*.logic-function.ts` и используйте `defineLogicFunction()`, следуя тому же шаблону.
### Пометка логической функции как инструмента
Логические функции можно предоставлять как **инструменты** для ИИ-агентов и рабочих процессов. Когда функция помечена как инструмент, она становится доступной для ИИ Twenty и может быть выбрана в качестве шага в автоматизациях рабочих процессов.
Чтобы пометить логическую функцию как инструмент, установите `isTool: true` и укажите `toolInputSchema` для описания ожидаемых входных параметров с помощью [схемы JSON](https://json-schema.org/):
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
Основные моменты:
* **`isTool`** (`boolean`, по умолчанию: `false`): Если значение равно `true`, функция регистрируется как инструмент и становится доступной агентам ИИ и автоматизациям рабочих процессов.
* **`toolInputSchema`** (`object`, необязательно): Объект JSON Schema, который описывает параметры, которые принимает ваша функция. Агенты ИИ используют эту схему, чтобы понять, какие входные данные ожидает инструмент, и проверять корректность вызовов. Если опущено, по умолчанию используется схема `{ type: 'object', properties: {} }` (без параметров).
* Функции с `isTool: false` (или без указания) **не** выставляются как инструменты. Их по-прежнему можно выполнять напрямую или вызывать из других функций, но они не будут отображаться при обнаружении инструментов.
* **Именование инструмента**: При публикации как инструмента имя функции автоматически нормализуется до `logic_function_<name>` (в нижнем регистре, небуквенно-цифровые символы заменяются на подчёркивания). Например, `enrich-company` становится `logic_function_enrich_company`.
* Вы можете комбинировать `isTool` с триггерами — функция может одновременно быть инструментом (вызываемым агентами ИИ) и запускаться событиями (cron, события базы данных, маршруты).
<Note>
**Напишите хорошее описание в поле `description`.** Агенты ИИ опираются на поле `description` функции, чтобы решить, когда использовать инструмент. Чётко опишите, что делает инструмент и когда его следует вызывать.
</Note>
### Фронт-компоненты
Фронт-компоненты позволяют создавать пользовательские компоненты React, которые рендерятся внутри интерфейса Twenty. Используйте `defineFrontComponent()` для определения компонентов со встроенной валидацией:
```typescript
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
Основные моменты:
* Фронт-компоненты — это компоненты React, которые рендерятся в изолированных контекстах внутри Twenty.
* Используйте суффикс файла `*.front-component.tsx` для автоматического обнаружения.
* Поле `component` ссылается на ваш компонент React.
* Компоненты автоматически собираются и синхронизируются во время `yarn twenty app:dev`.
Вы можете создать новые фронт-компоненты двумя способами:
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового фронтенд-компонента.
* **Вручную**: Создайте новый файл `*.front-component.tsx` и используйте `defineFrontComponent()`.
### Сгенерированный типизированный клиент
Запустите yarn app:generate, чтобы создать локальный типизированный клиент в generated/ на основе схемы вашего рабочего пространства. Используйте его в своих функциях:
Типизированный клиент автоматически генерируется с помощью `yarn twenty app:dev` и сохраняется в `node_modules/twenty-sdk/generated` на основе схемы вашего рабочего пространства. Используйте его в своих функциях:
```typescript
import Twenty from '~/generated';
@@ -594,7 +752,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
Клиент повторно генерируется командой `yarn app:generate`. Запускайте повторно после изменения ваших объектов или при подключении к новому рабочему пространству.
Клиент автоматически перегенерируется с помощью `yarn twenty app:dev` при изменении ваших объектов или полей.
#### Учётные данные времени выполнения в логических функциях
@@ -606,49 +764,38 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
Заметки:
* Вам не нужно передавать URL или ключ API сгенерированному клиенту. Он читает `TWENTY_API_URL` и `TWENTY_API_KEY` из process.env во время выполнения.
* Права ключа API определяются ролью, на которую ссылается ваш `application.config.ts` через `roleUniversalIdentifier`. Это роль по умолчанию, используемая логическими функциями вашего приложения.
* Приложения могут определять роли, чтобы следовать принципу наименьших привилегий. Выдавайте только те права, которые нужны вашим функциям, затем укажите в `roleUniversalIdentifier` универсальный идентификатор этой роли.
* Права ключа API определяются ролью, на которую ссылается ваш `application-config.ts` через `defaultRoleUniversalIdentifier`. Это роль по умолчанию, используемая логическими функциями вашего приложения.
* Приложения могут определять роли, чтобы следовать принципу наименьших привилегий. Предоставляйте только те права, которые нужны вашим функциям, затем укажите в `defaultRoleUniversalIdentifier` универсальный идентификатор этой роли.
### Пример Hello World
Посмотрите минимальный сквозной пример, демонстрирующий объекты, функции и несколько триггеров, [здесь](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
Ознакомьтесь с минимальным сквозным примером, демонстрирующим объекты, логические функции, фронт-компоненты и несколько триггеров, [здесь](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Ручная настройка (без генератора)
Хотя мы рекомендуем использовать `create-twenty-app` для наилучшего старта, вы также можете настроить проект вручную. Не устанавливайте CLI глобально. Вместо этого добавьте `twenty-sdk` как локальную зависимость и настройте скрипты в вашем package.json:
Хотя мы рекомендуем использовать `create-twenty-app` для наилучшего старта, вы также можете настроить проект вручную. Не устанавливайте CLI глобально. Вместо этого добавьте `twenty-sdk` как локальную зависимость и настройте один скрипт в вашем package.json:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
Затем добавьте скрипты, подобные этим:
Затем добавьте скрипт `twenty`:
```json filename="package.json"
{
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
"twenty": "twenty"
}
}
```
Теперь вы можете запускать те же команды через Yarn, например, `yarn app:dev`, `yarn app:generate` и т. д.
Теперь вы можете запускать все команды через `yarn twenty <command>`, например, `yarn twenty app:dev`, `yarn twenty help` и т. д.
## Устранение неполадок
* Ошибки аутентификации: выполните `yarn auth:login` и убедитесь, что у вашего ключа API есть необходимые права.
* Ошибки аутентификации: выполните `yarn twenty auth:login` и убедитесь, что у вашего ключа API есть необходимые права.
* Не удаётся подключиться к серверу: проверьте URL API и доступность сервера Twenty.
* Типы или клиент отсутствуют/устарели: выполните `yarn app:generate`.
* Режим разработки не синхронизируется: убедитесь, что запущен `yarn app:dev`, и что ваша среда не игнорирует изменения.
* Types or client missing/outdated: restart `yarn twenty app:dev` — it auto-generates the typed client.
* Режим разработки не синхронизируется: убедитесь, что запущен `yarn twenty app:dev`, и что ваша среда не игнорирует изменения.
Канал помощи в Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -17,10 +17,6 @@ Uygulamalar, Twenty özelleştirmelerini **kod olarak** oluşturup yönetmenizi
* Özel tetikleyicilerle mantık fonksiyonları oluşturun
* Aynı uygulamayı birden çok çalışma alanına dağıtın
**Yakında:**
* Özel UI düzenleri ve bileşenleri
## Ön Gereksinimler
* Node.js 24+ ve Yarn 4
@@ -31,7 +27,7 @@ Uygulamalar, Twenty özelleştirmelerini **kod olarak** oluşturup yönetmenizi
Resmi scaffolder aracını kullanarak yeni bir uygulama oluşturun, ardından kimlik doğrulaması yapıp geliştirmeye başlayın:
```bash filename="Terminal"
# Scaffold a new app
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
@@ -40,32 +36,45 @@ corepack enable
yarn install
# Authenticate using your API key (you'll be prompted)
yarn auth:login
yarn twenty auth:login
# Start dev mode: automatically syncs local changes to your workspace
yarn app:dev
yarn twenty app:dev
```
İskelet oluşturucu, hangi örnek dosyaların dahil edileceğini kontrol etmek için üç modu destekler:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
# Interactive: select which examples to include
npx create-twenty-app@latest my-app --interactive
```
Buradan şunları yapabilirsiniz:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn entity:add
# Generate a typed Twenty client and workspace entity types
yarn app:generate
yarn twenty entity:add
# Watch your application's function logs
yarn function:logs
yarn twenty function:logs
# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn app:uninstall
yarn twenty app:uninstall
# Display commands' help
yarn help
yarn twenty help
```
Ayrıca bkz.: [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) ve [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk) için CLI başvuru sayfaları.
@@ -77,9 +86,9 @@ Ayrıca bkz.: [create-twenty-app](https://www.npmjs.com/package/create-twenty-ap
* Minimal bir temel uygulamayı `my-twenty-app/` içine kopyalar
* Yerel bir `twenty-sdk` bağımlılığı ve Yarn 4 yapılandırması ekler
* `twenty` CLI ile bağlantılı yapılandırma dosyaları ve betikler oluşturur
* Varsayılan bir uygulama yapılandırması ve varsayılan bir fonksiyon rolü üretir
* İskelet oluşturma moduna bağlı olarak çekirdek dosyaları (uygulama yapılandırması, varsayılan işlev rolü, kurulum sonrası işlev) ile örnek dosyaları üretir
Yeni şablondan oluşturulan bir uygulama şöyle görünür:
Varsayılan `--exhaustive` moduyla yeni oluşturulmuş bir uygulama şu şekilde görünür:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -93,92 +102,78 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Genel varlıklar klasörü (görseller, yazı tipleri vb.)
public/ # Public assets folder (images, fonts, etc.)
src/
application.config.ts # Gerekli - ana uygulama yapılandırması
default-function.role.ts # Sunucusuz işlevler için varsayılan rol
hello-world.function.ts # Örnek sunucusuz işlev
hello-world.front-component.tsx # Örnek ön uç bileşeni
// varlıklarınız (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # Default role for logic functions
├── objects/
│ └── example-object.ts # Example custom object definition
├── fields/
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
├── front-components/
│ └── hello-world.tsx # Example front component
├── views/
│ └── example-view.ts # Example saved view definition
└── navigation-menu-items/
└── example-navigation-menu-item.ts # Example sidebar navigation link
```
### Sözleşme-öncelikli yapılandırma
Uygulamalar, varlıkların dosya sonekiyle algılandığı **sözleşme-öncelikli yapılandırma** yaklaşımını kullanır. Bu, `src/app/` klasörü içinde esnek bir düzenlemeye olanak tanır:
| Dosya soneki | Varlık türü |
| ----------------------- | ----------------------------- |
| `*.object.ts` | Özel nesne tanımları |
| `*.function.ts` | Sunucusuz fonksiyon tanımları |
| `*.front-component.tsx` | Front component definitions |
| `*.role.ts` | Rol tanımları |
### Desteklenen klasör düzenleri
Varlıklarınızı şu desenlerden herhangi birine göre düzenleyebilirsiniz:
**Geleneksel (türe göre):**
```text
src/
├── application.config.ts
├── objects/
│ └── postCard.object.ts
├── functions/
│ └── createPostCard.function.ts
├── components/
│ └── card.front-component.tsx
└── roles/
└── admin.role.ts
```
**Özelliğe dayalı:**
```text
src/
├── application.config.ts
└── post-card/
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── postCardAdmin.role.ts
```
**Düz:**
```text
src/
├── application.config.ts
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── admin.role.ts
```
`--minimal` ile yalnızca çekirdek dosyalar oluşturulur (`application-config.ts`, `roles/default-role.ts` ve `logic-functions/post-install.ts`). `--interactive` ile hangi örnek dosyaların dahil edileceğini siz seçersiniz.
Genel hatlarıyla:
* **package.json**: Uygulama adını, sürümünü, motorları (Node 24+, Yarn 4) bildirir ve yerel `twenty` CLIsine yetki devreden `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` ve `auth:login` gibi betiklerin yanı sıra `twenty-sdk` ekler.
* **package.json**: Uygulama adını, sürümünü, motorları (Node 24+, Yarn 4) bildirir ve `twenty-sdk` ile yerel `twenty` CLI'sine yetki devreden bir `twenty` betiği ekler. Tüm mevcut komutları listelemek için `yarn twenty help` komutunu çalıştırın.
* **.gitignore**: `node_modules`, `.yarn`, `generated/` (türlendirilmiş istemci), `dist/`, `build/`, kapsam klasörleri, günlük dosyaları ve `.env*` dosyaları gibi yaygın artifaktları yok sayar.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Proje tarafından kullanılan Yarn 4 araç zincirini kilitler ve yapılandırır.
* **.nvmrc**: Projenin beklediği Node.js sürümünü sabitler.
* **eslint.config.mjs** ve **tsconfig.json**: Uygulamanızın TypeScript kaynakları için linting ve TypeScript yapılandırması sağlar.
* **README.md**: Uygulama kökünde temel talimatların yer aldığı kısa bir README.
* **public/**: Uygulamanızla birlikte sunulacak genel varlıkları (görseller, yazı tipleri, statik dosyalar) depolamak için bir klasör. Buraya yerleştirilen dosyalar senkronizasyon sırasında yüklenir ve çalışma zamanında erişilebilir olur.
* **src/**: Uygulamanızı kod olarak tanımladığınız ana yer:
* `application.config.ts`: Uygulamanız için genel yapılandırma (meta veriler ve çalışma zamanı bağlantıları). Aşağıda "Uygulama yapılandırması"na bakın.
* `*.role.ts`: Mantık fonksiyonlarınız tarafından kullanılan rol tanımları. Aşağıda "Varsayılan fonksiyon rolü"ne bakın.
* `*.object.ts`: Özel nesne tanımları.
* `*.function.ts`: Mantık fonksiyon tanımları.
* `*.front-component.tsx`: Ön bileşen tanımları.
* **src/**: Uygulamanızı kod olarak tanımladığınız ana yer
### Varlık algılama
SDK, TypeScript dosyalarınızı **`export default define<Entity>({...})`** çağrılarını arayarak ayrıştırıp varlıkları algılar. Her varlık türünün, `twenty-sdk` tarafından dışa aktarılan karşılık gelen bir yardımcı fonksiyonu vardır:
| Yardımcı fonksiyon | Varlık türü |
| ---------------------------- | ---------------------------------------- |
| `defineObject()` | Özel nesne tanımları |
| `defineLogicFunction()` | Mantık fonksiyon tanımları |
| `defineFrontComponent()` | Front component definitions |
| `defineRole()` | Rol tanımları |
| `defineField()` | Mevcut nesneler için alan genişletmeleri |
| `defineView()` | Kaydedilmiş görünüm tanımları |
| `defineNavigationMenuItem()` | Gezinme menüsü öğesi tanımları |
<Note>
**Dosya adlandırma esnektir.** Varlık algılama AST tabanlıdır — SDK, kaynak dosyalarınızı `export default define<Entity>({...})` desenini bulmak için tarar. Dosyalarınızı ve klasörlerinizi dilediğiniz gibi düzenleyebilirsiniz. Varlık türüne göre gruplama (örn. `logic-functions/`, `roles/`) bir gereklilik değil, yalnızca kod organizasyonu için bir gelenektir.
</Note>
Algılanan bir varlığa örnek:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
İlerideki komutlar daha fazla dosya ve klasör ekleyecektir:
* `yarn app:generate`, `generated/` klasörünü oluşturur (türlendirilmiş Twenty istemcisi + çalışma alanı türleri).
* `yarn entity:add`, özel nesneleriniz, fonksiyonlarınız, ön bileşenleriniz veya rolleriniz için `src/` altında varlık tanım dosyaları ekler.
* `yarn twenty app:dev`, `node_modules/twenty-sdk/generated` içinde tipli bir API istemcisini otomatik olarak oluşturur (tipli Twenty istemcisi + çalışma alanı türleri).
* `yarn twenty entity:add`, özel nesneleriniz, fonksiyonlarınız, ön bileşenleriniz veya rolleriniz için `src/` altında varlık tanım dosyaları ekler.
## Kimlik Doğrulama
`yarn auth:login` komutunu ilk kez çalıştırdığınızda, sizden şunlar istenir:
`yarn twenty auth:login` komutunu ilk kez çalıştırdığınızda, sizden şunlar istenir:
* API URLsi (varsayılan: http://localhost:3000 veya mevcut çalışma alanı profiliniz)
* API anahtarı
@@ -188,26 +183,26 @@ Kimlik bilgileriniz kullanıcı başına `~/.twenty/config.json` içinde saklan
### Managing workspaces
```bash filename="Terminal"
# Login interactively (recommended)
yarn auth:login
# Etkileşimli giriş yapın (önerilir)
yarn twenty auth:login
# Login to a specific workspace profile
yarn auth:login --workspace my-custom-workspace
# Belirli bir çalışma alanı profiline giriş yapın
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn auth:list
# Yapılandırılmış tüm çalışma alanlarını listeleyin
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn auth:switch
# Varsayılan çalışma alanını değiştirin (etkileşimli)
yarn twenty auth:switch
# Switch to a specific workspace
yarn auth:switch production
# Belirli bir çalışma alanına geçin
yarn twenty auth:switch production
# Check current authentication status
yarn auth:status
# Mevcut kimlik doğrulama durumunu kontrol edin
yarn twenty auth:status
```
Once you've switched workspaces with `auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
`yarn twenty auth:switch` ile çalışma alanlarını değiştirdikten sonra, sonraki tüm komutlar varsayılan olarak o çalışma alanını kullanacaktır. You can still override it temporarily with `--workspace <name>`.
## SDK kaynaklarını kullanın (türler ve yapılandırma)
@@ -215,16 +210,20 @@ twenty-sdk, uygulamanız içinde kullandığınız türlendirilmiş yapı taşla
### Yardımcı fonksiyonlar
SDK, uygulama varlıklarınızı tanımlamak için yerleşik doğrulamaya sahip dört yardımcı fonksiyon sunar:
SDK, uygulama varlıklarınızı tanımlamak için yardımcı fonksiyonlar sağlar. [Varlık algılama](#entity-detection) bölümünde açıklandığı gibi, varlıklarınızın algılanması için `export default define<Entity>({...})` kullanmalısınız:
| Fonksiyon | Amaç |
| --------------------- | ---------------------------------------------- |
| `defineApplication()` | Uygulama meta verilerini yapılandırın |
| `defineObject()` | Alanlara sahip özel nesneler tanımlayın |
| `defineFunction()` | İşleyicilerle mantık fonksiyonları tanımlayın |
| `defineRole()` | Rol izinlerini ve nesne erişimini yapılandırın |
| Fonksiyon | Amaç |
| ---------------------------- | ------------------------------------------------------------------------- |
| `defineApplication()` | Uygulama meta verilerini yapılandırın (zorunlu, uygulama başına bir adet) |
| `defineObject()` | Alanlara sahip özel nesneler tanımlayın |
| `defineLogicFunction()` | İşleyicilerle mantık fonksiyonları tanımlayın |
| `defineFrontComponent()` | Özel kullanıcı arayüzü için ön uç bileşenlerini tanımlayın |
| `defineRole()` | Rol izinlerini ve nesne erişimini yapılandırın |
| `defineField()` | Mevcut nesneleri ek alanlarla genişletin |
| `defineView()` | Nesneler için kaydedilmiş görünümler tanımlayın |
| `defineNavigationMenuItem()` | Kenar çubuğu gezinme bağlantılarını tanımlayın |
Bu fonksiyonlar, yapılandırmanızı çalışma anında doğrular ve daha iyi IDE otomatik tamamlama ve tür güvenliği sağlar.
Bu fonksiyonlar, derleme zamanında yapılandırmanızı doğrular ve IDE otomatik tamamlama ile tür güvenliği sağlar.
### Nesnelerin tanımlanması
@@ -305,26 +304,32 @@ export default defineObject({
* `universalIdentifier` dağıtımlar arasında benzersiz ve kararlı olmalıdır.
* Her alan bir `name`, `type`, `label` ve kendi kararlı `universalIdentifier` değerini gerektirir.
* `fields` dizisi isteğe bağlıdır — özel alanlar olmadan da nesneler tanımlayabilirsiniz.
* `yarn entity:add` kullanarak, adlandırma, alanlar ve ilişkiler konusunda sizi yönlendirerek yeni nesneler oluşturabilirsiniz.
* `yarn twenty entity:add` kullanarak, adlandırma, alanlar ve ilişkiler konusunda sizi yönlendirerek yeni nesneler oluşturabilirsiniz.
<Note>
**Temel alanlar otomatik olarak oluşturulur.** Özel bir nesne tanımladığınızda Twenty, `name`, `createdAt`, `updatedAt`, `createdBy`, `position` ve `deletedAt` gibi standart alanları otomatik olarak ekler. Bunları `fields` dizinizde tanımlamanız gerekmez — yalnızca özel alanlarınızı ekleyin.
**Temel alanlar otomatik olarak oluşturulur.** Özel bir nesne tanımladığınızda Twenty, standart alanları otomatik olarak ekler
örneğin `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` ve `deletedAt`.
Bunları `fields` dizinizde tanımlamanız gerekmez — yalnızca özel alanlarınızı ekleyin.
`fields` dizinizde aynı ada sahip bir alan tanımlayarak varsayılan alanları geçersiz kılabilirsiniz,
ancak bu önerilmez.
</Note>
### Uygulama yapılandırması (application.config.ts)
### Uygulama yapılandırması (application-config.ts)
Her uygulamanın aşağıdakileri açıklayan tek bir `application.config.ts` dosyası vardır:
Her uygulamanın aşağıdakileri açıklayan tek bir `application-config.ts` dosyası vardır:
* **Uygulamanın kim olduğu**: tanımlayıcılar, görünen ad ve açıklama.
* **Fonksiyonlarının nasıl çalıştığı**: izinler için hangi rolü kullandıkları.
* **(İsteğe bağlı) değişkenler**: fonksiyonlarınıza ortam değişkenleri olarak sunulan anahtardeğer çiftleri.
* **(İsteğe bağlı) kurulum sonrası işlev**: uygulama yüklendikten sonra çalışan bir mantık işlevi.
Uygulama yapılandırmanızı tanımlamak için `defineApplication()` kullanın:
```typescript
// src/app/application.config.ts
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -339,7 +344,8 @@ export default defineApplication({
isSecret: false,
},
},
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -347,11 +353,12 @@ Notlar:
* `universalIdentifier` alanları size ait belirleyici kimliklerdir; bunları bir kez oluşturun ve eşitlemeler boyunca kararlı tutun.
* `applicationVariables`, fonksiyonlarınız için ortam değişkenlerine dönüşür (örneğin, `DEFAULT_RECIPIENT_NAME` değeri `process.env.DEFAULT_RECIPIENT_NAME` olarak kullanılabilir).
* `roleUniversalIdentifier`, `*.role.ts` dosyanızda tanımladığınız rolle eşleşmelidir (aşağıya bakın).
* `defaultRoleUniversalIdentifier`, rol dosyasıyla eşleşmelidir (aşağıya bakın).
* `postInstallLogicFunctionUniversalIdentifier` (isteğe bağlı), uygulama yüklendikten sonra otomatik olarak çalışan bir mantık işlevine işaret eder. Bkz. [Kurulum sonrası işlevler](#post-install-functions).
#### Roller ve izinler
Uygulamalar, çalışma alanınızdaki nesneler ve eylemler üzerindeki izinleri kapsülleyen roller tanımlayabilir. `application.config.ts` içindeki `roleUniversalIdentifier` alanı, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan rolü belirtir.
Uygulamalar, çalışma alanınızdaki nesneler ve eylemler üzerindeki izinleri kapsülleyen roller tanımlayabilir. `application-config.ts` içindeki `defaultRoleUniversalIdentifier` alanı, uygulamanızın mantık fonksiyonlarının kullandığı varsayılan rolü belirtir.
* `TWENTY_API_KEY` olarak enjekte edilen çalışma zamanı API anahtarı bu varsayılan fonksiyon rolünden türetilir.
* Türlendirilmiş istemci, o role tanınan izinlerle sınırlandırılır.
@@ -362,7 +369,7 @@ Uygulamalar, çalışma alanınızdaki nesneler ve eylemler üzerindeki izinleri
Yeni bir uygulama oluşturduğunuzda CLI ayrıca varsayılan bir rol dosyası da oluşturur. Yerleşik doğrulamayla roller tanımlamak için `defineRole()` kullanın:
```typescript
// src/app/default-function.role.ts
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
@@ -401,10 +408,10 @@ export default defineRole({
});
```
Bu rolün `universalIdentifier` değeri, `application.config.ts` içinde `roleUniversalIdentifier` olarak referans verilir. Başka bir deyişle:
Bu rolün `universalIdentifier` değeri daha sonra `application-config.ts` içinde `defaultRoleUniversalIdentifier` olarak referans verilir. Başka bir deyişle:
* **\*.role.ts**, varsayılan fonksiyon rolünün neler yapabileceğini tanımlar.
* **application.config.ts** bu role işaret eder, böylece fonksiyonlarınız onun izinlerini devralır.
* **application-config.ts**, fonksiyonlarınızın izinlerini devralması için bu role işaret eder.
Notlar:
@@ -415,11 +422,11 @@ Notlar:
### Mantık fonksiyon yapılandırması ve giriş noktası
Her fonksiyon dosyası, bir işleyici ve isteğe bağlı tetikleyiciler içeren bir yapılandırmayı dışa aktarmak için `defineFunction()` kullanır. Otomatik algılama için `*.function.ts` dosya soneğini kullanın.
Her fonksiyon dosyası, bir işleyici ve isteğe bağlı tetikleyiciler içeren bir yapılandırmayı dışa aktarmak için `defineLogicFunction()` kullanır.
```typescript
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
@@ -439,7 +446,7 @@ const handler = async (params: RoutePayload) => {
return result;
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
@@ -486,6 +493,55 @@ Notlar:
* `triggers` dizisi isteğe bağlıdır. Tetikleyicisi olmayan fonksiyonlar, diğer fonksiyonlar tarafından çağrılan yardımcı fonksiyonlar olarak kullanılabilir.
* Tek bir fonksiyonda birden çok tetikleyici türünü birleştirebilirsiniz.
### Kurulum sonrası işlevler
Kurulum sonrası işlev, uygulamanız bir çalışma alanına yüklendikten sonra otomatik olarak çalışan bir mantık işlevidir. Bu, varsayılan verileri tohumlama, ilk kayıtları oluşturma veya çalışma alanı ayarlarını yapılandırma gibi tek seferlik kurulum görevleri için yararlıdır.
`create-twenty-app` ile yeni bir uygulama iskeleti oluşturduğunuzda, `src/logic-functions/post-install.ts` konumunda sizin için bir kurulum sonrası işlevi oluşturulur:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
İşlev, `application-config.ts` içinde evrensel tanımlayıcısına başvurularak uygulamanıza bağlanır:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Ayrıca kurulum sonrası işlevi istediğiniz zaman CLI kullanarak manuel olarak çalıştırabilirsiniz:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Önemli noktalar:
* Kurulum sonrası işlevleri standart mantık işlevleridir — diğer herhangi bir işlev gibi `defineLogicFunction()` kullanırlar.
* `defineApplication()` içindeki `postInstallLogicFunctionUniversalIdentifier` alanı isteğe bağlıdır. Atlanırsa, kurulumdan sonra hiçbir işlev çalıştırılmaz.
* Varsayılan zaman aşımı, veri tohumlama gibi daha uzun kurulum görevlerine izin vermek için 300 saniye (5 dakika) olarak ayarlanmıştır.
* Kurulum sonrası işlevlerin tetikleyicilere ihtiyacı yoktur — kurulum sırasında platform tarafından veya `function:execute --postInstall` aracılığıyla manuel olarak çağrılırlar.
### Rota tetikleyicisi yükü
<Warning>
@@ -515,7 +571,7 @@ Notlar:
Bir rota tetikleyicisi mantık fonksiyonunuzu çağırdığında, AWS HTTP API v2 formatını izleyen bir `RoutePayload` nesnesi alır. Türü `twenty-sdk` içinden içe aktarın:
```typescript
import { defineFunction, type RoutePayload } from 'twenty-sdk';
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
@@ -545,7 +601,7 @@ const handler = async (event: RoutePayload) => {
Varsayılan olarak, güvenlik nedenleriyle gelen isteklerden HTTP başlıkları mantık fonksiyonunuza **aktarılmaz**. Belirli başlıklara erişmek için bunları açıkça `forwardedRequestHeaders` dizisinde listeleyin:
```typescript
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
@@ -580,12 +636,114 @@ const handler = async (event: RoutePayload) => {
Yeni fonksiyonları iki şekilde oluşturabilirsiniz:
* **Şablondan**: `yarn entity:add` çalıştırın ve yeni bir fonksiyon ekleme seçeneğini seçin. Bu, bir işleyici ve yapılandırma içeren bir başlangıç dosyası oluşturur.
* **Manuel**: Yeni bir `*.function.ts` dosyası oluşturun ve aynı deseni izleyerek `defineFunction()` kullanın.
* **Şablondan**: `yarn twenty entity:add` çalıştırın ve yeni bir mantık fonksiyonu ekleme seçeneğini seçin. Bu, bir işleyici ve yapılandırma içeren bir başlangıç dosyası oluşturur.
* **Manuel**: Yeni bir `*.logic-function.ts` dosyası oluşturun ve aynı deseni izleyerek `defineLogicFunction()` kullanın.
### Bir mantık işlevini araç olarak işaretleme
Mantık işlevleri, yapay zeka ajanları ve iş akışları için **araçlar** olarak sunulabilir. Bir işlev bir araç olarak işaretlendiğinde, Twenty'nin yapay zeka özellikleri tarafından keşfedilebilir hâle gelir ve iş akışı otomasyonlarında bir adım olarak seçilebilir.
Bir mantık işlevini bir araç olarak işaretlemek için `isTool: true` olarak ayarlayın ve beklenen giriş parametrelerini açıklayan bir `toolInputSchema`yı [JSON Şeması](https://json-schema.org/) kullanarak sağlayın:
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
Önemli noktalar:
* **`isTool`** (`boolean`, varsayılan: `false`): `true` olarak ayarlandığında, işlev bir araç olarak kaydedilir ve AI ajanları ile iş akışı otomasyonları tarafından kullanılabilir hale gelir.
* **`toolInputSchema`** (`object`, isteğe bağlı): İşlevinizin kabul ettiği parametreleri tanımlayan bir JSON Schema nesnesi. AI ajanları, aracın hangi girdileri beklediğini anlamak ve çağrıları doğrulamak için bu şemayı kullanır. Atlanırsa, şema varsayılan olarak `{ type: 'object', properties: {} }` olur (parametre yok).
* `isTool: false` (veya ayarlanmamış) olan işlevler araç olarak **sunulmaz**. Yine de doğrudan yürütülebilir veya diğer işlevler tarafından çağrılabilirler, ancak araç keşfinde görünmezler.
* **Araç adlandırma**: Bir araç olarak sunulduğunda, işlev adı otomatik olarak `logic_function_<name>` biçimine dönüştürülür (küçük harfe çevrilir, alfasayısal olmayan karakterler alt çizgi ile değiştirilir). Örneğin, `enrich-company` `logic_function_enrich_company` haline gelir.
* `isTool` özelliğini tetikleyicilerle birleştirebilirsiniz — bir işlev aynı anda hem bir araç (AI ajanları tarafından çağrılabilir) olabilir hem de olaylar tarafından tetiklenebilir (cron, veritabanı olayları, routes).
<Note>
**İyi bir `description` yazın.** AI ajanları, aracı ne zaman kullanacaklarına karar vermek için işlevin `description` alanına güvenir. Aracın ne yaptığını ve ne zaman çağrılması gerektiğini açıkça belirtin.
</Note>
### Ön uç bileşenleri
Ön uç bileşenleri, Twenty'nin kullanıcı arayüzünde görüntülenen özel React bileşenleri oluşturmanıza olanak tanır. Yerleşik doğrulamayla bileşenleri tanımlamak için `defineFrontComponent()` kullanın:
```typescript
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
Önemli noktalar:
* Ön uç bileşenleri, Twenty içinde yalıtılmış bağlamlarda görüntülenen React bileşenleridir.
* Otomatik algılama için `*.front-component.tsx` dosya soneğini kullanın.
* `component` alanı, React bileşeninize referans verir.
* Bileşenler, `yarn twenty app:dev` sırasında otomatik olarak oluşturulur ve senkronize edilir.
Yeni ön uç bileşenlerini iki şekilde oluşturabilirsiniz:
* **Şablondan**: `yarn twenty entity:add` çalıştırın ve yeni bir ön uç bileşeni ekleme seçeneğini seçin.
* **Manuel**: Yeni bir `*.front-component.tsx` dosyası oluşturun ve `defineFrontComponent()` kullanın.
### Oluşturulmuş türlendirilmiş istemci
Çalışma alanı şemanıza göre generated/ içinde yerel bir türlendirilmiş istemci oluşturmak için yarn app:generate çalıştırın. Fonksiyonlarınızda kullanın:
Tipli istemci, `yarn twenty app:dev` tarafından otomatik olarak oluşturulur ve çalışma alanı şemanıza göre `node_modules/twenty-sdk/generated` içine kaydedilir. Fonksiyonlarınızda kullanın:
```typescript
import Twenty from '~/generated';
@@ -594,7 +752,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
İstemci `yarn app:generate` tarafından yeniden oluşturulur. Nesnelerinizi değiştirdikten sonra veya yeni bir çalışma alanına katılırken yeniden çalıştırın.
Nesneleriniz veya alanlarınız değiştiğinde, istemci `yarn twenty app:dev` tarafından otomatik olarak yeniden oluşturulur.
#### Mantık fonksiyonlarında çalışma zamanı kimlik bilgileri
@@ -606,49 +764,38 @@ Fonksiyonunuz Twenty üzerinde çalıştığında, platform kodunuz yürütülme
Notlar:
* Oluşturulan istemciye URL veya API anahtarı geçirmeniz gerekmez. Çalışma zamanında `TWENTY_API_URL` ve `TWENTY_API_KEY` değerlerini process.env üzerinden okur.
* API anahtarının izinleri, `application.config.ts` içinde `roleUniversalIdentifier` aracılığıyla referans verilen role göre belirlenir. Bu, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan roldür.
* Uygulamalar, en az ayrıcalık ilkesini izlemek için roller tanımlayabilir. Yalnızca fonksiyonlarınızın ihtiyaç duyduğu izinleri verin ve ardından `roleUniversalIdentifier` değerini o rolün evrensel tanımlayıcısına yönlendirin.
* API anahtarının izinleri, `application-config.ts` içinde `defaultRoleUniversalIdentifier` aracılığıyla referans verilen role göre belirlenir. Bu, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan roldür.
* Uygulamalar, en az ayrıcalık ilkesini izlemek için roller tanımlayabilir. Yalnızca fonksiyonlarınızın ihtiyaç duyduğu izinleri verin ve ardından `defaultRoleUniversalIdentifier` değerini o rolün evrensel tanımlayıcısına yönlendirin.
### Hello World örneği
Nesneleri, fonksiyonları ve birden çok tetikleyiciyi gösteren minimal, uçtan uca bir örneği [buradan](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world) inceleyin:
Nesneleri, mantık fonksiyonlarını, ön uç bileşenlerini ve birden çok tetikleyiciyi gösteren minimal, uçtan uca bir örneği [buradan](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world) inceleyin:
## Manuel kurulum (scaffolder olmadan)
En iyi başlangıç deneyimi için `create-twenty-app` kullanmanızı önersek de, bir projeyi manuel olarak da kurabilirsiniz. CLI'yi global olarak kurmayın. Bunun yerine `twenty-sdk`'yi yerel bir bağımlılık olarak ekleyin ve package.json içinde betikleri bağlayın:
En iyi başlangıç deneyimi için `create-twenty-app` kullanmanızı önersek de, bir projeyi manuel olarak da kurabilirsiniz. CLI'yi global olarak kurmayın. Bunun yerine `twenty-sdk`'yi yerel bir bağımlılık olarak ekleyin ve package.json içinde tek bir betik tanımlayın:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
Ardından şu gibi betikler ekleyin:
Ardından bir `twenty` betiği ekleyin:
```json filename="package.json"
{
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
"twenty": "twenty"
}
}
```
Artık aynı komutları Yarn üzerinden çalıştırabilirsiniz; örn. `yarn app:dev`, `yarn app:generate` vb.
Artık tüm komutları `yarn twenty <command>` üzerinden çalıştırabilirsiniz; örn. `yarn twenty app:dev`, `yarn twenty help` vb.
## Sorun Giderme
* Kimlik doğrulama hataları: `yarn auth:login` çalıştırın ve API anahtarınızın gerekli izinlere sahip olduğundan emin olun.
* Kimlik doğrulama hataları: `yarn twenty auth:login` çalıştırın ve API anahtarınızın gerekli izinlere sahip olduğundan emin olun.
* Sunucuya bağlanılamıyor: API URLsini ve Twenty sunucusunun erişilebilir olduğunu doğrulayın.
* Türler veya istemci eksik/eski: `yarn app:generate` çalıştırın.
* Geliştirme modu eşitlenmiyor: `yarn app:dev`'in çalıştığından ve değişikliklerin ortamınız tarafından yok sayılmadığından emin olun.
* Türler veya istemci eksik/eski: `yarn twenty app:dev` komutunu yeniden çalıştırın — tip tanımlı istemciyi otomatik olarak oluşturur.
* Geliştirme modu eşitlenmiyor: `yarn twenty app:dev`'in çalıştığından ve değişikliklerin ortamınız tarafından yok sayılmadığından emin olun.
Discord Yardım Kanalı: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -17,10 +17,6 @@ description: 以代码的形式构建并管理 Twenty 自定义项。
* 构建带有自定义触发器的逻辑函数
* 将同一个应用部署到多个工作空间
**即将推出:**
* 自定义 UI 布局和组件
## 先决条件
* Node.js 24+ 和 Yarn 4
@@ -31,41 +27,54 @@ description: 以代码的形式构建并管理 Twenty 自定义项。
使用官方脚手架创建一个新应用,然后进行身份验证并开始开发:
```bash filename="Terminal"
# 搭建一个新应用
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# 如果你不使用 yarn@4
# If you don't use yarn@4
corepack enable
yarn install
# 使用你的 API 密钥进行身份验证(系统会提示你)
yarn auth:login
# Authenticate using your API key (you'll be prompted)
yarn twenty auth:login
# 启动开发模式:会将本地更改自动同步到你的工作区
yarn app:dev
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty app:dev
```
The scaffolder supports three modes for controlling which example files are included:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
# Interactive: select which examples to include
npx create-twenty-app@latest my-app --interactive
```
从这里您可以:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn entity:add
# 向你的应用添加一个新实体(引导式)
yarn twenty entity:add
# Generate a typed Twenty client and workspace entity types
yarn app:generate
# 监听你的应用函数日志
yarn twenty function:logs
# Watch your application's function logs
yarn function:logs
# 按名称执行一个函数
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
# Execute a function by name
yarn function:execute -n my-function -p '{\"name\": \"test\"}'
# 执行安装后函数
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn app:uninstall
# 从当前工作区卸载该应用
yarn twenty app:uninstall
# Display commands' help
yarn help
# 显示命令帮助
yarn twenty help
```
另请参阅:[create-twenty-app](https://www.npmjs.com/package/create-twenty-app) 和 [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk) 的 CLI 参考页面。
@@ -77,9 +86,9 @@ yarn help
* 将一个最小的基础应用复制到 `my-twenty-app/` 中
* 添加本地 `twenty-sdk` 依赖和 Yarn 4 配置
* 创建与 `twenty` CLI 关联的配置文件和脚本
* 生成默认的应用配置和默认的函数角色
* Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
一个新生成的脚手架应用如下所示:
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -93,92 +102,78 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # 公共资源文件夹(图像、字体等)
public/ # Public assets folder (images, fonts, etc.)
src/
application.config.ts # 必需 - 主应用程序配置
default-function.role.ts # 用于无服务器函数的默认角色
hello-world.function.ts # 示例无服务器函数
hello-world.front-component.tsx # 示例前端组件
// 你的实体 (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
├── application-config.ts # Required - main application configuration
├── roles/
└── default-role.ts # Default role for logic functions
├── objects/
│ └── example-object.ts # Example custom object definition
├── fields/
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
├── front-components/
│ └── hello-world.tsx # Example front component
├── views/
│ └── example-view.ts # Example saved view definition
└── navigation-menu-items/
└── example-navigation-menu-item.ts # Example sidebar navigation link
```
### 约定优于配置
应用采用**约定优于配置**的方式,根据文件后缀检测实体。 这使得可以在 `src/app/` 文件夹内灵活组织:
| 文件后缀 | 实体类型 |
| ----------------------- | -------- |
| `*.object.ts` | 自定义对象定义 |
| `*.function.ts` | 无服务器函数定义 |
| `*.front-component.tsx` | 前端组件定义 |
| `*.role.ts` | 角色定义 |
### 支持的文件夹组织方式
你可以按以下任一模式组织实体:
**传统(按类型):**
```text
src/
├── application.config.ts
├── objects/
│ └── postCard.object.ts
├── functions/
│ └── createPostCard.function.ts
├── components/
│ └── card.front-component.tsx
└── roles/
└── admin.role.ts
```
**基于特性:**
```text
src/
├── application.config.ts
└── post-card/
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── postCardAdmin.role.ts
```
**扁平:**
```text
src/
├── application.config.ts
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── admin.role.ts
```
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
总体来说:
* **package.json**:声明应用名称、版本、运行时Node 24+、Yarn 4),并添加 `twenty-sdk`以及诸如 `app:dev`、`app:generate`、`entity:add`、`function:logs`、`function:execute`、`app:uninstall` 和 `auth:login` 等脚本,这些脚本会委托给本地的 `twenty` CLI。
* **package.json**:声明应用名称、版本、引擎Node 24+、Yarn 4),并添加 `twenty-sdk` 以及一个 `twenty` 脚本,脚本会委托给本地的 `twenty` CLI。 运行 `yarn twenty help` 以列出所有可用命令。
* **.gitignore**:忽略常见产物,如 `node_modules`、`.yarn`、`generated/`(类型化客户端)、`dist/`、`build/`、覆盖率文件夹、日志文件以及 `.env*` 文件。
* **yarn.lock**、**.yarnrc.yml**、**.yarn/**:锁定并配置项目使用的 Yarn 4 工具链。
* **.nvmrc**:固定项目期望的 Node.js 版本。
* **eslint.config.mjs** 和 **tsconfig.json**:为应用的 TypeScript 源码提供 Lint 与 TypeScript 配置。
* **README.md**:应用根目录中的简短 README,包含基本说明。
* **public/**: 一个用于存储公共资源(图像、字体、静态文件)的文件夹,这些资源将随你的应用程序一起提供。 放置在此处的文件会在同步期间上传,并可在运行时访问。
* **src/**:你以代码形式定义应用的主要位置
* `application.config.ts`:应用的全局配置(元数据和运行时关联)。 参见下方“应用配置”。
* `*.role.ts`:你的逻辑函数所使用的角色定义。 参见下方“默认函数角色”。
* `*.object.ts`:自定义对象定义。
* `*.function.ts`:逻辑函数定义。
* `*.front-component.tsx`:前端组件定义。
* **src/**:你以代码形式定义应用的主要位置
### 实体检测
该 SDK 通过在你的 TypeScript 文件中解析 **`export default define<Entity>({...})`** 调用来检测实体。 每种实体类型都有一个从 `twenty-sdk` 导出的对应辅助函数:
| 辅助函数 | 实体类型 |
| ---------------------------- | -------------------------------- |
| `defineObject()` | 自定义对象定义 |
| `defineLogicFunction()` | 逻辑函数定义 |
| `defineFrontComponent()` | 前端组件定义 |
| `defineRole()` | 角色定义 |
| `defineField()` | 现有对象的字段扩展 |
| `defineView()` | Saved view definitions |
| `defineNavigationMenuItem()` | Navigation menu item definitions |
<Note>
**文件命名是灵活的。** 实体检测基于 AST — SDK 会扫描你的源文件以查找 `export default define<Entity>({...})` 模式。 你可以按照自己的喜好组织文件和文件夹。 按实体类型分组(例如 `logic-functions/`、`roles/`)只是代码组织的一种约定,并非必需。
</Note>
已检测实体的示例:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
后续命令将添加更多文件和文件夹:
* `yarn app:generate` 将创建一个 `generated/` 文件夹(类型化 Twenty 客户端 + 工作空间类型)。
* `yarn entity:add` 会在 `src/` 下为你的自定义对象、函数、前端组件或角色添加实体定义文件。
* `yarn twenty app:dev` 将在 `node_modules/twenty-sdk/generated` 中自动生成一个类型化的 API 客户端(类型化 Twenty 客户端 + 工作类型)。
* `yarn twenty entity:add` 会在 `src/` 下为你的自定义对象、函数、前端组件或角色添加实体定义文件。
## 身份验证
首次运行 `yarn auth:login` 时,你将被提示输入:
首次运行 `yarn twenty auth:login` 时,你将被提示输入:
* API URL(默认为 http://localhost:3000 或你当前的工作空间配置)
* API 密钥
@@ -189,25 +184,25 @@ src/
```bash filename="Terminal"
# Login interactively (recommended)
yarn auth:login
yarn twenty auth:login
# Login to a specific workspace profile
yarn auth:login --workspace my-custom-workspace
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn auth:list
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn auth:switch
yarn twenty auth:switch
# Switch to a specific workspace
yarn auth:switch production
yarn twenty auth:switch production
# Check current authentication status
yarn auth:status
yarn twenty auth:status
```
使用 `auth:switch` 切换工作空间后,后续所有命令将默认使用该工作空间。 你仍可通过 `--workspace <name>` 临时覆盖。
使用 `yarn twenty auth:switch` 切换工作空间后,后续所有命令将默认使用该工作空间。 你仍可通过 `--workspace <name>` 临时覆盖。
## 使用 SDK 资源(类型与配置)
@@ -215,16 +210,20 @@ twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以
### 辅助函数
该 SDK 提供四个带内置校验的辅助函数用于定义你的应用实体:
该 SDK 提供辅助函数用于定义你的应用实体。 如 [实体检测](#entity-detection) 中所述,你必须使用 `export default define<Entity>({...})` 才能让你的实体被检测到
| 函数 | 目的 |
| --------------------- | ------------ |
| `defineApplication()` | 配置应用元数据 |
| `defineObject()` | 定义带字段的自定义对象 |
| `defineFunction()` | 定义带处理程序的逻辑函数 |
| `defineRole()` | 配置角色权限和对象访问 |
| 函数 | 目的 |
| ---------------------------- | ------------------------------- |
| `defineApplication()` | 配置应用元数据(必需,每个应用一个) |
| `defineObject()` | 定义带字段的自定义对象 |
| `defineLogicFunction()` | 定义带处理程序的逻辑函数 |
| `defineFrontComponent()` | 为自定义 UI 定义前端组件 |
| `defineRole()` | 配置角色权限和对象访问 |
| `defineField()` | 为现有对象扩展额外字段 |
| `defineView()` | Define saved views for objects |
| `defineNavigationMenuItem()` | Define sidebar navigation links |
这些函数会在运行时校验你的配置,并提供更好的 IDE 自动补全和类型安全。
这些函数会在构建时校验你的配置,并提供 IDE 自动补全和类型安全。
### 定义对象
@@ -305,26 +304,32 @@ export default defineObject({
* `universalIdentifier` 必须在各次部署间保持唯一且稳定。
* 每个字段都需要 `name`、`type`、`label` 以及其自身稳定的 `universalIdentifier`。
* `fields` 数组是可选的——你可以定义没有自定义字段的对象。
* 你可以使用 `yarn entity:add` 脚手架创建新对象,它会引导你完成命名、字段和关系。
* 你可以使用 `yarn twenty entity:add` 脚手架创建新对象,它会引导你完成命名、字段和关系。
<Note>
**基础字段会自动创建。** 当你定义自定义对象时,Twenty 会自动添加 `name`、`createdAt`、`updatedAt`、`createdBy`、`position`、`deletedAt` 等标准字段。 你无需在 `fields` 数组中定义这些字段——只需添加你的自定义字段。
**基础字段会自动创建。** 当你定义自定义对象时,Twenty 会自动添加标准字段
例如 `id`、`name`、`createdAt`、`updatedAt`、`createdBy`、`updatedBy` 和 `deletedAt`。
你无需在 `fields` 数组中定义这些字段——只需添加你的自定义字段。
你可以通过在你的 `fields` 数组中定义一个同名字段来覆盖默认字段,
但不建议这样做。
</Note>
### 应用配置(application.config.ts
### 应用配置(application-config.ts
每个应用都有一个 `application.config.ts` 文件,用于描述:
每个应用都有一个 `application-config.ts` 文件,用于描述:
* **应用的身份**:标识符、显示名称和描述。
* **函数如何运行**:它们用于权限的角色。
* **(可选)变量**:以环境变量形式提供给函数的键值对。
* **(可选)安装后函数**:在应用安装后运行的逻辑函数。
使用 `defineApplication()` 定义你的应用配置:
```typescript
// src/app/application.config.ts
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -339,7 +344,8 @@ export default defineApplication({
isSecret: false,
},
},
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -347,11 +353,12 @@ export default defineApplication({
* `universalIdentifier` 字段是你拥有的确定性 ID;生成一次并在多次同步中保持稳定。
* `applicationVariables` 会变成函数可用的环境变量(例如,`DEFAULT_RECIPIENT_NAME` 可作为 `process.env.DEFAULT_RECIPIENT_NAME` 使用)。
* `roleUniversalIdentifier` 必须与在 `*.role.ts` 文件中定义的角色一致(见下文)。
* `defaultRoleUniversalIdentifier` 必须与角色文件一致(见下文)。
* `postInstallLogicFunctionUniversalIdentifier`(可选)指向一个在应用安装后自动运行的逻辑函数。 参见 [安装后函数](#post-install-functions)。
#### 角色和权限
应用可以定义角色,以封装对工作空间对象与操作的权限。 `application.config.ts` 中的 `roleUniversalIdentifier` 字段指定你的应用逻辑函数所使用的默认角色。
应用可以定义角色,以封装对工作空间对象与操作的权限。 `application-config.ts` 中的 `defaultRoleUniversalIdentifier` 字段指定你的应用逻辑函数所使用的默认角色。
* 作为 `TWENTY_API_KEY` 注入的运行时 API 密钥源自该默认函数角色。
* 类型化客户端将受限于该角色授予的权限。
@@ -362,7 +369,7 @@ export default defineApplication({
当你脚手架生成新应用时,CLI 也会创建一个默认角色文件。 使用 `defineRole()` 定义带内置校验的角色:
```typescript
// src/app/default-function.role.ts
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
@@ -401,10 +408,10 @@ export default defineRole({
});
```
随后,该角色的 `universalIdentifier` 会在 `application.config.ts` 中被引用为 `roleUniversalIdentifier`。 换句话说:
随后,该角色的 `universalIdentifier` 会在 `application-config.ts` 中被引用为 `defaultRoleUniversalIdentifier`。 换句话说:
* **\*.role.ts** 定义默认函数角色可以执行的操作。
* **application.config.ts** 指向该角色,使你的函数继承其权限。
* **application-config.ts** 指向该角色,使你的函数继承其权限。
备注:
@@ -415,11 +422,11 @@ export default defineRole({
### 逻辑函数的配置与入口点
每个函数文件都使用 `defineFunction()` 导出包含处理程序和可选触发器的配置。 使用 `*.function.ts` 文件后缀以便自动检测。
每个函数文件都使用 `defineLogicFunction()` 导出包含处理程序和可选触发器的配置。
```typescript
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
@@ -439,7 +446,7 @@ const handler = async (params: RoutePayload) => {
return result;
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
@@ -486,6 +493,55 @@ export default defineFunction({
* `triggers` 数组是可选的。 没有触发器的函数可作为实用函数,被其他函数调用。
* 你可以在单个函数中混用多种触发器类型。
### 安装后函数
安装后函数是在你的应用安装到工作区后自动运行的逻辑函数。 这对于一次性设置任务很有用,例如填充默认数据、创建初始记录或配置工作区设置。
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
关键点:
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### 路由触发器负载
<Warning>
@@ -515,7 +571,7 @@ export default defineFunction({
当路由触发器调用你的逻辑函数时,它会接收一个遵循 AWS HTTP API v2 格式的 `RoutePayload` 对象。 从 `twenty-sdk` 导入该类型:
```typescript
import { defineFunction, type RoutePayload } from 'twenty-sdk';
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
@@ -545,7 +601,7 @@ const handler = async (event: RoutePayload) => {
出于安全原因,默认**不会**将传入请求的 HTTP 请求头传递给你的逻辑函数。 如需访问特定请求头,请在 `forwardedRequestHeaders` 数组中显式列出:
```typescript
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
@@ -580,12 +636,114 @@ const handler = async (event: RoutePayload) => {
你可以通过两种方式创建新函数:
* **脚手架生成**:运行 `yarn entity:add` 并选择添加新函数的选项。 这将生成一个包含处理程序和配置的入门文件。
* **手动**:创建一个新的 `*.function.ts` 文件,并使用 `defineFunction()`,遵循相同的模式。
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新逻辑函数的选项。 这将生成一个包含处理程序和配置的入门文件。
* **手动**:创建一个新的 `*.logic-function.ts` 文件,并使用 `defineLogicFunction()`,遵循相同的模式。
### 将逻辑函数标记为工具
逻辑函数可以作为供 AI 智能体和工作流使用的**工具**对外提供。 当函数被标记为工具时,Twenty 的 AI 功能即可发现它,并可在工作流自动化中将其选作一个步骤。
要将逻辑函数标记为工具,请设置 `isTool: true`,并提供 `toolInputSchema`,使用 [JSON Schema](https://json-schema.org/) 描述预期的输入参数:
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
关键点:
* **`isTool`** (`boolean`, 默认: `false`): 当设置为 `true` 时,该函数会被注册为工具,并可供 AI 代理和工作流自动化使用。
* **`toolInputSchema`** (`object`, 可选): 描述函数可接受参数的 JSON Schema 对象。 AI 代理使用此架构来理解该工具期望的输入并验证调用。 如果省略,架构将默认为 `{ type: 'object', properties: {} }`(无参数)。
* 设置为 `isTool: false`(或未设置)的函数**不会**被暴露为工具。 它们仍可直接执行或被其他函数调用,但不会出现在工具发现中。
* **工具命名**: 当作为工具对外暴露时,函数名会被自动规范化为 `logic_function_<name>`(转换为小写,非字母数字字符替换为下划线)。 例如,`enrich-company` 将变为 `logic_function_enrich_company`。
* 你可以将 `isTool` 与触发器结合使用——一个函数既可以作为工具(由 AI 代理调用),也可以同时由事件(cron、数据库事件、路由)触发。
<Note>
**写一个好的 `description`。** AI 代理会依赖该函数的 `description` 字段来决定何时使用该工具。 明确说明该工具的作用以及应在何时调用。
</Note>
### 前端组件
前端组件使你可以构建在 Twenty 的 UI 中渲染的自定义 React 组件。 使用 `defineFrontComponent()` 以内置校验定义组件:
```typescript
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
关键点:
* 前端组件是在 Twenty 中的隔离上下文中渲染的 React 组件。
* 使用 `*.front-component.tsx` 文件后缀以便自动检测。
* `component` 字段引用你的 React 组件。
* 组件会在 `yarn twenty app:dev` 期间自动构建并同步。
你可以通过两种方式创建新的前端组件:
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新前端组件的选项。
* **手动**:创建一个新的 `*.front-component.tsx` 文件,并使用 `defineFrontComponent()`。
### 生成的类型化客户端
运行 yarn app:generate,根据你的工作空间模式在 generated/ 中创建本地类型化客户端。 在你的函数中使用它:
类型化客户端由 `yarn twenty app:dev` 自动生成,并基于你的工作区架构存放在 `node_modules/twenty-sdk/generated`。 在你的函数中使用它:
```typescript
import Twenty from '~/generated';
@@ -594,7 +752,7 @@ const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
客户端会通过 `yarn app:generate` 重新生成。 在更改对象之后或接入新工作空间时,请重新运行
每当你的对象或字段发生变化时,`yarn twenty app:dev` 都会自动重新生成该客户端
#### 逻辑函数中的运行时凭据
@@ -606,49 +764,38 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
备注:
* 你无需向生成的客户端传递 URL 或 API 密钥。 它会在运行时从 process.env 读取 `TWENTY_API_URL` 和 `TWENTY_API_KEY`。
* API 密钥的权限由 `application.config.ts` 中通过 `roleUniversalIdentifier` 引用的角色决定。 这是你的应用逻辑函数使用的默认角色。
* 应用可以定义角色以遵循最小权限原则。 仅授予函数所需的权限,然后将 `roleUniversalIdentifier` 指向该角色的通用标识符。
* API 密钥的权限由 `application-config.ts` 中通过 `defaultRoleUniversalIdentifier` 引用的角色决定。 这是你的应用逻辑函数使用的默认角色。
* 应用可以定义角色以遵循最小权限原则。 仅授予函数所需的权限,然后将 `defaultRoleUniversalIdentifier` 指向该角色的通用标识符。
### Hello World 示例
在[此处](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world)查看一个最小的端到端示例,展示对象、函数和多种触发器:
在[此处](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world)查看一个最小的端到端示例,展示对象、逻辑函数、前端组件和多种触发器:
## 手动设置(不使用脚手架)
虽然我们建议使用 `create-twenty-app` 以获得最佳的上手体验,但你也可以手动设置项目。 不要全局安装 CLI。 相反,请将 `twenty-sdk` 添加为本地依赖,并在你的 package.json 中连接相关脚本:
虽然我们建议使用 `create-twenty-app` 以获得最佳的上手体验,但你也可以手动设置项目。 不要全局安装 CLI。 相反,请将 `twenty-sdk` 添加为本地依赖,并在你的 package.json 中配置一个脚本:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
然后添加如下脚本:
然后添加一个 `twenty` 脚本:
```json filename="package.json"
{
"scripts": {
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
"twenty": "twenty"
}
}
```
现在你可以通过 Yarn 运行相同的命令,例如 `yarn app:dev`、`yarn app:generate` 等。
现在你可以通过 `yarn twenty <command>` 运行所有命令,例如 `yarn twenty app:dev`、`yarn twenty help` 等。
## 故障排除
* 身份验证错误:运行 `yarn auth:login`,并确保你的 API 密钥具有所需权限。
* 身份验证错误:运行 `yarn twenty auth:login`,并确保你的 API 密钥具有所需权限。
* 无法连接到服务器:请验证 API URL,并确保 Twenty 服务器可达。
* 类型或客户端缺失/过期:运行 `yarn app:generate`
* 开发模式未同步:确保 `yarn app:dev` 正在运行,并且你的环境不会忽略变更。
* 类型或客户端缺失/过期:重启 `yarn twenty app:dev` — 它会自动生成类型化客户端
* 开发模式未同步:确保 `yarn twenty app:dev` 正在运行,并且你的环境不会忽略变更。
Discord 帮助频道:https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -111,8 +111,8 @@ test('Create and update record', async ({ page }) => {
await companyRelationWidget.hover();
await companyRelationWidget.locator('.tabler-icon-pencil').click();
await page.getByRole('textbox', { name: 'Search' }).fill('Goog');
await expect(page.getByRole('option', { name: 'Google' })).toBeVisible();
await page.getByRole('textbox', { name: 'Search' }).fill('VMw');
await expect(page.getByRole('option', { name: 'VMware' })).toBeVisible();
const [updatePersonResponse] = await Promise.all([
page.waitForResponse(async (response) => {
if (!response.url().endsWith('/graphql')) {
@@ -123,7 +123,7 @@ test('Create and update record', async ({ page }) => {
return requestBody.operationName === 'UpdateOnePerson';
}),
await page.getByRole('option', { name: 'Google' }).click({force: true})
await page.getByRole('option', { name: 'VMware' }).click({force: true})
]);
const body = await updatePersonResponse.json()
@@ -153,6 +153,6 @@ test('Create and update record', async ({ page }) => {
expect(findOnePersonReponseBody.data.person.linkedinLink.primaryLinkUrl).toBe('linkedin.com/johndoe');
expect(findOnePersonReponseBody.data.person.phones.primaryPhoneNumber).toBe('611223344');
expect(findOnePersonReponseBody.data.person.workPreference).toEqual(['HYBRID']);
expect(findOnePersonReponseBody.data.person.company.name).toBe('Google');
expect(findOnePersonReponseBody.data.person.company.name).toBe('VMware');
});
@@ -7,6 +7,9 @@ test('Create workflow', async ({ page }) => {
await page.goto(process.env.LINK);
const workflowsFolder = page.getByRole('button', { name: 'Workflows' });
await workflowsFolder.click();
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
await workflowsLink.click();
+10 -2
View File
@@ -5,9 +5,17 @@
"tags": ["scope:backend"],
"targets": {
"build": {
"outputs": ["{options.outputPath}"],
"executor": "nx:run-commands",
"cache": true,
"inputs": ["production", "^production"],
"outputs": ["{projectRoot}/dist"],
"options": {
"outputPath": "{projectRoot}/dist"
"cwd": "{projectRoot}",
"commands": [
"npx vite build",
"tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --outDir dist --rootDir src --composite false && npx tsc-alias -p tsconfig.lib.json --outDir dist"
],
"parallel": false
},
"dependsOn": ["^build"]
},
+1
View File
@@ -2,6 +2,7 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "bundler",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
+2 -9
View File
@@ -3,7 +3,6 @@ import react from '@vitejs/plugin-react-swc';
import * as path from 'path';
import { APP_LOCALES } from 'twenty-shared/translations';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
@@ -25,19 +24,13 @@ export default defineConfig({
configPath: path.resolve(__dirname, './lingui.config.ts'),
}),
tsconfigPaths({
root: __dirname
}),
dts({
entryRoot: 'src',
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
root: __dirname,
}),
],
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
reportCompressedSize: true,
reportCompressedSize: false,
commonjsOptions: {
transformMixedEsModules: true,
},
@@ -52,11 +52,11 @@ export const rule = createRule<[], 'restApiMethodsShouldBeGuarded'>({
meta: {
docs: {
description:
'REST API endpoints should have authentication guards (UserAuthGuard, WorkspaceAuthGuard, FilePathGuard, or FilesFieldGuard) or be explicitly marked as public (PublicEndpointGuard) and permission guards (SettingsPermissionsGuard or CustomPermissionGuard) to maintain our security model.',
'REST API endpoints should have authentication guards (UserAuthGuard, WorkspaceAuthGuard, FilePathGuard, FileByIdGuard) or be explicitly marked as public (PublicEndpointGuard) and permission guards (SettingsPermissionsGuard or CustomPermissionGuard) to maintain our security model.',
},
messages: {
restApiMethodsShouldBeGuarded:
'All REST API controller endpoints must have authentication guards (@UseGuards(UserAuthGuard/WorkspaceAuthGuard/FilePathGuard/FileIdGuard/FilesFieldGuard/PublicEndpointGuard)) and permission guards (@UseGuards(..., SettingsPermissionsGuard(PermissionFlagType.XXX)), CustomPermissionGuard for custom logic, or NoPermissionGuard for special cases).',
'All REST API controller endpoints must have authentication guards (@UseGuards(UserAuthGuard/WorkspaceAuthGuard/FilePathGuard/FileByIdGuard/PublicEndpointGuard)) and permission guards (@UseGuards(..., SettingsPermissionsGuard(PermissionFlagType.XXX)), CustomPermissionGuard for custom logic, or NoPermissionGuard for special cases).',
},
schema: [],
hasSuggestions: false,
@@ -42,7 +42,7 @@ export const typedTokenHelpers = {
TSESTree.AST_NODE_TYPES.Identifier &&
decorator.expression.callee.name === 'UseGuards'
) {
// Check the arguments for UserAuthGuard, WorkspaceAuthGuard, PublicEndpoint, FilePathGuard, or FilesFieldGuard
// Check the arguments for UserAuthGuard, WorkspaceAuthGuard, PublicEndpoint, FilePathGuard or FileByIdGuard
return decorator.expression.arguments.some((arg) => {
if (arg.type === TSESTree.AST_NODE_TYPES.Identifier) {
return (
@@ -50,7 +50,7 @@ export const typedTokenHelpers = {
arg.name === 'WorkspaceAuthGuard' ||
arg.name === 'PublicEndpointGuard' ||
arg.name === 'FilePathGuard' ||
arg.name === 'FilesFieldGuard'
arg.name === 'FileByIdGuard'
);
}
return false;
@@ -9,6 +9,8 @@ import { SOURCE_LOCALE } from 'twenty-shared/translations';
// eslint-disable-next-line no-restricted-imports
import { RootDecorator } from '../src/testing/decorators/RootDecorator';
// eslint-disable-next-line no-restricted-imports
import { resetJotaiStore } from '../src/modules/ui/utilities/state/jotai/jotaiStore';
import 'react-loading-skeleton/dist/skeleton.css';
import 'twenty-ui/style.css';
@@ -84,6 +86,10 @@ const preview: Preview = {
RootDecorator,
],
beforeEach: () => {
resetJotaiStore();
},
loaders: [mswLoader],
parameters: {
+5 -1
View File
@@ -18,9 +18,9 @@ module.exports = {
'./src/modules/billing/graphql/**/*.{ts,tsx}',
'./src/modules/settings/**/graphql/**/*.{ts,tsx}',
'./src/modules/logic-functions/graphql/**/*.{ts,tsx}',
'./src/modules/databases/graphql/**/*.{ts,tsx}',
'./src/modules/workflow/**/graphql/**/*.{ts,tsx}',
'./src/modules/analytics/graphql/**/*.{ts,tsx}',
'./src/modules/object-metadata/graphql/**/*.{ts,tsx}',
'./src/modules/navigation-menu-item/graphql/**/*.{ts,tsx}',
@@ -28,9 +28,13 @@ module.exports = {
'./src/modules/attachments/graphql/**/*.{ts,tsx}',
'./src/modules/file/graphql/**/*.{ts,tsx}',
'./src/modules/onboarding/graphql/**/*.{ts,tsx}',
'./src/modules/front-components/graphql/**/*.{ts,tsx}',
'./src/modules/page-layout/widgets/**/graphql/**/*.{ts,tsx}',
'./src/modules/dashboards/graphql/**/*.{ts,tsx}',
'./src/modules/page-layout/graphql/**/*.{ts,tsx}',
'./src/modules/marketplace/graphql/**/*.{ts,tsx}',
'!./src/**/*.test.{ts,tsx}',
'!./src/**/*.stories.{ts,tsx}',
'!./src/**/__mocks__/*.ts',
+3 -14
View File
@@ -5,22 +5,11 @@ module.exports = {
(process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') +
'/graphql',
documents: [
'./src/modules/activities/graphql/**/*.{ts,tsx}',
'./src/modules/companies/graphql/**/*.{ts,tsx}',
'./src/modules/people/graphql/**/*.{ts,tsx}',
'./src/modules/opportunities/graphql/**/*.{ts,tsx}',
'./src/modules/workflow/**/graphql/**/*.{ts,tsx}',
'./src/modules/activities/emails/graphql/**/*.{ts,tsx}',
'./src/modules/activities/calendar/graphql/**/*.{ts,tsx}',
'./src/modules/search/graphql/**/*.{ts,tsx}',
'./src/modules/views/graphql/**/*.{ts,tsx}',
'./src/modules/favorites/graphql/**/*.{ts,tsx}',
'./src/modules/spreadsheet-import/graphql/**/*.{ts,tsx}',
'./src/modules/command-menu/graphql/**/*.{ts,tsx}',
'./src/modules/marketplace/graphql/**/*.{ts,tsx}',
'./src/modules/prefetch/graphql/**/*.{ts,tsx}',
'./src/modules/subscription/graphql/**/*.{ts,tsx}',
'./src/modules/page-layout/graphql/**/*.{ts,tsx}',
'!./src/**/*.test.{ts,tsx}',
'!./src/**/*.stories.{ts,tsx}',
+3 -4
View File
@@ -14,7 +14,6 @@ process.env.TZ = 'GMT';
// eslint-disable-next-line no-undef
process.env.LC_ALL = 'en_US.UTF-8';
const jestConfig = {
silent: true,
// For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string
// Prettier v3 will should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1
prettierPath: null,
@@ -62,9 +61,9 @@ const jestConfig = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageThreshold: {
global: {
statements: 50,
lines: 48.9,
functions: 40.9,
statements: 49.5,
lines: 48,
functions: 40,
},
},
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
+1
View File
@@ -84,6 +84,7 @@
"graphql": "16.8.1",
"graphql-sse": "^2.5.4",
"input-otp": "^1.4.2",
"jotai": "^2.17.1",
"js-cookie": "^3.0.5",
"json-2-csv": "^5.4.0",
"json-logic-js": "^2.0.5",
+26
View File
@@ -3,6 +3,11 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import {
ReadableStream as NodeReadableStream,
TransformStream as NodeTransformStream,
WritableStream as NodeWritableStream,
} from 'node:stream/web';
import { i18n } from '@lingui/core';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
@@ -12,6 +17,27 @@ import { messages as enMessages } from '~/locales/generated/en';
i18n.load({ [SOURCE_LOCALE]: enMessages });
i18n.activate(SOURCE_LOCALE);
const globalWithWebStreams = globalThis as Record<string, unknown>;
if (globalWithWebStreams.TransformStream === undefined) {
globalWithWebStreams.TransformStream = NodeTransformStream;
}
if (globalWithWebStreams.ReadableStream === undefined) {
globalWithWebStreams.ReadableStream = NodeReadableStream;
}
if (globalWithWebStreams.WritableStream === undefined) {
globalWithWebStreams.WritableStream = NodeWritableStream;
}
if (typeof window !== 'undefined') {
Object.defineProperty(window, 'scrollTo', {
value: () => {},
writable: true,
});
}
// Add Jest matchers for toThrowError and other missing methods
declare global {
namespace jest {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@ import { useRecoilValue } from 'recoil';
import { AppPath, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { OnboardingStatus } from '~/generated/graphql';
import { OnboardingStatus } from '~/generated-metadata/graphql';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
@@ -9,6 +9,18 @@ export const useCopyToClipboard = () => {
const { t } = useLingui();
const copyToClipboard = async (valueAsString: string, message?: string) => {
if (!window.isSecureContext) {
enqueueErrorSnackBar({
message: t`Clipboard requires a secure connection (HTTPS). Please access this app over HTTPS to enable copying.`,
options: {
icon: <IconExclamationCircle size={16} color="red" />,
duration: 6000,
},
});
return;
}
try {
await navigator.clipboard.writeText(valueAsString);
@@ -1,5 +1,5 @@
import { useNavigate } from 'react-router-dom';
import { type AppPath } from 'twenty-shared/types';
import { type AppPath, type NavigateOptions } from 'twenty-shared/types';
import { getAppPath } from 'twenty-shared/utils';
export const useNavigateApp = () => {
@@ -9,10 +9,7 @@ export const useNavigateApp = () => {
to: T,
params?: Parameters<typeof getAppPath<T>>[1],
queryParams?: Record<string, any>,
options?: {
replace?: boolean;
state?: any;
},
options?: NavigateOptions,
) => {
const path = getAppPath(to, params, queryParams);
return navigate(path, options);
@@ -11,7 +11,7 @@ import { useRecoilValue } from 'recoil';
import { AppPath, SettingsPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { OnboardingStatus } from '~/generated/graphql';
import { OnboardingStatus } from '~/generated-metadata/graphql';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
export const usePageChangeEffectNavigateLocation = () => {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

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