diff --git a/packages/twenty-docs/docs.json b/packages/twenty-docs/docs.json index b651e42d60..1f88570823 100644 --- a/packages/twenty-docs/docs.json +++ b/packages/twenty-docs/docs.json @@ -374,65 +374,15 @@ { "group": "Apps", "pages": [ - { - "group": "Getting Started", - "pages": [ - "developers/extend/apps/getting-started/quick-start", - "developers/extend/apps/getting-started/concepts", - "developers/extend/apps/getting-started/project-structure", - "developers/extend/apps/getting-started/local-server", - "developers/extend/apps/getting-started/scaffolding", - "developers/extend/apps/getting-started/troubleshooting" - ] - }, - { - "group": "Config", - "pages": [ - "developers/extend/apps/config/overview", - "developers/extend/apps/config/application", - "developers/extend/apps/config/roles", - "developers/extend/apps/config/install-hooks", - "developers/extend/apps/config/public-assets" - ] - }, - { - "group": "Data", - "pages": [ - "developers/extend/apps/data/overview", - "developers/extend/apps/data/objects", - "developers/extend/apps/data/extending-objects", - "developers/extend/apps/data/relations" - ] - }, - { - "group": "Logic", - "pages": [ - "developers/extend/apps/logic/overview", - "developers/extend/apps/logic/logic-functions", - "developers/extend/apps/logic/skills-and-agents", - "developers/extend/apps/logic/connections" - ] - }, - { - "group": "Layout", - "pages": [ - "developers/extend/apps/layout/overview", - "developers/extend/apps/layout/views", - "developers/extend/apps/layout/navigation-menu-items", - "developers/extend/apps/layout/page-layouts", - "developers/extend/apps/layout/front-components", - "developers/extend/apps/layout/command-menu-items" - ] - }, - { - "group": "Operations", - "pages": [ - "developers/extend/apps/operations/overview", - "developers/extend/apps/operations/cli", - "developers/extend/apps/operations/testing", - "developers/extend/apps/operations/publishing" - ] - } + "developers/extend/apps/getting-started", + "developers/extend/apps/building", + "developers/extend/apps/data-model", + "developers/extend/apps/logic-functions", + "developers/extend/apps/front-components", + "developers/extend/apps/layout", + "developers/extend/apps/skills-and-agents", + "developers/extend/apps/cli-and-testing", + "developers/extend/apps/publishing" ] }, { diff --git a/packages/twenty-docs/l/de/developers/extend/apps/config/application.mdx b/packages/twenty-docs/l/de/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..8fb069a517 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/config/application.mdx @@ -0,0 +1,65 @@ +--- +title: Application Config +description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. +icon: rocket +--- + +Every app must have exactly one `defineApplication` call. It declares: + +* **Identity** — universal identifier, display name, description. +* **Permissions** — which role its logic functions and front components run under. +* **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. +* **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/l/de/developers/extend/apps/logic/logic-functions). + +```ts src/application-config.ts +import { defineApplication } from 'twenty-sdk/define'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; + +export default defineApplication({ + universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', + displayName: 'My Twenty App', + description: 'My first Twenty app', + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', + description: 'Default recipient name for postcards', + value: 'Jane Doe', + isSecret: false, + }, + }, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). +* `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/l/de/developers/extend/apps/config/roles). +* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. + +## Default function role + +The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: + +* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. +* The typed API client is restricted to the permissions granted to that role. +* Follow least-privilege: declare only the permissions your functions need. + +When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/l/de/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/l/de/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `author` | Author or company name | +| `category` | App category for marketplace filtering | +| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) | +| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) | +| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | +| `websiteUrl` | Link to your website | +| `termsUrl` | Link to terms of service | +| `emailSupport` | Support email address | +| `issueReportUrl` | Link to issue tracker | diff --git a/packages/twenty-docs/l/de/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/l/de/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..e4c2ead390 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/config/install-hooks.mdx @@ -0,0 +1,206 @@ +--- +title: Install Hooks +description: Run logic before or after the install — seed data, back up records, validate the upgrade. +icon: wrench +--- + +Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/l/de/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload`, but they're declared with their own define functions — `definePostInstallLogicFunction()` and `definePreInstallLogicFunction()` — and live outside the normal trigger model (HTTP, cron, database events). + +Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build will error if more than one of either is detected. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ install flow │ +│ │ +│ upload package → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +│ │ +│ old schema visible new schema visible │ +└─────────────────────────────────────────────────────────────┘ +``` + + + + +A post-install function runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services. + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Post install logic function executed successfully!', payload.previousVersion); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Runs after installation to set up the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + shouldRunSynchronously: false, + handler, +}); +``` + +You can also manually execute the post-install function at any time using the CLI: + +```bash filename="Terminal" +yarn twenty exec --postInstall +``` + +Key points: +* Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`). +* The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic. +* **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook. +* **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed. + * `shouldRunSynchronously: false` *(default)* — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window. + * `shouldRunSynchronously: true` — the hook is executed **inline during the install flow** (same executor as pre-install). The install request blocks until the handler finishes, and if it throws, the install caller receives a `POST_INSTALL_ERROR`. No automatic retries. **Use this for fast, must-complete-before-response work** — for example, emitting a validation error to the user, or quick setup that the client will rely on immediately after the install call returns. Keep in mind the metadata migration has already been applied by the time post-install runs, so a sync-mode failure does **not** roll back the schema changes — it only surfaces the error. +* Make sure your handler is idempotent. In async mode the queue may retry up to three times; in either mode the hook may run again on upgrades when `shouldRunOnVersionUpgrade: true`. +* The environment variables `APPLICATION_ID`, `APP_ACCESS_TOKEN`, and `API_URL` are available inside the handler (same as any other logic function), so you can call the Twenty API with an application access token scoped to your app. +* Only one post-install function is allowed per application. The manifest build will error if more than one is detected. +* The function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in [`defineApplication()`](/l/de/developers/extend/apps/config/application). +* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding. +* **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` to trigger it manually against a running workspace. + + + + +A pre-install function runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped. + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Pre install logic function executed successfully!', payload.previousVersion); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Runs before installation to prepare the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +You can also manually execute the pre-install function at any time using the CLI: + +```bash filename="Terminal" +yarn twenty exec --preInstall +``` + +Key points: +* Pre-install functions use `definePreInstallLogicFunction()` — same specialized config as post-install, just attached to a different lifecycle slot. +* Both pre- and post-install handlers receive the same `InstallPayload` type: `{ previousVersion?: string; newVersion: string }`. Import it once and reuse it for both hooks. +* **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state. +* **Execution model**: pre-install is executed **synchronously** and **blocks the install**. If the handler throws, the install is aborted before any schema changes are applied — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade. +* As with post-install, only one pre-install function is allowed per application. It is attached to the application manifest under `preInstallLogicFunction` automatically during the build. +* **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty exec --preInstall` to trigger it manually. + + + + +Both hooks are part of the same install flow and receive the same `InstallPayload`. The difference is **when** they run relative to the workspace metadata migration, and that changes what data they can safely touch. + +Pre-install is always **synchronous** (it blocks the install and can abort it). Post-install is **asynchronous by default** — enqueued on a worker with automatic retries — but can opt into synchronous execution with `shouldRunSynchronously: true`. See the `definePostInstallLogicFunction` accordion above for when to use each mode. + +**Use `post-install` for anything that needs the new schema to exist.** This is the common case: + +* Seeding default data (creating initial records, default views, demo content) against newly-added objects and fields. +* Registering webhooks with third-party services now that the app has its credentials. +* Calling your own API to finish setup that depends on the synchronized metadata. +* Idempotent "ensure this exists" logic that should reconcile state on every upgrade — combine with `shouldRunOnVersionUpgrade: true`. + +Example — seed a default `PostCard` record after install: + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion }: InstallPayload): Promise => { + if (previousVersion) return; // fresh installs only + + const client = createClient(); + await client.postCard.create({ + data: { title: 'Welcome to Postcard', content: 'Your first card!' }, + }); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Seeds a welcome post card after install.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + handler, +}); +``` + +**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the *previous* schema and its failure rolls back the upgrade, it is the right place for anything risky: + +* **Backing up data that is about to be dropped or restructured** — e.g. you are removing a field in v2 and need to copy its values into another field or export them to storage before the migration runs. +* **Archiving records that a new constraint would invalidate** — e.g. a field is becoming `NOT NULL` and you need to delete or fix rows with null values first. +* **Validating compatibility and refusing the upgrade if the current data cannot be migrated cleanly** — throw from the handler and the install aborts with no changes applied. This is safer than discovering the incompatibility mid-migration. +* **Renaming or rekeying data** ahead of a schema change that would lose the association. + +Example — archive records before a destructive migration: + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise => { + // Only the 1.x → 2.x upgrade drops the legacy `notes` field. + if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) { + return; + } + + const client = createClient(); + const legacyRecords = await client.postCard.findMany({ + where: { notes: { isNotNull: true } }, + }); + + if (legacyRecords.length === 0) return; + + // Copy legacy `notes` into the new `description` field before the migration + // drops the `notes` column. If this fails, the upgrade is aborted and the + // workspace stays on v1 with all data intact. + await Promise.all( + legacyRecords.map((record) => + client.postCard.update({ + where: { id: record.id }, + data: { description: record.notes }, + }), + ), + ); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Backs up legacy notes into description before the v2 migration.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +**Rule of thumb:** + +| You want to... | Use | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Seed default data, configure the workspace, register external resources | `post-install` | +| Run long-running seeding or third-party calls that shouldn't block the install response | `post-install` (default — `shouldRunSynchronously: false`, with worker retries) | +| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` | +| Read or back up data that the upcoming migration would lose | `pre-install` | +| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) | +| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` | +| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) | + + +If in doubt, default to **post-install**. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone. + + + + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/config/overview.mdx b/packages/twenty-docs/l/de/developers/extend/apps/config/overview.mdx new file mode 100644 index 0000000000..1d53dd80b4 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/config/overview.mdx @@ -0,0 +1,51 @@ +--- +title: Overview +description: Configure the app itself — its identity, default permissions, and what runs at install time. +icon: screwdriver-wrench +--- + +A Twenty app's **config layer** is what describes the app *to the platform* — its identity, the permissions it holds, and the code that runs during install or upgrade. These declarations don't add new data shapes or runtime behavior; they tell Twenty *who the app is* and *how to set it up*. + +```text +┌────────────────────────────────────────────────────────┐ +│ Application — identity, default role, variables, │ +│ marketplace metadata │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Role — what the app's logic functions can read │ │ +│ │ and write (referenced by Application) │ │ +│ └──────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────┘ + │ + ▼ (at install / upgrade time) + ┌──────────────────────────────────┐ + │ Pre-install hook │ before metadata migration + └──────────────────────────────────┘ + ┌──────────────────────────────────┐ + │ Post-install hook │ after metadata migration + └──────────────────────────────────┘ +``` + +## In this section + + + + `defineApplication` — identity, default role, variables, marketplace metadata. + + + `defineRole` — declare what your app's logic functions can read and write. + + + `definePreInstallLogicFunction` and `definePostInstallLogicFunction` — back up data, seed defaults, validate upgrades. + + + +## How the pieces relate + +* **Application** is the entry point. Every app has exactly one `defineApplication()` call, and it points at one **Role** as its default. +* The **Role** controls what the app's logic functions and front components can read and write. Follow least-privilege: only grant the permissions your code actually needs. +* **Install Hooks** run during install or upgrade — pre-install before the metadata migration (so it can refuse a risky upgrade), post-install after the migration (so it can seed default data against the new schema). + + +Install hooks share the [logic function](/l/de/developers/extend/apps/logic/logic-functions) runtime — same handler signature, same environment variables, same typed API client — but they're declared with their own define functions and live outside the regular trigger model (HTTP, cron, database events). + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/config/public-assets.mdx b/packages/twenty-docs/l/de/developers/extend/apps/config/public-assets.mdx new file mode 100644 index 0000000000..338c0f7f8e --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/config/public-assets.mdx @@ -0,0 +1,59 @@ +--- +title: Public Assets +description: Ship static files — images, icons, fonts — alongside your app via the public/ folder. +icon: folder-open +--- + +The `public/` folder at the root of your app holds static files — images, icons, fonts, or any other assets your app needs at runtime. These files are automatically included in builds, synced during dev mode, and uploaded to the server. + +Files placed in `public/` are: + +* **Publicly accessible** — once synced to the server, assets are served at a public URL. No authentication is needed to access them. +* **Available in front components** — use asset URLs to display images, icons, or any media inside your React components. +* **Available in logic functions** — reference asset URLs in emails, API responses, or any server-side logic. +* **Used for marketplace metadata** — the `logoUrl` and `screenshots` fields in `defineApplication()` reference files from this folder (e.g., `public/logo.png`). These are displayed in the marketplace when your app is published. +* **Auto-synced in dev mode** — when you add, update, or delete a file in `public/`, it is synced to the server automatically. No restart needed. +* **Included in builds** — `yarn twenty build` bundles all public assets into the distribution output. + +## Accessing public assets with `getPublicAssetUrl` + +Use the `getPublicAssetUrl` helper from `twenty-sdk` to get the full URL of a file in your `public/` directory. It works in both **logic functions** and **front components**. + +**In a logic function:** + +```ts src/logic-functions/send-invoice.ts +import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define'; + +const handler = async (): Promise => { + const logoUrl = getPublicAssetUrl('logo.png'); + const invoiceUrl = getPublicAssetUrl('templates/invoice.png'); + + // Fetch the file content (no auth required — public endpoint) + const response = await fetch(invoiceUrl); + const buffer = await response.arrayBuffer(); + + return { logoUrl, size: buffer.byteLength }; +}; + +export default defineLogicFunction({ + universalIdentifier: 'a1b2c3d4-...', + name: 'send-invoice', + description: 'Sends an invoice with the app logo', + timeoutSeconds: 10, + handler, +}); +``` + +**In a front component:** + +```tsx src/front-components/company-card.tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +export default defineFrontComponent(() => { + const logoUrl = getPublicAssetUrl('logo.png'); + + return App logo; +}); +``` + +The `path` argument is relative to your app's `public/` folder. Both `getPublicAssetUrl('logo.png')` and `getPublicAssetUrl('public/logo.png')` resolve to the same URL — the `public/` prefix is stripped automatically if present. diff --git a/packages/twenty-docs/l/de/developers/extend/apps/config/roles.mdx b/packages/twenty-docs/l/de/developers/extend/apps/config/roles.mdx new file mode 100644 index 0000000000..6b45e948bc --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/config/roles.mdx @@ -0,0 +1,90 @@ +--- +title: Roles & Permissions +description: Declare what objects and fields your app's logic functions and front components can read and write. +icon: shield-halved +--- + +A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role declared as `defaultRoleUniversalIdentifier` in [`defineApplication`](/l/de/developers/extend/apps/config/application). + +```ts src/roles/restricted-company-role.ts +import { + defineRole, + PermissionFlag, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; + +export default defineRole({ + universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6', + label: 'My new role', + description: 'A role that can be used in your workspace', + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + fieldPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + fieldUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier, + canReadFieldValue: false, + canUpdateFieldValue: false, + }, + ], + permissionFlags: [PermissionFlag.APPLICATIONS], +}); +``` + +## The default function role + +When you scaffold a new app, the CLI creates a default role file: + +```ts src/roles/default-role.ts +import { defineRole, PermissionFlag } from 'twenty-sdk/define'; + +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = + 'b648f87b-1d26-4961-b974-0908fd991061'; + +export default defineRole({ + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + label: 'Default function role', + description: 'Default role for function Twenty client', + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [], + fieldPermissions: [], + permissionFlags: [], +}); +``` + +This role's `universalIdentifier` is referenced from `application-config.ts` as `defaultRoleUniversalIdentifier`: + +* **`*.role.ts`** declares what the role can do. +* **`application-config.ts`** points to that role so your functions inherit its permissions. + +## Best practices + +* Start from the scaffolded role, then progressively restrict it — the default grants broad read access, which is rarely what you want in production. +* Replace `objectPermissions` and `fieldPermissions` with the exact objects and fields your functions actually need. +* `permissionFlags` control access to platform-level capabilities. Keep them minimal. +* See a working example: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts). diff --git a/packages/twenty-docs/l/de/developers/extend/apps/data/extending-objects.mdx b/packages/twenty-docs/l/de/developers/extend/apps/data/extending-objects.mdx new file mode 100644 index 0000000000..c70d1dc64d --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/data/extending-objects.mdx @@ -0,0 +1,48 @@ +--- +title: Extending Objects +description: Add fields to standard Twenty objects (Person, Company, …) or to objects from other apps using defineField. +icon: wand-magic-sparkles +--- + +Use `defineField()` to add a field to an object you don't own — a standard Twenty object like Person or Company, or an object shipped by another installed app. Unlike inline fields declared inside [`defineObject`](/l/de/developers/extend/apps/data/objects), standalone fields require an `objectUniversalIdentifier` to specify which object they extend. + +```ts src/fields/company-loyalty-tier.field.ts +import { defineField, FieldType } from 'twenty-sdk/define'; + +export default defineField({ + universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890', + objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object + name: 'loyaltyTier', + type: FieldType.SELECT, + label: 'Loyalty Tier', + icon: 'IconStar', + options: [ + { value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' }, + { value: 'SILVER', label: 'Silver', position: 1, color: 'gray' }, + { value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' }, + ], +}); +``` + +## Key points + +* `objectUniversalIdentifier` identifies the target object. For standard Twenty objects, import the constant from `twenty-sdk`: + + ```ts + import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define'; + + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier + // … + ``` + +* When defining fields **inline inside `defineObject()`**, you do **not** need `objectUniversalIdentifier` — it's inherited from the parent object. + +* `defineField()` is the only way to add fields to objects you didn't create with `defineObject()`. + +* File location is up to you. The convention is `src/fields/\.field.ts`, but the SDK detects fields anywhere in `src/`. + +## Adding a relation to an existing object + +To add a relation field (e.g. linking your custom object to a standard `Person`), use `defineField()` with `FieldType.RELATION`. The pattern is the same as for inline relations but with `objectUniversalIdentifier` set explicitly. See [Relations](/l/de/developers/extend/apps/data/relations) for the bidirectional pattern. diff --git a/packages/twenty-docs/l/de/developers/extend/apps/data/objects.mdx b/packages/twenty-docs/l/de/developers/extend/apps/data/objects.mdx new file mode 100644 index 0000000000..df590fd477 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/data/objects.mdx @@ -0,0 +1,93 @@ +--- +title: Objekte +description: Deklariere neue Record-Typen – benutzerdefinierte Tabellen mit eigenen Feldern – mit defineObject. +icon: tabelle +--- + +Custom **objects** are new record types your app adds to a workspace — Post Card, Invoice, Subscription, anything specific to your domain. Each object declares its schema (fields, relations, default values) and a stable universal identifier that survives across syncs and deploys. + +```ts src/objects/post-card.object.ts +import { defineObject, FieldType } from 'twenty-sdk/define'; + +enum PostCardStatus { + DRAFT = 'DRAFT', + SENT = 'SENT', + DELIVERED = 'DELIVERED', + RETURNED = 'RETURNED', +} + +export default defineObject({ + universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', + nameSingular: 'postCard', + namePlural: 'postCards', + labelSingular: 'Post Card', + labelPlural: 'Post Cards', + description: 'A post card object', + icon: 'IconMail', + fields: [ + { + universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b', + name: 'content', + type: FieldType.TEXT, + label: 'Content', + description: "Postcard's content", + icon: 'IconAbc', + }, + { + universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac', + name: 'recipientName', + type: FieldType.FULL_NAME, + label: 'Recipient name', + icon: 'IconUser', + }, + { + universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266', + name: 'recipientAddress', + type: FieldType.ADDRESS, + label: 'Recipient address', + icon: 'IconHome', + }, + { + universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e', + name: 'status', + type: FieldType.SELECT, + label: 'Status', + icon: 'IconSend', + defaultValue: `'${PostCardStatus.DRAFT}'`, + options: [ + { value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' }, + { value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' }, + { value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' }, + { value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' }, + ], + }, + { + universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433', + name: 'deliveredAt', + type: FieldType.DATE_TIME, + label: 'Delivered at', + icon: 'IconCheck', + isNullable: true, + defaultValue: null, + }, + ], +}); +``` + +## 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. +* Inline fields defined here do **not** need an `objectUniversalIdentifier` — it's inherited from the parent object. Use [`defineField()`](/l/de/developers/extend/apps/data/extending-objects) to add fields to objects you don't own. +* You can scaffold new objects with `yarn twenty add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/l/de/developers/extend/apps/getting-started/scaffolding). + + +**Base fields are added automatically.** When you define a custom object, Twenty creates standard fields like `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and `deletedAt` for you. You don't need to declare them in your `fields` array — only your custom fields. You can override a default field by declaring one with the same name, but this is rarely a good idea. + + +## Was kommt als Nächstes + +* **Connect this object to others** — see [Relations](/l/de/developers/extend/apps/data/relations) for the bidirectional relation pattern. +* **Add fields to objects from other apps** — see [Extending Objects](/l/de/developers/extend/apps/data/extending-objects) for `defineField()`. +* **Display this object in the UI** — see [Views](/l/de/developers/extend/apps/layout/views) and [Navigation Menu Items](/l/de/developers/extend/apps/layout/navigation-menu-items) to put it in the sidebar. diff --git a/packages/twenty-docs/l/de/developers/extend/apps/data/overview.mdx b/packages/twenty-docs/l/de/developers/extend/apps/data/overview.mdx new file mode 100644 index 0000000000..ceda955c30 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/data/overview.mdx @@ -0,0 +1,52 @@ +--- +title: Overview +description: Shape the data your app adds to a workspace — objects, fields, and relations. +icon: database +--- + +A Twenty app's **data layer** is the data your app *adds* to a workspace — the new record types it declares, the columns it adds to existing objects, and how those records connect to each other. + +```text +┌──────────────────────────────────────────────────┐ +│ Object — a record type, e.g. PostCard │ +│ ├─ Field (name, type, label) │ +│ ├─ Field │ +│ └─ Relation (link to another object) │ +└──────────────────────────────────────────────────┘ + │ + ├── lives in your app, OR + │ + ▼ +┌──────────────────────────────────────────────────┐ +│ Standard / other apps' objects │ +│ └─ Field added by your app via defineField │ +└──────────────────────────────────────────────────┘ +``` + +## In this section + + + + `defineObject` — declare new record types with their own fields. + + + `defineField` — add fields to standard or other apps' objects. + + + Bidirectional `MANY_TO_ONE` / `ONE_TO_MANY` connections between objects. + + + +## Entities at a glance + +| Entity | Purpose | Defined with | +| ------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| **Object** | A new custom record type (e.g. PostCard, Invoice) with its own fields | `defineObject()` | +| **Field** | A column on an object. Standalone fields can extend objects you didn't create (e.g. add `loyaltyTier` to Company) | `defineField()` | +| **Relation** | A bidirectional link between two objects — both sides declared as fields | `defineField()` with `FieldType.RELATION` | + +The SDK detects these via AST analysis at build time, so file organization is up to you — the convention is `src/objects/` and `src/fields/`. Stable `universalIdentifier` UUIDs tie everything together across deploys. + + +Looking for **Application Config** or **Roles & Permissions**? Those describe the app itself rather than the data it adds — they live under [Config](/l/de/developers/extend/apps/config/overview). Looking for **Connections** (Linear, GitHub, Slack OAuth)? Those exist to be called *from* logic functions and live under [Logic](/l/de/developers/extend/apps/logic/connections). + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/data/relations.mdx b/packages/twenty-docs/l/de/developers/extend/apps/data/relations.mdx new file mode 100644 index 0000000000..db6c663e76 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/data/relations.mdx @@ -0,0 +1,160 @@ +--- +title: Beziehungen +description: Connect objects together with bidirectional MANY_TO_ONE / ONE_TO_MANY relations. +icon: diagram-project +--- + +Relations connect two objects together. In Twenty, relations are always **bidirectional** — every relation has two sides, and each side is declared as a field that references the other. + +| Beziehungstyp | Beschreibung | Fremdschlüssel vorhanden? | +| ------------- | ----------------------------------------------------------------------- | ------------------------- | +| `MANY_TO_ONE` | Viele Datensätze dieses Objekts verweisen auf einen Datensatz des Ziels | Ja (`joinColumnName`) | +| `ONE_TO_MANY` | Ein Datensatz dieses Objekts hat viele Datensätze des Ziels | No (the inverse side) | + +## How relations work + +Every relation requires **two fields** that reference each other: + +1. The **MANY_TO_ONE** side — lives on the object that holds the foreign key. +2. The **ONE_TO_MANY** side — lives on the object that owns the collection. + +Both fields use `FieldType.RELATION` and cross-reference each other via `relationTargetFieldMetadataUniversalIdentifier`. + +## Example: Post Card has many Recipients + +A `PostCard` can be sent to many `PostCardRecipient` records. Each recipient belongs to exactly one post card. + +**Step 1: Define the ONE_TO_MANY side on PostCard** (the "one" side): + +```ts src/fields/post-card-recipients-on-post-card.field.ts +import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111'; +// Import from the other side +import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field'; + +export default defineField({ + universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCardRecipients', + label: 'Post Card Recipients', + icon: 'IconUsers', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, +}); +``` + +**Step 2: Define the MANY_TO_ONE side on PostCardRecipient** (the "many" side — holds the foreign key): + +```ts src/fields/post-card-on-post-card-recipient.field.ts +import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222'; +// Import from the other side +import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field'; + +export default defineField({ + universalIdentifier: POST_CARD_FIELD_ID, + objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + icon: 'IconMail', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, +}); +``` + + +**Circular imports:** both relation fields reference each other's `universalIdentifier`. To avoid circular import issues, export your field IDs as named constants from each file and import them in the other. The build system resolves these at compile time. + + +## Relating to standard objects + +To create a relation with a built-in Twenty object (Person, Company, etc.), use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`: + +```ts src/fields/person-on-self-hosting-user.field.ts +import { + defineField, + FieldType, + RelationType, + OnDeleteAction, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; +import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object'; + +export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333'; +export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444'; + +export default defineField({ + universalIdentifier: PERSON_FIELD_ID, + objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'person', + label: 'Person', + description: 'Person matching with the self hosting user', + isNullable: true, + relationTargetObjectMetadataUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, + relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.SET_NULL, + joinColumnName: 'personId', + }, +}); +``` + +## Relation field properties + +| Property | Required | Description | +| ------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------- | +| `type` | Yes | Must be `FieldType.RELATION` | +| `relationTargetObjectMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the target object | +| `relationTargetFieldMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the matching field on the target object | +| `universalSettings.relationType` | Yes | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` | +| `universalSettings.onDelete` | MANY_TO_ONE only | What happens when the referenced record is deleted: `CASCADE`, `SET_NULL`, `RESTRICT`, or `NO_ACTION` | +| `universalSettings.joinColumnName` | MANY_TO_ONE only | Database column name for the foreign key (e.g., `postCardId`) | + +## Inline relation fields + +You can also declare a relation directly inside [`defineObject`](/l/de/developers/extend/apps/data/objects). When inline, omit `objectUniversalIdentifier` — it's inherited from the parent object: + +```ts +export default defineObject({ + universalIdentifier: '...', + nameSingular: 'postCardRecipient', + // ... + fields: [ + { + universalIdentifier: POST_CARD_FIELD_ID, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, + }, + // … other fields + ], +}); +``` diff --git a/packages/twenty-docs/l/de/developers/extend/apps/getting-started/concepts.mdx b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/concepts.mdx new file mode 100644 index 0000000000..c6dcfe8b74 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/concepts.mdx @@ -0,0 +1,101 @@ +--- +title: Concepts +description: How Twenty apps work — entity model, sandboxing, and the install lifecycle. +icon: sitemap +--- + +Twenty apps are TypeScript packages that extend your workspace with custom objects, logic, UI components, and AI capabilities. They run on the Twenty platform with full sandboxing and permission controls. + +## How apps work + +An app is a collection of **entities** declared using `defineEntity()` functions from the `twenty-sdk` package. The SDK detects these declarations via AST analysis at build time and produces a **manifest** — a complete description of what your app adds to a workspace. These functions validate your configuration at build time and provide IDE autocompletion and type safety. + +``` +your-app/ +├── src/ +│ ├── application-config.ts ← defineApplication (required, one per app) +│ ├── roles/ ← defineRole +│ ├── objects/ ← defineObject +│ ├── fields/ ← defineField +│ ├── logic-functions/ ← defineLogicFunction +│ ├── front-components/ ← defineFrontComponent +│ ├── skills/ ← defineSkill +│ ├── agents/ ← defineAgent +│ ├── views/ ← defineView +│ ├── navigation-menu-items/ ← defineNavigationMenuItem +│ └── page-layouts/ ← definePageLayout +├── public/ ← Static assets (images, icons) +└── package.json +``` + + + **File organization is up to you.** Entity detection is AST-based — the SDK finds `export default defineEntity(...)` calls regardless of where the file lives. The folder structure above is a convention, not a requirement. + + +## Entity types + +| Entity | Purpose | Docs | +| ------------------------ | ------------------------------------------ | ----------------------------------------------------------------------------- | +| **Application** | App identity, default role, variables | [Application Config](/l/de/developers/extend/apps/config/application) | +| **Role** | Permission sets on objects and fields | [Roles & Permissions](/l/de/developers/extend/apps/config/roles) | +| **Object** | Custom record types with fields | [Objects](/l/de/developers/extend/apps/data/objects) | +| **Field** | Add fields to objects from other apps | [Extending Objects](/l/de/developers/extend/apps/data/extending-objects) | +| **Relation** | Bidirectional links between objects | [Relations](/l/de/developers/extend/apps/data/relations) | +| **Logic Function** | Server-side TypeScript with triggers | [Logic Functions](/l/de/developers/extend/apps/logic/logic-functions) | +| **Skill** | Reusable AI agent instructions | [Skills & Agents](/l/de/developers/extend/apps/logic/skills-and-agents) | +| **Agent** | AI assistants with custom prompts | [Skills & Agents](/l/de/developers/extend/apps/logic/skills-and-agents) | +| **Connection Provider** | OAuth credentials for third-party APIs | [Connections](/l/de/developers/extend/apps/logic/connections) | +| **View** | Pre-configured record list views | [Views](/l/de/developers/extend/apps/layout/views) | +| **Navigation Menu Item** | Custom sidebar entries | [Navigation Menu Items](/l/de/developers/extend/apps/layout/navigation-menu-items) | +| **Page Layout** | Tabs and widgets on a record's detail page | [Page Layouts](/l/de/developers/extend/apps/layout/page-layouts) | +| **Front Component** | Sandboxed React UI inside Twenty | [Front Components](/l/de/developers/extend/apps/layout/front-components) | +| **Command Menu Item** | Quick actions and Cmd+K entries | [Command Menu Items](/l/de/developers/extend/apps/layout/command-menu-items) | + +## Sandboxing + +* **Logic functions** run in isolated Node.js processes on the server. They only access data through the typed API client, scoped to the app's role permissions. +* **Front components** run in Web Workers using Remote DOM — sandboxed from the main page but rendering native DOM elements (not iframes). They communicate with Twenty via a message-passing host API. +* **Permissions** are enforced at the API level. The runtime token (`TWENTY_APP_ACCESS_TOKEN`) is derived from the role defined in `defineApplication()`. + +## App lifecycle + +``` +┌─────────────────────────────────────────────────────────┐ +│ Development │ +│ npx create-twenty-app → yarn twenty dev (live sync) │ +├─────────────────────────────────────────────────────────┤ +│ Build & Deploy │ +│ yarn twenty build → yarn twenty deploy │ +├─────────────────────────────────────────────────────────┤ +│ Install flow │ +│ upload → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +├─────────────────────────────────────────────────────────┤ +│ Publish │ +│ npm publish → appears in Twenty marketplace │ +└─────────────────────────────────────────────────────────┘ +``` + +* **`yarn twenty dev`** — watches your source files and live-syncs changes to a connected Twenty server. The typed API client is regenerated automatically when the schema changes. +* **`yarn twenty build`** — compiles TypeScript, bundles logic functions and front components with esbuild, and produces a manifest. +* **Pre/post-install hooks** — optional functions that run during installation. See [Install Hooks](/l/de/developers/extend/apps/config/install-hooks) for details. + +## Next steps + + + + Application identity, default role, and install hooks. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/getting-started/local-server.mdx b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/local-server.mdx new file mode 100644 index 0000000000..a424e5203a --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/local-server.mdx @@ -0,0 +1,72 @@ +--- +title: Local Server +description: Manage the local Twenty Docker server — start, stop, upgrade, parallel test instance, and manual SDK setup. +icon: server +--- + +## Managing the local server + +Use `yarn twenty server` to control the local Twenty container: + +| Command | What it does | +| -------------------------------------- | -------------------------------------------- | +| `yarn twenty server start` | Start the server (pulls the image if needed) | +| `yarn twenty server start --port 3030` | Start on a custom port | +| `yarn twenty server stop` | Stop the server (preserves data) | +| `yarn twenty server status` | Show URL, version, and login credentials | +| `yarn twenty server logs` | Stream server logs | +| `yarn twenty server reset` | Wipe data and start fresh | +| `yarn twenty server upgrade` | Pull the latest `twenty-app-dev` image | +| `yarn twenty server upgrade 2.2.0` | Upgrade to a specific version | + +Data persists across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything. + +## Upgrading the server image + +`yarn twenty server upgrade` pulls the latest image, compares digests, and only recreates the container if anything actually changed. Volumes are preserved — only the container is replaced. If a new image was pulled and the container was running, the upgrade automatically starts a new container; run `yarn twenty server start` afterward to wait for it to become healthy. + +```bash filename="Terminal" +yarn twenty server upgrade # Latest +yarn twenty server upgrade 2.2.0 # Specific version +``` + +Verify the running version with `yarn twenty server status` (it shows the `APP_VERSION` baked into the container). + +## Running a parallel test instance + +Pass `--test` to any `server` command to manage a second, fully isolated instance — useful for integration tests or experiments without touching your main dev data: + +| Command | What it does | +| ----------------------------------- | ----------------------------------------------- | +| `yarn twenty server start --test` | Start the test instance (defaults to port 2021) | +| `yarn twenty server stop --test` | Stop it | +| `yarn twenty server status --test` | Show its status | +| `yarn twenty server logs --test` | Stream its logs | +| `yarn twenty server reset --test` | Wipe its data | +| `yarn twenty server upgrade --test` | Upgrade its image | + +The test instance has its own container (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`), and config — it runs alongside your main instance without conflicts. Combine `--test` with `--port` to override 2021. + +## Manual setup (without the scaffolder) + +Skip the scaffolder if you're adding the SDK to an existing project: + +```bash filename="Terminal" +yarn add twenty-sdk twenty-client-sdk +``` + +Add the script to `package.json`: + +```json filename="package.json" +{ + "scripts": { + "twenty": "twenty" + } +} +``` + +You can now run `yarn twenty dev`, `yarn twenty server start`, and the rest. + + +Don't install `twenty-sdk` globally — pin it per project so each app uses its own version. + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/getting-started/project-structure.mdx b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/project-structure.mdx new file mode 100644 index 0000000000..ad85a70452 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/project-structure.mdx @@ -0,0 +1,40 @@ +--- +title: Project Structure +description: What's inside a scaffolded Twenty app — files, folders, and what each one does. +icon: folder-tree +--- + +A new app generated by `npx create-twenty-app` looks like this: + +```text filename="my-twenty-app/" +my-twenty-app/ + package.json + src/ + application-config.ts # Required — your app's entry point + default-role.ts # Permissions for logic functions + constants/ + universal-identifiers.ts # Auto-generated UUIDs and metadata + __tests__/ + setup-test.ts + app-install.integration-test.ts + .github/workflows/ci.yml # GitHub Actions + public/ # Static assets + vitest.config.ts # Test runner config + tsconfig.json, tsconfig.spec.json + .nvmrc, .yarnrc.yml, .oxlintrc.json + README.md, LLMS.md +``` + +## Key files + +| File / Folder | Purpose | +| ---------------------------------------- | -------------------------------------------------------------- | +| `src/application-config.ts` | **Required.** The main configuration file for your app. | +| `src/default-role.ts` | Default role controlling what your logic functions can access. | +| `src/constants/universal-identifiers.ts` | Auto-generated UUIDs and metadata (display name, description). | +| `src/__tests__/` | Integration tests (setup + example test). | +| `public/` | Static assets (images, fonts) served with your app. | + + +**File organization is up to you.** The folders above are conventions — the SDK detects entities via AST analysis on `export default defineEntity(...)` calls regardless of where the file lives. + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/getting-started/quick-start.mdx b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/quick-start.mdx new file mode 100644 index 0000000000..5497298600 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/quick-start.mdx @@ -0,0 +1,184 @@ +--- +title: Quick Start +icon: rocket +description: Create your first Twenty app in minutes. +--- + +## Prerequisites + +* **Node.js 24+** — [Download](https://nodejs.org/) +* **Yarn 4** — bundled with Node via Corepack. Enable it: `corepack enable` +* **Docker** — [Download](https://www.docker.com/products/docker-desktop/). Needed to run a local Twenty server. Skip if you already have Twenty running elsewhere. + +Building a Twenty app has three phases. The scaffolder collapses them into one happy-path command, but each phase is a separate concept — when something fails, knowing which phase you're in tells you what to fix. + +| Phase | What you do | Tool | Result | +| ------------------- | ---------------------------------- | ----------------------------- | ----------------------------- | +| **1. Scaffold** | Generate the app's source code | `npx create-twenty-app` | A TypeScript project on disk | +| **2. Run a server** | Start a Twenty server to sync into | Docker + `yarn twenty server` | A running Twenty instance | +| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` | Your changes appear in the UI | + +--- + +## Phase 1 — Scaffold your project + +Create a new app from the template: + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app +``` + +You'll be prompted for a name and description — press **Enter** for the defaults. This generates a TypeScript project in `my-twenty-app/` with a starter `application-config.ts`, a default role, a CI workflow, and an integration test. + +**After this phase:** you have an app's source code on your machine. It isn't running yet — that's Phase 2. + +--- + +## Phase 2 — Run a local Twenty server + +Your app needs a Twenty server to sync into. The server is a full Twenty instance — UI, GraphQL API, PostgreSQL — running locally in Docker. Your local code uploads its definitions to that server, which makes them appear in the UI. + +The scaffolder offers to start one for you: + +> **Would you like to set up a local Twenty instance?** + +* **Yes (recommended)** — pulls the `twentycrm/twenty-app-dev` Docker image and starts it on port `2020`. Make sure Docker is running first. +* **No** — choose this if you already have a Twenty server you want to connect to. You can wire it up later with `yarn twenty remote add`. + +
+ Should start local instance? +
+ +Once the server is up, a browser opens for sign-in. Use the pre-seeded demo account: + +* **Email:** `tim@apple.dev` +* **Password:** `tim@apple.dev` + +
+ Twenty login screen +
+ +Click **Authorize** on the next screen — this gives the CLI access to your workspace. + +
+ Twenty CLI authorization screen +
+ +Your terminal will confirm everything is set up. + +
+ App scaffolded successfully +
+ +**After this phase:** you have a running Twenty server at [http://localhost:2020](http://localhost:2020) with your CLI authorized to sync to it. + + +If Docker isn't installed or running, the scaffolder will tell you the right start command for your OS. Once Docker is up, you can resume with `yarn twenty server start` — no need to re-scaffold. + + +--- + +## Phase 3 — Sync your changes + +This is the inner loop you'll spend most of your time in. + +```bash filename="Terminal" +cd my-twenty-app +yarn twenty dev +``` + +This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a second the server reflects the change. You'll see a live status panel in your terminal. + +For more detailed output (build logs, sync requests, error traces), add `--verbose`. + +
+ Dev mode terminal output +
+ +Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). You should see your app under **Your Apps**. + +
+ Your Apps list showing My twenty app +
+ +Click **My twenty app** to see its **application registration** — a server-level record describing your app (name, identifier, OAuth credentials, source). One registration can be installed across multiple workspaces on the same server. + +
+ Application registration details +
+ +Click **View installed app** to see the workspace install. The **About** tab shows version and management options. + +
+ Installed app +
+ +**After this phase:** you have a live development loop. Edit any file in `src/` and it appears in the UI. + +### One-shot sync for CI and scripts + +Pass `--once` to run a single build + sync and exit — same pipeline, no watcher: + +```bash filename="Terminal" +yarn twenty dev --once +``` + +| Command | Behavior | When to use | +| ------------------------ | ------------------------------------------------------------- | ---------------------------------------------------- | +| `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. | +| `yarn twenty dev --once` | Single build + sync, exits `0` on success, `1` on failure. | CI, pre-commit hooks, AI agents, scripted workflows. | + +Both modes need a server in development mode and an authenticated remote. + + +Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests — use `yarn twenty deploy` to deploy to production servers. See [Publishing](/l/de/developers/extend/apps/operations/publishing). + + +--- + +## Starting from an example + +Use `--example` to start with a more complete project (custom objects, fields, logic functions, front components): + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app --example postcard +``` + +Examples live in [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). You can also scaffold individual entities into an existing project with `yarn twenty add` — see [Scaffolding](/l/de/developers/extend/apps/getting-started/scaffolding). + +--- + +## What you can build + +Apps are composed of **entities** — each defined as a TypeScript file with a single `export default`: + +| Entity | What it does | +| ---------------------- | ----------------------------------------------------------------------------------- | +| **Objects & Fields** | Custom data models (Post Card, Invoice, etc.) with typed fields | +| **Logic functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events | +| **Front components** | React components that render inside Twenty's UI (side panel, widgets, command menu) | +| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants | +| **Views & Navigation** | Pre-configured list views and sidebar menu items | +| **Page layouts** | Custom record detail pages with tabs and widgets | + +Full reference: [Concepts](/l/de/developers/extend/apps/getting-started/concepts). + +## Next steps + + + + Application identity, default role, install hooks, public assets. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/getting-started/scaffolding.mdx b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/scaffolding.mdx new file mode 100644 index 0000000000..b2853cadf2 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/scaffolding.mdx @@ -0,0 +1,58 @@ +--- +title: Scaffolding +description: Generate entity files interactively with yarn twenty add — objects, fields, views, logic functions, and more. +icon: wand-magic-sparkles +--- + +Instead of creating entity files by hand, use the interactive scaffolder: + +```bash filename="Terminal" +yarn twenty add +``` + +It prompts you to pick an entity type and walks you through the required fields, then writes a ready-to-use file with a stable `universalIdentifier` and the correct `defineEntity()` call. + +You can also pass the entity type directly to skip the first prompt: + +```bash filename="Terminal" +yarn twenty add object +yarn twenty add logicFunction +yarn twenty add frontComponent +``` + +## Available entity types + +| Entity type | Command | Generated file | +| -------------------- | ------------------------------------ | ------------------------------------------------------- | +| Object | `yarn twenty add object` | `src/objects/\.ts` | +| Field | `yarn twenty add field` | `src/fields/\.ts` | +| Logic function | `yarn twenty add logicFunction` | `src/logic-functions/\.ts` | +| Front component | `yarn twenty add frontComponent` | `src/front-components/\.tsx` | +| Role | `yarn twenty add role` | `src/roles/\.ts` | +| Skill | `yarn twenty add skill` | `src/skills/\.ts` | +| Agent | `yarn twenty add agent` | `src/agents/\.ts` | +| View | `yarn twenty add view` | `src/views/\.ts` | +| Navigation menu item | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\.ts` | +| Page layout | `yarn twenty add pageLayout` | `src/page-layouts/\.ts` | + +## What the scaffolder generates + +Each entity type has its own template. For example, `yarn twenty add object` asks for: + +1. **Name (singular)** — e.g., `invoice` +2. **Name (plural)** — e.g., `invoices` +3. **Label (singular)** — auto-populated from the name (e.g., `Invoice`) +4. **Label (plural)** — auto-populated (e.g., `Invoices`) +5. **Create a view and navigation item?** — if you answer yes, the scaffolder also generates a matching view and sidebar link for the new object. + +Other entity types have simpler prompts — most only ask for a name. + +The `field` entity type is more detailed: it asks for the field name, label, type (from a list of all available field types like `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.), and the target object's `universalIdentifier`. + +## Custom output path + +Use the `--path` flag to place the generated file in a custom location: + +```bash filename="Terminal" +yarn twenty add logicFunction --path src/custom-folder +``` diff --git a/packages/twenty-docs/l/de/developers/extend/apps/getting-started/troubleshooting.mdx b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/troubleshooting.mdx new file mode 100644 index 0000000000..0a44d31c22 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/getting-started/troubleshooting.mdx @@ -0,0 +1,12 @@ +--- +title: Fehlerbehebung +description: Häufige Probleme beim ersten Start — Docker, Node-Version, Yarn, Abhängigkeiten. +icon: Schraubenschlüssel +--- + +* **Docker-Fehler** — Stellen Sie sicher, dass Docker Desktop (oder der Daemon) läuft, bevor Sie `yarn twenty server start` ausführen. Die Fehlermeldung zeigt den richtigen Startbefehl für Ihr Betriebssystem an. +* **Falsche Node-Version** — 24+ erforderlich. Prüfen Sie mit `node -v`. +* **Yarn 4 fehlt** — Führen Sie `corepack enable` aus. +* **Abhängigkeiten defekt** — `rm -rf node_modules && yarn install`. + +Hängen Sie fest? Bitten Sie im [Twenty-Discord](https://discord.com/channels/1130383047699738754/1130386664812982322) um Hilfe. diff --git a/packages/twenty-docs/l/de/developers/extend/apps/layout/command-menu-items.mdx b/packages/twenty-docs/l/de/developers/extend/apps/layout/command-menu-items.mdx new file mode 100644 index 0000000000..292f6ad415 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/layout/command-menu-items.mdx @@ -0,0 +1,144 @@ +--- +title: Command Menu Items +description: Surface front components as quick actions and command menu (Cmd+K) entries with defineCommandMenuItem. +icon: terminal +--- + +A **command menu item** is the bridge between the user and a [front component](/l/de/developers/extend/apps/layout/front-components). It registers the component in Twenty's command menu (Cmd+K) and, optionally, as a pinned quick-action button in the top-right corner of the page. + +```ts src/command-menu-items/open-dashboard.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + label: 'Open Dashboard', + shortLabel: 'Dashboard', + icon: 'IconLayoutDashboard', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +## Configuration fields + +| Field | Required | Description | +| --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `universalIdentifier` | Yes | Stable unique ID for the command | +| `label` | Yes | Full label shown in the command menu (Cmd+K) | +| `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens | +| `shortLabel` | No | Shorter label displayed on the pinned quick-action button | +| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) | +| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page | +| `availabilityType` | No | Controls where the command appears: `'GLOBAL'` (always available), `'RECORD_SELECTION'` (only when records are selected), or `'FALLBACK'` (shown when no other commands match) | +| `availabilityObjectUniversalIdentifier` | No | Restrict the command to pages of a specific object type (e.g. only on Company records) | +| `conditionalAvailabilityExpression` | No | A boolean expression that dynamically controls visibility (see below) | + +## Headless commands + +A command menu item paired with a [headless front component](/l/de/developers/extend/apps/layout/front-components#headless-vs-non-headless) is the idiomatic way to ship a one-click action — run code, navigate, or confirm and execute. The Front Components page covers the [SDK Command components](/l/de/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) that handle the action-and-unmount pattern. + +A typical flow: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +## Conditional availability expressions + +The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions: + +```ts src/command-menu-items/bulk-update.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; +import { + objectPermissions, + everyEquals, +} from 'twenty-sdk/front-component'; + +export default defineCommandMenuItem({ + universalIdentifier: '...', + label: 'Bulk Update', + availabilityType: 'RECORD_SELECTION', + frontComponentUniversalIdentifier: '...', + conditionalAvailabilityExpression: everyEquals( + objectPermissions, + 'canUpdateObjectRecords', + true, + ), +}); +``` + +### Context variables + +These represent the current state of the page: + +| Variable | Type | Description | +| ------------------------------ | --------- | ---------------------------------------------------------------- | +| `pageType` | `string` | Current page type (e.g. `'RecordIndexPage'`, `'RecordShowPage'`) | +| `isInSidePanel` | `boolean` | Whether the component is rendered in a side panel | +| `numberOfSelectedRecords` | `number` | Number of currently selected records | +| `isSelectAll` | `boolean` | Whether "select all" is active | +| `selectedRecords` | `array` | The selected record objects | +| `favoriteRecordIds` | `array` | IDs of favorited records | +| `objectPermissions` | `object` | Permissions for the current object type | +| `targetObjectReadPermissions` | `object` | Read permissions for the target object | +| `targetObjectWritePermissions` | `object` | Write permissions for the target object | +| `featureFlags` | `object` | Active feature flags | +| `objectMetadataItem` | `object` | Metadata of the current object type | +| `hasAnySoftDeleteFilterOnView` | `boolean` | Whether the current view has a soft-delete filter | + +### Operators + +Combine variables into boolean expressions: + +| Operator | Description | +| ----------------------------------- | ----------------------------------------------------------------- | +| `isDefined(value)` | `true` if the value is not null/undefined | +| `isNonEmptyString(value)` | `true` if the value is a non-empty string | +| `includes(array, value)` | `true` if the array contains the value | +| `includesEvery(array, prop, value)` | `true` if every item's property includes the value | +| `every(array, prop)` | `true` if the property is truthy on every item | +| `everyDefined(array, prop)` | `true` if the property is defined on every item | +| `everyEquals(array, prop, value)` | `true` if the property equals the value on every item | +| `some(array, prop)` | `true` if the property is truthy on at least one item | +| `someDefined(array, prop)` | `true` if the property is defined on at least one item | +| `someEquals(array, prop, value)` | `true` if the property equals the value on at least one item | +| `someNonEmptyString(array, prop)` | `true` if the property is a non-empty string on at least one item | +| `none(array, prop)` | `true` if the property is falsy on every item | +| `noneDefined(array, prop)` | `true` if the property is undefined on every item | +| `noneEquals(array, prop, value)` | `true` if the property does not equal the value on any item | diff --git a/packages/twenty-docs/l/de/developers/extend/apps/layout/front-components.mdx b/packages/twenty-docs/l/de/developers/extend/apps/layout/front-components.mdx new file mode 100644 index 0000000000..ea9652069f --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/layout/front-components.mdx @@ -0,0 +1,404 @@ +--- +title: Front Components +description: Build React components that render inside Twenty's UI with sandboxed isolation. +icon: window-maximize +--- + +Front components are React components that render directly inside Twenty's UI. They run in an **isolated Web Worker** using Remote DOM — your code is sandboxed but renders natively in the page, not in an iframe. + +## Where front components can be used + +Front components can render in two locations within Twenty: + +* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu. +* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside [page layouts](/l/de/developers/extend/apps/layout/page-layouts). When configuring a dashboard or a record page layout, users can add a front component widget. + +A front component on its own isn't reachable from the UI — you need to *surface* it. The two ways to do that are: + +* **Pair it with a [command menu item](/l/de/developers/extend/apps/layout/command-menu-items)** — registers it in the command menu (Cmd+K) and, optionally, as a pinned quick-action. +* **Embed it as a widget in a [page layout](/l/de/developers/extend/apps/layout/page-layouts)** — places it on a record's detail page or dashboard. + +## Basic example + +The quickest way to see a front component in action is to pair it with a [`defineCommandMenuItem`](/l/de/developers/extend/apps/layout/command-menu-items), so it appears as a quick-action button in the top-right corner of the page: + +```tsx src/front-components/hello-world.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; + +const HelloWorld = () => { + return ( +
+

Hello from my app!

+

This component renders inside Twenty.

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', + name: 'hello-world', + description: 'A simple front component', + component: HelloWorld, +}); +``` + +```ts src/command-menu-items/hello-world.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345', + shortLabel: 'Hello', + label: 'Hello World', + icon: 'IconBolt', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +After syncing with `yarn twenty dev` (or running a one-shot `yarn twenty dev --once`), the quick action appears in the top-right corner of the page: + +
+ Quick action button in the top-right corner +
+ +Click it to render the component inline. + +## Configuration fields + +| Field | Required | Description | +| --------------------- | -------- | ------------------------------------------------------------ | +| `universalIdentifier` | Yes | Stable unique ID for this component | +| `component` | Yes | A React component function | +| `name` | No | Display name | +| `description` | No | Description of what the component does | +| `isHeadless` | No | Set to `true` if the component has no visible UI (see below) | + +## Placing a front component on a page + +Beyond commands, you can embed a front component directly into a record page by adding it as a widget in a **page layout**. See [Page Layouts](/l/de/developers/extend/apps/layout/page-layouts) for details. + +## Headless vs non-headless + +Front components come in two rendering modes controlled by the `isHeadless` option: + +**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted. + +**Headless (`isHeadless: true`)** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below. + +```tsx src/front-components/sync-tracker.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component'; +import { useEffect } from 'react'; + +const SyncTracker = () => { + const recordId = useRecordId(); + + useEffect(() => { + enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' }); + }, [recordId]); + + return null; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'sync-tracker', + description: 'Tracks record views silently', + isHeadless: true, + component: SyncTracker, +}); +``` + +Because the component returns `null`, Twenty skips rendering a container for it — no empty space appears in the layout. The component still has access to all hooks and the host communication API. + +## SDK Command components + +The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done. + +Import them from `twenty-sdk/command`: + +* **`Command`** — Runs an async callback via the `execute` prop. +* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`. +* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`. +* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`. + +Here is a full example of a headless front component using `Command` to run an action from the command menu: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +And an example using `CommandModal` to ask for confirmation before executing: + +```tsx src/front-components/delete-draft.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { CommandModal } from 'twenty-sdk/command'; + +const DeleteDraft = () => { + const execute = async () => { + // perform the deletion + }; + + return ( + + ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456', + name: 'delete-draft', + description: 'Deletes a draft with confirmation', + component: DeleteDraft, + isHeadless: true, +}); +``` + +## Accessing runtime context + +Inside your component, use SDK hooks to access the current user, record, and component instance: + +```tsx src/front-components/record-info.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { + useUserId, + useRecordId, + useFrontComponentId, +} from 'twenty-sdk/front-component'; + +const RecordInfo = () => { + const userId = useUserId(); + const recordId = useRecordId(); + const componentId = useFrontComponentId(); + + return ( +
+

User: {userId}

+

Record: {recordId ?? 'No record context'}

+

Component: {componentId}

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012', + name: 'record-info', + component: RecordInfo, +}); +``` + +Available hooks: + +| Hook | Returns | Description | +| --------------------------------------------- | ------------------ | ---------------------------------------------------------- | +| `useUserId()` | `string` or `null` | The current user's ID | +| `useSelectedRecordIds()` | `string[]` | All selected record IDs (empty array if none selected) | +| `useRecordId()` | `string` or `null` | **Deprecated.** Use `useSelectedRecordIds()` instead | +| `useFrontComponentId()` | `string` | This component instance's ID | +| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function | + +## Host communication API + +Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`: + +| Function | Description | +| ----------------------------------------------- | ----------------------------- | +| `navigate(to, params?, queryParams?, options?)` | Navigate to a page in the app | +| `openSidePanelPage(params)` | Open a side panel | +| `closeSidePanel()` | Close the side panel | +| `openCommandConfirmationModal(params)` | Show a confirmation dialog | +| `enqueueSnackbar(params)` | Show a toast notification | +| `unmountFrontComponent()` | Unmount the component | +| `updateProgress(progress)` | Update a progress indicator | + +Here is an example that uses the host API to show a snackbar and close the side panel after an action completes: + +```tsx src/front-components/archive-record.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const ArchiveRecord = () => { + const recordId = useRecordId(); + + const handleArchive = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { status: 'ARCHIVED' } }, + id: true, + }, + }); + + await enqueueSnackbar({ + message: 'Record archived', + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Archive this record?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678', + name: 'archive-record', + description: 'Archives the current record', + component: ArchiveRecord, +}); +``` + +### Working with multiple records + +Use `useSelectedRecordIds()` to handle multiple selected records. This is useful for bulk operations: + +```tsx src/front-components/bulk-export.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const BulkExport = () => { + const selectedRecordIds = useSelectedRecordIds(); + + const handleExport = async () => { + const client = new CoreApiClient(); + + for (const recordId of selectedRecordIds) { + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { exported: true } }, + id: true, + }, + }); + } + + await enqueueSnackbar({ + message: `Exported ${selectedRecordIds.length} records`, + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Export {selectedRecordIds.length} selected record(s)?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901', + name: 'bulk-export', + description: 'Export selected records', + component: BulkExport, + command: { + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902', + label: 'Bulk Export', + availabilityType: 'RECORD_SELECTION', + conditionalAvailabilityExpression: numberOfSelectedRecords > 0, + }, +}); +``` + +## Public assets + +Front components can access files from the app's `public/` directory using `getPublicAssetUrl`: + +```tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +const Logo = () => Logo; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'logo', + component: Logo, +}); +``` + +See the [public assets section](/l/de/developers/extend/apps/config/public-assets) for details. + +## Styling + +Front components support multiple styling approaches. You can use: + +* **Inline styles** — `style={{ color: 'red' }}` +* **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more) +* **Emotion** — CSS-in-JS with `@emotion/react` +* **Styled-components** — `styled.div` patterns +* **Tailwind CSS** — utility classes +* **Any CSS-in-JS library** compatible with React + +```tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Button, Tag, Status } from 'twenty-sdk/ui'; + +const StyledWidget = () => { + return ( +
+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456', + name: 'styled-widget', + component: StyledWidget, +}); +``` diff --git a/packages/twenty-docs/l/de/developers/extend/apps/layout/navigation-menu-items.mdx b/packages/twenty-docs/l/de/developers/extend/apps/layout/navigation-menu-items.mdx new file mode 100644 index 0000000000..bbfca36165 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/layout/navigation-menu-items.mdx @@ -0,0 +1,44 @@ +--- +title: Navigation Menu Items +description: Add custom entries to the workspace sidebar — links to saved views or external URLs. +icon: Balken +--- + +A **navigation menu item** is an entry in the left sidebar. Use `defineNavigationMenuItem()` to ship custom sidebar links — typically one per [view](/l/de/developers/extend/apps/layout/views) you ship — or to point at external URLs. + +```ts src/navigation-menu-items/example-navigation-menu-item.ts +import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define'; +import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view'; + +export default defineNavigationMenuItem({ + universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c', + name: 'example-navigation-menu-item', + icon: 'IconList', + color: 'blue', + position: 0, + type: NavigationMenuItemType.VIEW, + viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER, +}); +``` + +## Hauptpunkte + +* `type` determines what the menu item links to. Each type pairs with a specific identifier field: + + | Typ | Was es tut | Required field | + | ------------------------------------ | ------------------------------------ | ----------------------------------------------------------------------------- | + | `NavigationMenuItemType.VIEW` | Opens a saved view | `viewUniversalIdentifier` | + | `NavigationMenuItemType.LINK` | Opens an external URL | `link` | + | `NavigationMenuItemType.FOLDER` | Groups nested items under a label | `name` (and child items reference the folder via `folderUniversalIdentifier`) | + | `NavigationMenuItemType.OBJECT` | Opens an object's default index page | `targetObjectUniversalIdentifier` | + | `NavigationMenuItemType.PAGE_LAYOUT` | Opens a standalone page layout | `pageLayoutUniversalIdentifier` | + +* `position` controls ordering in the sidebar. + +* `icon` and `color` are optional and customize how the entry looks. + +* `folderUniversalIdentifier` is also available on any item to nest it inside a `FOLDER`-type parent. + + +**Common pitfall:** creating an object without an associated view + navigation menu item makes that object invisible to users. Unless it's a technical/internal object, every custom object should have a default view *and* a sidebar entry pointing at it. + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/layout/overview.mdx b/packages/twenty-docs/l/de/developers/extend/apps/layout/overview.mdx new file mode 100644 index 0000000000..3abcc1608c --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/layout/overview.mdx @@ -0,0 +1,56 @@ +--- +title: Overview +description: Place your app inside Twenty's UI — sidebar entries, saved views, record page tabs, and sandboxed React components. +icon: table-columns +--- + +A Twenty app's **layout layer** is everything the user sees: where the app surfaces in the sidebar, which list views it ships, how its record detail pages are arranged, and which custom React components render inside those pages. + +```text + Sidebar Record list Record detail page + ─────── ─────────── ────────────────── + [📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐ + [📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │ + [📋 Inbox ] │ ──────── │ │ [Notes ] │ + ▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab + │ │ Acme │ │ │ adds a tab... + └ defineNavi- │ … │ │ ┌────────────────┐ │ + gationMenu- └────▲─────┘ │ │ │ │ + Item points │ │ │ React UI │◀── …with a + to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent + └ defineView │ │ a Worker) │ │ widget inside + picks columns │ └────────────────┘ │ + and filters └─────────────────────┘ +``` + +## In this section + + + + `defineView` — saved list configurations: visible columns, filters, groups. + + + `defineNavigationMenuItem` — sidebar entries pointing at views or external URLs. + + + `definePageLayout` and `definePageLayoutTab` — tabs and widgets on a record's detail page. + + + `defineFrontComponent` — sandboxed React components that render inside Twenty. + + + `defineCommandMenuItem` — register front components as Cmd+K entries and quick actions. + + + +## Where the app surfaces + +| Surface | What it controls | Entity | +| --------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------- | +| **Sidebar** | A custom entry linking to a saved view or external URL | `defineNavigationMenuItem` | +| **Record list** | A saved configuration for an object — visible columns, order, filters, groups | `defineView` | +| **Record detail page** | The tabs and widgets on a record page (your own object's, or a standard one) | `definePageLayout`, `definePageLayoutTab` | +| **Inside any of the above** | A custom React widget — buttons, forms, dashboards, integrations | `defineFrontComponent` | +| **Command menu (Cmd+K)** | A pinned quick action or hidden command | `defineCommandMenuItem` | + +Front components run inside an isolated Web Worker using Remote DOM — they render *natively* in the page (not inside an iframe), but cannot reach the host page or DOM directly. Communication with Twenty happens through a message-passing host API. diff --git a/packages/twenty-docs/l/de/developers/extend/apps/layout/page-layouts.mdx b/packages/twenty-docs/l/de/developers/extend/apps/layout/page-layouts.mdx new file mode 100644 index 0000000000..6062b4df63 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/layout/page-layouts.mdx @@ -0,0 +1,102 @@ +--- +title: Page Layouts +description: Customize record detail pages — tabs, widgets, and where front components render — using definePageLayout and definePageLayoutTab. +icon: table-columns +--- + +A **page layout** controls how a record's detail page is arranged: which tabs appear and what widgets they contain. Use `definePageLayout()` to declare a layout for an object you own, or `definePageLayoutTab()` to add a single tab to a layout that already exists (yours or a standard Twenty one). + +| Use case | Entity | +| ---------------------------------------------------------------------- | --------------------- | +| Define the entire layout for a record page on an object you own | `definePageLayout` | +| Add one tab to an existing layout (your own object, or a standard one) | `definePageLayoutTab` | + +## definePageLayout + +Use this when you own the entire detail page — typically for a custom object you defined yourself. + +```ts src/page-layouts/example-record-page-layout.ts +import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +export default definePageLayout({ + universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134', + name: 'Example Record Page', + type: 'RECORD_PAGE', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + tabs: [ + { + universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5', + title: 'Hello World', + position: 50, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], + }, + ], +}); +``` + +### Key points + +* `type` is typically `'RECORD_PAGE'` to customize the detail view of a specific object. +* `objectUniversalIdentifier` specifies which object this layout applies to. +* Each `tab` defines a section of the page with a `title`, `position`, and `layoutMode` (`CANVAS` for free-form layout). +* Each `widget` inside a tab can render a [front component](/l/de/developers/extend/apps/layout/front-components), a relation list, or other built-in widget types. +* `position` on tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones. + +## definePageLayoutTab + +Use this when you only want to **add** a tab to an existing layout — for example, an analytics tab on the standard Company page, or an AI summary tab attached to your own object's layout. + +```ts src/page-layouts/example-extra-tab.ts +import { + definePageLayoutTab, + PageLayoutTabLayoutMode, +} from 'twenty-sdk/define'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = + '20202020-ab01-4001-8001-c0aba11c0100'; + +export default definePageLayoutTab({ + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001', + pageLayoutUniversalIdentifier: + COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER, + title: 'Hello World', + position: 1000, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], +}); +``` + +### Key points + +* `pageLayoutUniversalIdentifier` is **required** and must point to a page layout that already exists at install time — either a standard Twenty layout or one defined by your own app. Cross-app references to layouts owned by another installed app are not supported today. When the parent layout is missing, installation fails with a clear validation error. +* `widgets` are scoped to this tab only — they reference [front components](/l/de/developers/extend/apps/layout/front-components), views, etc. exactly like widgets defined inline in `definePageLayout`. +* `position` controls ordering against existing tabs on the targeted layout. Pick a value that places your tab where you want it relative to built-in tabs. +* Use this instead of `definePageLayout` when you only want to add to an existing layout. Use `definePageLayout` when you own the entire layout. diff --git a/packages/twenty-docs/l/de/developers/extend/apps/layout/views.mdx b/packages/twenty-docs/l/de/developers/extend/apps/layout/views.mdx new file mode 100644 index 0000000000..6fe6e52474 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/layout/views.mdx @@ -0,0 +1,43 @@ +--- +title: Views +description: Ship pre-configured saved views — column order, filters, groups — for objects in your app. +icon: list +--- + +A **view** is a saved configuration for how records of an object are displayed: which fields appear, their order, whether they're visible, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app — typically a default index view for each custom object you create. + +```ts src/views/example-view.ts +import { defineView, ViewKey } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; + +export default defineView({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'All example items', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + icon: 'IconList', + key: ViewKey.INDEX, + position: 0, + fields: [ + { + universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0', + fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER, + position: 0, + isVisible: true, + size: 200, + }, + ], +}); +``` + +## Key points + +* `objectUniversalIdentifier` specifies which object this view applies to. It can be a custom object you defined or a standard Twenty object. +* `key` determines the view type — `ViewKey.INDEX` is the main list view for the object. +* `fields` controls which columns appear and in what order. Each field references a `fieldMetadataUniversalIdentifier`. +* You can also declare `filters`, `filterGroups`, `groups`, and `fieldGroups` for advanced configurations. +* `position` controls ordering when multiple views exist for the same object. + +## How views show up in the UI + +A view by itself isn't reachable from the sidebar. To make it appear there, pair it with a [navigation menu item](/l/de/developers/extend/apps/layout/navigation-menu-items) of type `VIEW` that points at the view's `universalIdentifier`. That's the canonical pattern: every custom object typically ships a default view + a sidebar entry that opens it. diff --git a/packages/twenty-docs/l/de/developers/extend/apps/logic/connections.mdx b/packages/twenty-docs/l/de/developers/extend/apps/logic/connections.mdx new file mode 100644 index 0000000000..1a99c59742 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/logic/connections.mdx @@ -0,0 +1,193 @@ +--- +title: Connections +description: Let your app act on a user's behalf in third-party services via OAuth. +icon: plug +--- + +Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API. + +Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate. + + + + + +A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace. + +A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials. + +```ts src/connection-providers/linear-connection.ts +import { defineConnectionProvider } from 'twenty-sdk/define'; + +export default defineConnectionProvider({ + universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f', + name: 'linear', + displayName: 'Linear', + icon: 'IconBrandLinear', + type: 'oauth', + oauth: { + authorizationEndpoint: 'https://linear.app/oauth/authorize', + tokenEndpoint: 'https://api.linear.app/oauth/token', + scopes: ['read', 'write'], + // These must match keys in `defineApplication.serverVariables` below. + clientIdVariable: 'LINEAR_CLIENT_ID', + clientSecretVariable: 'LINEAR_CLIENT_SECRET', + // Optional: defaults to 'json'. Some providers (Linear, Slack) want + // 'form-urlencoded' for the token request. + tokenRequestContentType: 'form-urlencoded', + // Optional: defaults to true. Disable only if the provider rejects PKCE. + usePkce: false, + // Optional: extra query params on the authorize URL. + // authorizationParams: { prompt: 'consent' }, + // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect. + // revokeEndpoint: 'https://example.com/oauth/revoke', + }, +}); +``` + +```ts src/application.config.ts +import { defineApplication } from 'twenty-sdk/define'; + +export default defineApplication({ + universalIdentifier: '...', + displayName: 'Linear', + description: 'Connect Linear to Twenty.', + defaultRoleUniversalIdentifier: '...', + // OAuth client credentials live on the app registration (one OAuth app per + // Twenty server, configured by the admin) — not per-workspace. Declare them + // as serverVariables so the admin can fill them in once for all installs. + serverVariables: { + LINEAR_CLIENT_ID: { + description: 'OAuth client ID from your Linear OAuth application.', + isSecret: false, + isRequired: true, + }, + LINEAR_CLIENT_SECRET: { + description: 'OAuth client secret from your Linear OAuth application.', + isSecret: true, + isRequired: true, + }, + }, +}); +``` + +Key points: + +* `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`). +* `displayName` shows in the per-app settings tab and in the AI tool list. +* `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo. +* Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server. +* Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled. +* `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`. + +The OAuth callback URL your provider needs to whitelist is: + +``` +https:///apps/oauth/callback +``` + + + + + +Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens. + +```ts src/logic-functions/handlers/create-linear-issue-handler.ts +import { listConnections } from 'twenty-sdk/logic-function'; + +export const createLinearIssueHandler = async (input: { + teamId?: string; + title?: string; +}) => { + if (!input.teamId || !input.title) { + return { success: false, error: 'teamId and title are required' }; + } + + const connections = await listConnections({ providerName: 'linear' }); + + // Workspace-shared credentials win when present; fall back to the first + // user-visibility one. For HTTP-route triggers you typically pick the + // request user's connection via event.userWorkspaceId instead. + const connection = + connections.find((c) => c.visibility === 'workspace') ?? connections[0]; + + if (!connection) { + return { + success: false, + error: + 'Linear is not connected. Open the app settings and click "Add connection".', + }; + } + + // Use connection.accessToken to call the third-party API. + const response = await fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + Authorization: `Bearer ${connection.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`, + }), + }); + + return { success: response.ok }; +}; +``` + +Each connection has: + +| Field | Description | +| ----------------- | -------------------------------------------------------------------------------------------------------- | +| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one | +| `visibility` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) | +| `scopes` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) | +| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers | +| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) | +| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) | +| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect | + +Key points: + +* Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers. +* The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set). +* `getConnection(id)` is the single-row equivalent. + + + + + +When a user clicks "Add connection," they're prompted to pick a visibility: + +* **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not. +* **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user. + +Use the right one for each handler: + +```ts +// HTTP-route trigger — prefer the request user's own connection. +const conn = + connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ?? + connections.find((c) => c.visibility === 'workspace'); + +// Cron trigger — no request user; only shared credentials are sensible. +const conn = connections.find((c) => c.visibility === 'workspace'); +``` + +Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side. + + + + + +For each connection provider, the server admin needs to register an OAuth app at the third party first. + +1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new). +2. Set the **Redirect URI** to `\/apps/oauth/callback`. +3. Copy the generated **Client ID** and **Client Secret**. +4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`. +5. Workspace members can then add connections from the per-app **Connections** section. + + + + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/logic/logic-functions.mdx b/packages/twenty-docs/l/de/developers/extend/apps/logic/logic-functions.mdx new file mode 100644 index 0000000000..06ca5c4d70 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/logic/logic-functions.mdx @@ -0,0 +1,375 @@ +--- +title: Logic Functions +description: Define server-side TypeScript functions with HTTP, cron, and database event triggers. +icon: bolt +--- + +Logic functions are server-side TypeScript functions that run on the Twenty platform. They can be triggered by HTTP requests, cron schedules, or database events — and can also be exposed as tools for AI agents. + + + + +Each function file uses `defineLogicFunction()` to export a configuration with a handler and optional triggers. + +```ts src/logic-functions/createPostCard.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define'; +import { CoreApiClient, type Person } from 'twenty-client-sdk/core'; + +const handler = async (params: RoutePayload) => { + const client = new CoreApiClient(); + const body = (params.body ?? {}) as { name?: string }; + const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'; + + const result = await client.mutation({ + createPostCard: { + __args: { data: { name } }, + id: true, + name: true, + }, + }); + return result; +}; + +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'create-new-post-card', + timeoutSeconds: 2, + handler, + httpRouteTriggerSettings: { + path: '/post-card/create', + httpMethod: 'POST', + isAuthRequired: true, + }, + /*databaseEventTriggerSettings: { + eventName: 'people.created', + },*/ + /*cronTriggerSettings: { + pattern: '0 0 1 1 *', + },*/ +}); +``` + +Available trigger types: +* **httpRoute**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**: +> e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create` +* **cron**: Runs your function on a schedule using a CRON expression. +* **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function. +> e.g. `person.updated`, `*.created`, `company.*` + + +You can also manually execute a function using the CLI: + +```bash filename="Terminal" +yarn twenty exec -n create-new-post-card -p '{"key": "value"}' +``` + +```bash filename="Terminal" +yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + +You can watch logs with: + +```bash filename="Terminal" +yarn twenty logs +``` + + +#### Route trigger payload + +When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the +[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). +Import the `RoutePayload` type from `twenty-sdk`: + +```ts +import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define'; + +const handler = async (event: RoutePayload) => { + const { headers, queryStringParameters, pathParameters, body } = event; + const { method, path } = event.requestContext.http; + + return { message: 'Success' }; +}; +``` + +The `RoutePayload` type has the following structure: + + | Property | Type | Description | Example | + | ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | + | `headers` | `Record\` | HTTP headers (only those listed in `forwardedRequestHeaders`) | see section below | + | `queryStringParameters` | `Record\` | Query string parameters (multiple values joined with commas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` | + | `pathParameters` | `Record\` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` | + | `body` | `object \| null` | Parsed request body (JSON) | `{ id: 1 }` -> `{ id: 1 }` | + | `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | | + | `isBase64Encoded` | `boolean` | Whether the body is base64 encoded | | + | `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) | | + | `requestContext.http.path` | `string` | Raw request path | | + + +#### forwardedRequestHeaders + +By default, HTTP headers from incoming requests are **not** passed to your logic function for security reasons. +To access specific headers, list them in the `forwardedRequestHeaders` array: + +```ts +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'webhook-handler', + handler, + httpRouteTriggerSettings: { + path: '/webhook', + httpMethod: 'POST', + isAuthRequired: false, + forwardedRequestHeaders: ['x-webhook-signature', 'content-type'], + }, +}); +``` + +In your handler, access the forwarded headers like this: + +```ts +const handler = async (event: RoutePayload) => { + const signature = event.headers['x-webhook-signature']; + const contentType = event.headers['content-type']; + + // Validate webhook signature... + return { received: true }; +}; +``` + + +Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`). + + +#### Exposing a function as an AI tool or workflow action + +Logic functions can be exposed on two surfaces, each with its own trigger: + +* **`toolTriggerSettings`** — makes the function discoverable by Twenty's AI features (chat, MCP, function calling). Uses standard JSON Schema, the format LLMs natively understand. +* **`workflowActionTriggerSettings`** — makes the function appear as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper field editors, variable pickers, and labels. + +A function can opt into one, the other, or both. They sit alongside `cronTriggerSettings`, `databaseEventTriggerSettings`, and `httpRouteTriggerSettings` — same pattern, same shape. + +```ts src/logic-functions/enrich-company.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const handler = async (params: { companyName: string; domain?: string }) => { + const client = new CoreApiClient(); + + 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, + toolTriggerSettings: {}, +}); +``` + +Key points: + +* A function can mix surfaces — declare both `toolTriggerSettings` and `workflowActionTriggerSettings` to expose it in chat AND in the workflow builder. +* `toolTriggerSettings.inputSchema` and `workflowActionTriggerSettings.inputSchema` are both optional. When omitted, the manifest builder infers them from the handler source code (JSON Schema for the AI tool, Twenty's `InputSchema` for the workflow action). Provide one explicitly when you want richer typing — for example, with `FieldMetadataType`-aware fields like `CURRENCY` or `RELATION` for the workflow builder, or with `description` fields the AI agent can read: + +```ts +export default defineLogicFunction({ + ..., + toolTriggerSettings: { + inputSchema: { + 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'], + }, + }, +}); +``` + + +**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. + + + + + + +**Install hooks** — pre-install and post-install handlers — share this runtime but are declared with their own define functions and don't take trigger settings. See [Install Hooks](/l/de/developers/extend/apps/config/install-hooks) for `definePreInstallLogicFunction` and `definePostInstallLogicFunction`. + + +## Typed API clients (twenty-client-sdk) + +The `twenty-client-sdk` package provides two typed GraphQL clients for interacting with the Twenty API from your logic functions and front components. + +| Client | Import | Endpoint | Generated? | +| ------------------- | ---------------------------- | ---------------------------------------------- | ---------------------- | +| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — workspace data (records, objects) | Yes, at dev/build time | +| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — workspace config, file uploads | No, ships pre-built | + + + + +`CoreApiClient` is the main client for querying and mutating workspace data. It is **generated from your workspace schema** during `yarn twenty dev` or `yarn twenty build`, so it is fully typed to match your objects and fields. + +```ts +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const client = new CoreApiClient(); + +// Query records +const { companies } = await client.query({ + companies: { + edges: { + node: { + id: true, + name: true, + domainName: { + primaryLinkLabel: true, + primaryLinkUrl: true, + }, + }, + }, + }, +}); + +// Create a record +const { createCompany } = await client.mutation({ + createCompany: { + __args: { + data: { + name: 'Acme Corp', + }, + }, + id: true, + name: true, + }, +}); +``` + +The client uses a selection-set syntax: pass `true` to include a field, use `__args` for arguments, and nest objects for relations. You get full autocompletion and type checking based on your workspace schema. + + +**CoreApiClient is generated at dev/build time.** If you use it without running `yarn twenty dev` or `yarn twenty build` first, it throws an error. The generation happens automatically — the CLI introspects your workspace's GraphQL schema and generates a typed client using `@genql/cli`. + + +#### Using CoreSchema for type annotations + +`CoreSchema` provides TypeScript types matching your workspace objects — useful for typing component state or function parameters: + +```ts +import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core'; +import { useState } from 'react'; + +const [company, setCompany] = useState< + Pick | undefined +>(undefined); + +const client = new CoreApiClient(); +const result = await client.query({ + company: { + __args: { filter: { position: { eq: 1 } } }, + id: true, + name: true, + }, +}); +setCompany(result.company); +``` + + + + +`MetadataApiClient` ships pre-built with the SDK (no generation required). It queries the `/metadata` endpoint for workspace configuration, applications, and file uploads. + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; + +const metadataClient = new MetadataApiClient(); + +// List first 10 objects in the workspace +const { objects } = await metadataClient.query({ + objects: { + edges: { + node: { + id: true, + nameSingular: true, + namePlural: true, + labelSingular: true, + isCustom: true, + }, + }, + __args: { + filter: {}, + paging: { first: 10 }, + }, + }, +}); +``` + +#### Uploading files + +`MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields: + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import * as fs from 'fs'; + +const metadataClient = new MetadataApiClient(); + +const fileBuffer = fs.readFileSync('./invoice.pdf'); + +const uploadedFile = await metadataClient.uploadFile( + fileBuffer, // file contents as a Buffer + 'invoice.pdf', // filename + 'application/pdf', // MIME type + '58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier +); + +console.log(uploadedFile); +// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' } +``` + +| Parameter | Type | Description | +| ---------------------------------- | -------- | --------------------------------------------------------------- | +| `fileBuffer` | `Buffer` | The raw file contents | +| `filename` | `string` | The name of the file (used for storage and display) | +| `contentType` | `string` | MIME type (defaults to `application/octet-stream` if omitted) | +| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object | + +Key points: +* Uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed. +* The returned `url` is a signed URL you can use to access the uploaded file. + + + + + + When your code runs on Twenty (logic functions or front components), the platform injects credentials as environment variables: + + * `TWENTY_API_URL` — Base URL of the Twenty API + * `TWENTY_APP_ACCESS_TOKEN` — Short-lived key scoped to your application's default function role + + You do **not** need to pass these to the clients — they read from `process.env` automatically. The API key's permissions are determined by the role referenced in `defaultRoleUniversalIdentifier` in your `application-config.ts`. + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/de/developers/extend/apps/logic/overview.mdx new file mode 100644 index 0000000000..a285970c14 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/logic/overview.mdx @@ -0,0 +1,55 @@ +--- +title: Overview +description: Server-side TypeScript that runs inside Twenty — triggered by HTTP routes, cron schedules, database events, AI tools, or workflow actions. +icon: bolt +--- + +A Twenty app's **logic layer** is the code that *runs* — server-side TypeScript handlers reacting to HTTP requests, cron schedules, and record changes; AI skills and agents that live inside the workspace; and OAuth connections that let your functions act on a user's behalf in third-party services. + +```text + ┌─ HTTP route ──┐ + │ Cron schedule │ + │ Database event │ ┌────────────────────┐ + triggers ─┤ AI tool call ├─────▶│ Logic function │ + │ Workflow action │ │ (your handler) │ + │ Manual exec │ └────────────────────┘ + └────────────────────┘ │ + ▼ + ┌────────────────────────────┐ + │ Twenty API (records) │ + │ Third-party API │ + │ (via Connection token) │ + └────────────────────────────┘ +``` + +## In this section + + + + The core building block — trigger types, payloads, and the typed API client. + + + Reusable AI agent instructions and assistants with custom system prompts. + + + OAuth credentials your app holds for third-party services — Linear, GitHub, Slack, and more. + + + +## Trigger types at a glance + +A logic function picks one or more triggers — every entry below is a separate field on `defineLogicFunction()`: + +| Trigger | When it runs | Setting | +| ------------------- | ---------------------------------------------------------- | ------------------------------- | +| **HTTP route** | A request hits your `/s/\` endpoint | `httpRouteTriggerSettings` | +| **Cron** | A CRON expression matches | `cronTriggerSettings` | +| **Database event** | A workspace record is created, updated, or deleted | `databaseEventTriggerSettings` | +| **AI tool** | A Twenty AI feature decides to call your function | `toolTriggerSettings` | +| **Workflow action** | A workflow step invokes your function | `workflowActionTriggerSettings` | + +Functions run sandboxed in isolated Node.js processes and access the workspace through a typed API client scoped to the role declared on [`defineApplication()`](/l/de/developers/extend/apps/config/application). + + +**Install-time hooks** — code that runs before or after the install — share this runtime but use their own define functions and live under [Config → Install Hooks](/l/de/developers/extend/apps/config/install-hooks). + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/logic/skills-and-agents.mdx b/packages/twenty-docs/l/de/developers/extend/apps/logic/skills-and-agents.mdx new file mode 100644 index 0000000000..82a66ef20f --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/logic/skills-and-agents.mdx @@ -0,0 +1,69 @@ +--- +title: Skills & Agents +description: Define AI skills and agents for your app. +icon: robot +--- + + + Skills and agents are currently in alpha. The feature works but is still evolving. + + +Apps can define AI capabilities that live inside the workspace — reusable skill instructions and agents with custom system prompts. + + + + +Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation: + +```ts src/skills/example-skill.ts +import { defineSkill } from 'twenty-sdk/define'; + +export default defineSkill({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'sales-outreach', + label: 'Sales Outreach', + description: 'Guides the AI agent through a structured sales outreach process', + icon: 'IconBrain', + content: `You are a sales outreach assistant. When reaching out to a prospect: +1. Research the company and recent news +2. Identify the prospect's role and likely pain points +3. Draft a personalized message referencing specific details +4. Keep the tone professional but conversational`, +}); +``` + +Key points: +* `name` is a unique identifier string for the skill (kebab-case recommended). +* `label` is the human-readable display name shown in the UI. +* `content` contains the skill instructions — this is the text the AI agent uses. +* `icon` (optional) sets the icon displayed in the UI. +* `description` (optional) provides additional context about the skill's purpose. + + + + +Agents are AI assistants that live inside your workspace. Use `defineAgent()` to create agents with a custom system prompt: + +```ts src/agents/example-agent.ts +import { defineAgent } from 'twenty-sdk/define'; + +export default defineAgent({ + universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123', + name: 'sales-assistant', + label: 'Sales Assistant', + description: 'Helps the sales team draft outreach emails and research prospects', + icon: 'IconRobot', + prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.', +}); +``` + +Key points: +* `name` is the unique identifier string for the agent (kebab-case recommended). +* `label` is the display name shown in the UI. +* `prompt` is the system prompt that defines the agent's behavior. +* `description` (optional) provides context about what the agent does. +* `icon` (optional) sets the icon displayed in the UI. +* `modelId` (optional) overrides the default AI model used by the agent. + + + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/operations/cli.mdx b/packages/twenty-docs/l/de/developers/extend/apps/operations/cli.mdx new file mode 100644 index 0000000000..0cccebdbb5 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/operations/cli.mdx @@ -0,0 +1,78 @@ +--- +title: CLI +description: yarn twenty commands for executing functions, streaming logs, managing app installations, and switching remotes. +icon: terminal +--- + +Beyond `dev`, `build`, `add`, and `typecheck`, the `yarn twenty` CLI provides commands for executing functions, viewing logs, and managing app installations. + +## Executing functions (`yarn twenty exec`) + +Run a logic function manually without triggering it via HTTP, cron, or database event: + +```bash filename="Terminal" +# Execute by function name +yarn twenty exec -n create-new-post-card + +# Execute by universalIdentifier +yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf + +# Pass a JSON payload +yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}' + +# Execute the post-install function +yarn twenty exec --postInstall +``` + +## Viewing function logs (`yarn twenty logs`) + +Stream execution logs for your app's logic functions: + +```bash filename="Terminal" +# Stream all function logs +yarn twenty logs + +# Filter by function name +yarn twenty logs -n create-new-post-card + +# Filter by universalIdentifier +yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + + +This is different from `yarn twenty server logs`, which shows the Docker container logs. `yarn twenty logs` shows your app's function execution logs from the Twenty server. + + +## Uninstalling an app (`yarn twenty uninstall`) + +Remove your app from the active workspace: + +```bash filename="Terminal" +yarn twenty uninstall + +# Skip the confirmation prompt +yarn twenty uninstall --yes +``` + +## Managing remotes + +A **remote** is a Twenty server that your app connects to. During setup, the scaffolder creates one for you automatically. You can add more remotes or switch between them at any time. + +```bash filename="Terminal" +# Add a new remote (opens a browser for OAuth login) +yarn twenty remote add + +# Connect to a local Twenty server (auto-detects port 2020 or 3000) +yarn twenty remote add --local + +# Add a remote non-interactively (useful for CI) +yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote + +# List all configured remotes +yarn twenty remote list + +# Switch the active remote +yarn twenty remote switch +``` + +Your credentials are stored in `~/.twenty/config.json`. diff --git a/packages/twenty-docs/l/de/developers/extend/apps/operations/overview.mdx b/packages/twenty-docs/l/de/developers/extend/apps/operations/overview.mdx new file mode 100644 index 0000000000..39ab9dda73 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/operations/overview.mdx @@ -0,0 +1,29 @@ +--- +title: Overview +description: Build, test, and ship your app — CLI commands, integration tests, CI, and publishing to a server or to npm. +icon: rocket +--- + +The **operations layer** is everything you do *to* your app rather than *with* it: invoking CLI commands, running integration tests against a real Twenty server, configuring CI, and shipping releases — either as a tarball deployed to a single server or as an npm package listed in the marketplace. + +```text + develop ─▶ test ─▶ build ─▶ deploy / publish + ─────── ──── ───── ───────────────── + yarn yarn yarn yarn twenty deploy (tarball → one server) + twenty test twenty + dev build yarn twenty publish (npm → marketplace) +``` + +## In this section + + + + `yarn twenty` reference — exec, logs, uninstall, remotes. + + + Vitest setup, integration tests, type checking, CI workflow. + + + Build, deploy a tarball, publish to npm, install. + + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/de/developers/extend/apps/operations/publishing.mdx new file mode 100644 index 0000000000..23b8920f94 --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/operations/publishing.mdx @@ -0,0 +1,295 @@ +--- +title: Publishing +icon: upload +description: Distribute your Twenty app to the marketplace or deploy it internally. +--- + +## Overview + +Once your app is [built and tested locally](/l/de/developers/extend/apps/getting-started/concepts), you have two paths for distributing it: + +* **Deploy a tarball** — upload your app directly to a specific Twenty server for internal or private use. +* **Publish to npm** — list your app in the Twenty marketplace for any workspace to discover and install. + +Both paths start from the same **build** step. + +## Building your app + +Run the build command to compile your app and generate a distribution-ready `manifest.json`: + +```bash filename="Terminal" +yarn twenty build +``` + +This compiles TypeScript sources, transpiles logic functions and front components, and writes everything to `.twenty/output/`. Add `--tarball` to also produce a `.tgz` package for manual distribution or the deploy command. + +## Deploying to a server (tarball) + +For apps you don't want publicly available — proprietary tools, enterprise-only integrations, or experimental builds — you can deploy a tarball directly to a Twenty server. + +### Prerequisites + +Before deploying, you need a configured remote pointing to the target server. Remotes store the server URL and authentication credentials locally in `~/.twenty/config.json`. + +Add a remote: + +```bash filename="Terminal" +yarn twenty remote add --api-url https://your-twenty-server.com --as production +``` + +### Deploying + +Build and upload your app to the server in one step: + +```bash filename="Terminal" +yarn twenty deploy +# To deploy to a specific remote: +# yarn twenty deploy --remote production +``` + +### Sharing a deployed app + + +Sharing private (tarball) apps across workspaces is an **Enterprise** feature. The **Distribution** tab will show an upgrade prompt instead of the share controls until your workspace has a valid Enterprise key. See [Settings > Admin Panel > Enterprise](/settings/admin-panel#enterprise) to activate it. + + +Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. Once your workspace is on the Enterprise plan, you can share a deployed app like this: + +1. Go to **Settings > Applications > Registrations** and open your app +2. In the **Distribution** tab, click **Copy share link** +3. Share this link with users on other workspaces — it takes them directly to the app's install page + +The share link uses the server's base URL (without any workspace subdomain) so it works for any workspace on the server. + +### Version management + +When updating an already deployed tarball app, the server requires the `version` in `package.json` to be **strictly higher** (per [semver](https://semver.org) ordering) than the currently deployed version. Re-deploying the same version, or pushing a lower one, is rejected before the tarball is stored — you'll see a `VERSION_ALREADY_EXISTS` error from the CLI. + +To release an update: + +1. Bump the `version` field in your `package.json` (e.g. `1.2.3` → `1.2.4`, `1.3.0`, or `2.0.0`) +2. Run `yarn twenty deploy` (or `yarn twenty deploy --remote production`) +3. Workspaces that have the app installed will see the upgrade available in their settings + + +Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allowed, and a final release like `1.0.0` is correctly recognized as higher than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver string. + + +{/* TODO: add screenshot of the Upgrade button */} + +### Server version compatibility + +If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`: + +```json filename="package.json" +{ + "name": "twenty-my-app", + "version": "1.0.0", + "engines": { + "node": "^24.5.0", + "twenty": ">=2.3.0" + } +} +``` + +The value is a standard [semver range](https://github.com/npm/node-semver#ranges). Common patterns: + +| Range | Meaning | +| ---------------------------------- | ------------------------------------------ | +| `>=2.3.0` | Any server from 2.3.0 onward | +| `>=2.3.0 \<3.0.0` | 2.3.0 or later, but below the next major | +| `^2.3.0` | Same as `>=2.3.0 \<3.0.0` | + +**What happens at deploy and install time:** + +* If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version. +* If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps). +* If the server has no `APP_VERSION` configured, the check is skipped. + + +The server is the authoritative check — it validates `engines.twenty` on both tarball upload and workspace install. If you deploy a tarball out-of-band or install from the marketplace, the server still enforces compatibility. + + +## Automated CI/CD (scaffolded workflows) + +Apps generated with `create-twenty-app` ship with two GitHub Actions workflows out of the box, under `.github/workflows/`. They are ready to run as soon as you push the repo to GitHub — no extra setup is needed for CI, and CD only requires a single secret. + +### CI — `ci.yml` + +Runs integration tests on every push to `main` and every pull request. + +**What it does:** + +1. Checks out your app's source. +2. Spawns an isolated Twenty test instance using the `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` composite action (the CI equivalent of `yarn twenty server start --test`). +3. Enables Corepack, sets up Node.js from your `.nvmrc`, and installs dependencies with `yarn install --immutable`. +4. Runs `yarn test`, passing `TWENTY_API_URL` and `TWENTY_API_KEY` from the spawned instance so your tests can talk to a real server. + +**Config knobs:** + +* `TWENTY_VERSION` (env, defaults to `latest`) — pin the Twenty server version used in CI by editing this in `ci.yml`. +* Concurrency is grouped by `github.ref` and cancels in-progress runs on new pushes. + +No secrets are required — the test instance is ephemeral and lives only for the duration of the job. + +### CD — `cd.yml` + +Deploys your app to a configured Twenty server on every push to `main`, and optionally from a pull request when the `deploy` label is applied. + +**What it does:** + +1. Checks out the PR head (for labeled PRs) or the pushed commit. +2. Runs `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — the CI equivalent of `yarn twenty deploy`. +3. Runs `twentyhq/twenty/.github/actions/install-twenty-app@main` so the newly deployed version is installed into the target workspace. + +**Required configuration:** + +| Setting | Where | Purpose | +| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `TWENTY_DEPLOY_URL` | `env` in `cd.yml` (defaults to `http://localhost:3000`) | The Twenty server to deploy to. Change this to your real server URL before first use. | +| `TWENTY_DEPLOY_API_KEY` | GitHub repo **Settings → Secrets and variables → Actions** | API key with deploy permission on the target server. | + + +The default `TWENTY_DEPLOY_URL` of `http://localhost:3000` is a placeholder — it will not reach anything from a GitHub-hosted runner. Update it to your server's public URL (or use a self-hosted runner with network access) before enabling CD. + + +**Triggering a preview deploy from a PR:** + +Add the `deploy` label to a pull request. The `if:` guard in `cd.yml` will run the job for that PR using the PR's head commit, letting you validate a change on the target server before merging. + +### Pinning the reusable actions + +Both workflows reference reusable actions at `@main`, so action updates in the `twentyhq/twenty` repo are picked up automatically. If you want deterministic builds, replace `@main` with a commit SHA or release tag on each `uses:` line. + +## Publishing to npm + +Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twenty workspace can browse, install, and upgrade marketplace apps directly from the UI. + +### Requirements + +* An [npm](https://www.npmjs.com) account +* The `twenty-app` keyword in your `package.json` `keywords` array (add it manually — it is not included by default in the `create-twenty-app` template) + +```json filename="package.json" +{ + "name": "twenty-app-postcard-sender", + "version": "1.0.0", + "keywords": ["twenty-app"] +} +``` + +### Marketplace metadata + +The `defineApplication()` config supports optional fields that control how your app appears in the marketplace. Use `logoUrl` and `screenshots` to reference images from the `public/` folder: + +```ts src/application-config.ts +export default defineApplication({ + universalIdentifier: '...', + displayName: 'My App', + description: 'A great app', + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + logoUrl: 'public/logo.png', + screenshots: [ + 'public/screenshot-1.png', + 'public/screenshot-2.png', + ], +}); +``` + +See the [defineApplication accordion](/l/de/developers/extend/apps/config/application#marketplace-metadata) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.). + +#### Recommended screenshot dimensions + +The marketplace renders `screenshots` in a fixed `8:5` container (for example, `1600×1000 px`). + + +Screenshots of any aspect ratio are displayed in full and are never cropped, but anything significantly taller or narrower than `8:5` will show empty bands on the sides. + + +### Publish + +```bash filename="Terminal" +yarn twenty publish +``` + +To publish under a specific dist-tag (e.g., `beta` or `next`): + +```bash filename="Terminal" +yarn twenty publish --tag beta +``` + +### How marketplace discovery works + +The Twenty server syncs its marketplace catalog from the npm registry **every hour**. + +You can trigger the sync immediately instead of waiting: + +```bash filename="Terminal" +yarn twenty server catalog-sync +# To target a specific remote: +# yarn twenty server catalog-sync --remote production +``` + +The metadata shown in the marketplace comes from your `defineApplication()` config — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`. + + +If your app does not define an `aboutDescription` in `defineApplication()`, the marketplace will automatically use your package's `README.md` from npm as the about page content. This means you can maintain a single README for both npm and the Twenty marketplace. If you want a different description in the marketplace, explicitly set `aboutDescription`. + + +### CI publishing + +Use this GitHub Actions workflow to publish automatically on every release (uses [OIDC](https://docs.npmjs.com/trusted-publishers)): + +```yaml filename=".github/workflows/publish.yml" +name: Publish +on: + release: + types: [published] + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: https://registry.npmjs.org + - run: yarn install --immutable + - run: npx twenty build + - run: npm publish --provenance --access public + working-directory: .twenty/output +``` + +For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `yarn twenty build`, then `npm publish` from `.twenty/output`. + + +**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions. + + +## Installing apps + +Once an app is published (npm) or deployed (tarball), workspaces can install it through the UI. + +Go to the **Settings > Applications** page in Twenty, where both marketplace and tarball-deployed apps can be browsed and installed. + +{/* TODO: add screenshot of the UI when the app is registered */} + +You can also install apps from the command line: + +```bash filename="Terminal" +yarn twenty install +``` + + +The server enforces semver versioning on install, mirroring the rules on deploy: + +* Installing the same version that is already installed in your workspace is rejected with an `APP_ALREADY_INSTALLED` error. +* Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error. + +To install a newer version, deploy or publish it first, then re-run `yarn twenty install`. + diff --git a/packages/twenty-docs/l/de/developers/extend/apps/operations/testing.mdx b/packages/twenty-docs/l/de/developers/extend/apps/operations/testing.mdx new file mode 100644 index 0000000000..b1959be0ed --- /dev/null +++ b/packages/twenty-docs/l/de/developers/extend/apps/operations/testing.mdx @@ -0,0 +1,301 @@ +--- +title: Testing +description: Vitest setup, integration tests against a real Twenty server, type checking, and CI with GitHub Actions. +icon: flask +--- + +The SDK provides programmatic APIs that let you build, deploy, install, and uninstall your app from test code. Combined with [Vitest](https://vitest.dev/) and the typed API clients, you can write integration tests that verify your app works end-to-end against a real Twenty server. + +## Using npm packages + +You can install and use any npm package in your app. Both logic functions and front components are bundled with [esbuild](https://esbuild.github.io/), which inlines all dependencies into the output — no `node_modules` are needed at runtime. + +### Installing a package + +```bash filename="Terminal" +yarn add axios +``` + +Then import it in your code: + +```ts src/logic-functions/fetch-data.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import axios from 'axios'; + +const handler = async (): Promise => { + const { data } = await axios.get('https://api.example.com/data'); + + return { data }; +}; + +export default defineLogicFunction({ + universalIdentifier: '...', + name: 'fetch-data', + description: 'Fetches data from an external API', + timeoutSeconds: 10, + handler, +}); +``` + +The same works for front components: + +```tsx src/front-components/chart.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { format } from 'date-fns'; + +const DateWidget = () => { + return

Today is {format(new Date(), 'MMMM do, yyyy')}

; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### How bundling works + +The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle. + +**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed. + +**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment. + +Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server. + +## Setup + +The scaffolded app already includes Vitest. If you set it up manually, install the dependencies: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Create a `vitest.config.ts` at the root of your app: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Create a setup file that verifies the server is reachable before tests run: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## Programmatic SDK APIs + +The `twenty-sdk/cli` subpath exports functions you can call directly from test code: + +| Function | Description | +| -------------- | ------------------------------------------- | +| `appBuild` | Build the app and optionally pack a tarball | +| `appDeploy` | Upload a tarball to the server | +| `appInstall` | Install the app on the active workspace | +| `appUninstall` | Uninstall the app from the active workspace | + +Each function returns a result object with `success: boolean` and either `data` or `error`. + +## Writing an integration test + +Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Running tests + +Make sure your local Twenty server is running, then: + +```bash filename="Terminal" +yarn test +``` + +Or in watch mode during development: + +```bash filename="Terminal" +yarn test:watch +``` + +## Type checking + +You can also run type checking on your app without running tests: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +This runs `tsc --noEmit` and reports any type errors. + +## CI with GitHub Actions + +The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests. + +The workflow: + +1. Checks out your code +2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action +3. Installs dependencies with `yarn install --immutable` +4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub. + +To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow. diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/config/application.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..2684760e48 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/config/application.mdx @@ -0,0 +1,65 @@ +--- +title: Application Config +description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. +icon: rocket +--- + +Every app must have exactly one `defineApplication` call. It declares: + +* **Identity** — universal identifier, display name, description. +* **Permissions** — which role its logic functions and front components run under. +* **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. +* **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/l/pt/developers/extend/apps/logic/logic-functions). + +```ts src/application-config.ts +import { defineApplication } from 'twenty-sdk/define'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; + +export default defineApplication({ + universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', + displayName: 'My Twenty App', + description: 'My first Twenty app', + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', + description: 'Default recipient name for postcards', + value: 'Jane Doe', + isSecret: false, + }, + }, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). +* `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/l/pt/developers/extend/apps/config/roles). +* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. + +## Default function role + +The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: + +* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. +* The typed API client is restricted to the permissions granted to that role. +* Follow least-privilege: declare only the permissions your functions need. + +When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/l/pt/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/l/pt/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `author` | Author or company name | +| `category` | App category for marketplace filtering | +| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) | +| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) | +| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | +| `websiteUrl` | Link to your website | +| `termsUrl` | Link to terms of service | +| `emailSupport` | Support email address | +| `issueReportUrl` | Link to issue tracker | diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..c2d4c0d2bc --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/config/install-hooks.mdx @@ -0,0 +1,206 @@ +--- +title: Install Hooks +description: Run logic before or after the install — seed data, back up records, validate the upgrade. +icon: wrench +--- + +Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/l/pt/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload`, but they're declared with their own define functions — `definePostInstallLogicFunction()` and `definePreInstallLogicFunction()` — and live outside the normal trigger model (HTTP, cron, database events). + +Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build will error if more than one of either is detected. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ install flow │ +│ │ +│ upload package → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +│ │ +│ old schema visible new schema visible │ +└─────────────────────────────────────────────────────────────┘ +``` + + + + +A post-install function runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services. + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Post install logic function executed successfully!', payload.previousVersion); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Runs after installation to set up the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + shouldRunSynchronously: false, + handler, +}); +``` + +You can also manually execute the post-install function at any time using the CLI: + +```bash filename="Terminal" +yarn twenty exec --postInstall +``` + +Key points: +* Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`). +* The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic. +* **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook. +* **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed. + * `shouldRunSynchronously: false` *(default)* — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window. + * `shouldRunSynchronously: true` — the hook is executed **inline during the install flow** (same executor as pre-install). The install request blocks until the handler finishes, and if it throws, the install caller receives a `POST_INSTALL_ERROR`. No automatic retries. **Use this for fast, must-complete-before-response work** — for example, emitting a validation error to the user, or quick setup that the client will rely on immediately after the install call returns. Keep in mind the metadata migration has already been applied by the time post-install runs, so a sync-mode failure does **not** roll back the schema changes — it only surfaces the error. +* Make sure your handler is idempotent. In async mode the queue may retry up to three times; in either mode the hook may run again on upgrades when `shouldRunOnVersionUpgrade: true`. +* The environment variables `APPLICATION_ID`, `APP_ACCESS_TOKEN`, and `API_URL` are available inside the handler (same as any other logic function), so you can call the Twenty API with an application access token scoped to your app. +* Only one post-install function is allowed per application. The manifest build will error if more than one is detected. +* The function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in [`defineApplication()`](/l/pt/developers/extend/apps/config/application). +* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding. +* **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` to trigger it manually against a running workspace. + + + + +A pre-install function runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped. + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Pre install logic function executed successfully!', payload.previousVersion); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Runs before installation to prepare the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +You can also manually execute the pre-install function at any time using the CLI: + +```bash filename="Terminal" +yarn twenty exec --preInstall +``` + +Key points: +* Pre-install functions use `definePreInstallLogicFunction()` — same specialized config as post-install, just attached to a different lifecycle slot. +* Both pre- and post-install handlers receive the same `InstallPayload` type: `{ previousVersion?: string; newVersion: string }`. Import it once and reuse it for both hooks. +* **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state. +* **Execution model**: pre-install is executed **synchronously** and **blocks the install**. If the handler throws, the install is aborted before any schema changes are applied — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade. +* As with post-install, only one pre-install function is allowed per application. It is attached to the application manifest under `preInstallLogicFunction` automatically during the build. +* **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty exec --preInstall` to trigger it manually. + + + + +Both hooks are part of the same install flow and receive the same `InstallPayload`. The difference is **when** they run relative to the workspace metadata migration, and that changes what data they can safely touch. + +Pre-install is always **synchronous** (it blocks the install and can abort it). Post-install is **asynchronous by default** — enqueued on a worker with automatic retries — but can opt into synchronous execution with `shouldRunSynchronously: true`. See the `definePostInstallLogicFunction` accordion above for when to use each mode. + +**Use `post-install` for anything that needs the new schema to exist.** This is the common case: + +* Seeding default data (creating initial records, default views, demo content) against newly-added objects and fields. +* Registering webhooks with third-party services now that the app has its credentials. +* Calling your own API to finish setup that depends on the synchronized metadata. +* Idempotent "ensure this exists" logic that should reconcile state on every upgrade — combine with `shouldRunOnVersionUpgrade: true`. + +Example — seed a default `PostCard` record after install: + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion }: InstallPayload): Promise => { + if (previousVersion) return; // fresh installs only + + const client = createClient(); + await client.postCard.create({ + data: { title: 'Welcome to Postcard', content: 'Your first card!' }, + }); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Seeds a welcome post card after install.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + handler, +}); +``` + +**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the *previous* schema and its failure rolls back the upgrade, it is the right place for anything risky: + +* **Backing up data that is about to be dropped or restructured** — e.g. you are removing a field in v2 and need to copy its values into another field or export them to storage before the migration runs. +* **Archiving records that a new constraint would invalidate** — e.g. a field is becoming `NOT NULL` and you need to delete or fix rows with null values first. +* **Validating compatibility and refusing the upgrade if the current data cannot be migrated cleanly** — throw from the handler and the install aborts with no changes applied. This is safer than discovering the incompatibility mid-migration. +* **Renaming or rekeying data** ahead of a schema change that would lose the association. + +Example — archive records before a destructive migration: + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise => { + // Only the 1.x → 2.x upgrade drops the legacy `notes` field. + if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) { + return; + } + + const client = createClient(); + const legacyRecords = await client.postCard.findMany({ + where: { notes: { isNotNull: true } }, + }); + + if (legacyRecords.length === 0) return; + + // Copy legacy `notes` into the new `description` field before the migration + // drops the `notes` column. If this fails, the upgrade is aborted and the + // workspace stays on v1 with all data intact. + await Promise.all( + legacyRecords.map((record) => + client.postCard.update({ + where: { id: record.id }, + data: { description: record.notes }, + }), + ), + ); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Backs up legacy notes into description before the v2 migration.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +**Rule of thumb:** + +| You want to... | Use | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Seed default data, configure the workspace, register external resources | `post-install` | +| Run long-running seeding or third-party calls that shouldn't block the install response | `post-install` (default — `shouldRunSynchronously: false`, with worker retries) | +| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` | +| Read or back up data that the upcoming migration would lose | `pre-install` | +| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) | +| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` | +| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) | + + +If in doubt, default to **post-install**. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone. + + + + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/config/overview.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/config/overview.mdx new file mode 100644 index 0000000000..bfe346d2d6 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/config/overview.mdx @@ -0,0 +1,51 @@ +--- +title: Overview +description: Configure the app itself — its identity, default permissions, and what runs at install time. +icon: screwdriver-wrench +--- + +A Twenty app's **config layer** is what describes the app *to the platform* — its identity, the permissions it holds, and the code that runs during install or upgrade. These declarations don't add new data shapes or runtime behavior; they tell Twenty *who the app is* and *how to set it up*. + +```text +┌────────────────────────────────────────────────────────┐ +│ Application — identity, default role, variables, │ +│ marketplace metadata │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Role — what the app's logic functions can read │ │ +│ │ and write (referenced by Application) │ │ +│ └──────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────┘ + │ + ▼ (at install / upgrade time) + ┌──────────────────────────────────┐ + │ Pre-install hook │ before metadata migration + └──────────────────────────────────┘ + ┌──────────────────────────────────┐ + │ Post-install hook │ after metadata migration + └──────────────────────────────────┘ +``` + +## In this section + + + + `defineApplication` — identity, default role, variables, marketplace metadata. + + + `defineRole` — declare what your app's logic functions can read and write. + + + `definePreInstallLogicFunction` and `definePostInstallLogicFunction` — back up data, seed defaults, validate upgrades. + + + +## How the pieces relate + +* **Application** is the entry point. Every app has exactly one `defineApplication()` call, and it points at one **Role** as its default. +* The **Role** controls what the app's logic functions and front components can read and write. Follow least-privilege: only grant the permissions your code actually needs. +* **Install Hooks** run during install or upgrade — pre-install before the metadata migration (so it can refuse a risky upgrade), post-install after the migration (so it can seed default data against the new schema). + + +Install hooks share the [logic function](/l/pt/developers/extend/apps/logic/logic-functions) runtime — same handler signature, same environment variables, same typed API client — but they're declared with their own define functions and live outside the regular trigger model (HTTP, cron, database events). + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/config/public-assets.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/config/public-assets.mdx new file mode 100644 index 0000000000..338c0f7f8e --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/config/public-assets.mdx @@ -0,0 +1,59 @@ +--- +title: Public Assets +description: Ship static files — images, icons, fonts — alongside your app via the public/ folder. +icon: folder-open +--- + +The `public/` folder at the root of your app holds static files — images, icons, fonts, or any other assets your app needs at runtime. These files are automatically included in builds, synced during dev mode, and uploaded to the server. + +Files placed in `public/` are: + +* **Publicly accessible** — once synced to the server, assets are served at a public URL. No authentication is needed to access them. +* **Available in front components** — use asset URLs to display images, icons, or any media inside your React components. +* **Available in logic functions** — reference asset URLs in emails, API responses, or any server-side logic. +* **Used for marketplace metadata** — the `logoUrl` and `screenshots` fields in `defineApplication()` reference files from this folder (e.g., `public/logo.png`). These are displayed in the marketplace when your app is published. +* **Auto-synced in dev mode** — when you add, update, or delete a file in `public/`, it is synced to the server automatically. No restart needed. +* **Included in builds** — `yarn twenty build` bundles all public assets into the distribution output. + +## Accessing public assets with `getPublicAssetUrl` + +Use the `getPublicAssetUrl` helper from `twenty-sdk` to get the full URL of a file in your `public/` directory. It works in both **logic functions** and **front components**. + +**In a logic function:** + +```ts src/logic-functions/send-invoice.ts +import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define'; + +const handler = async (): Promise => { + const logoUrl = getPublicAssetUrl('logo.png'); + const invoiceUrl = getPublicAssetUrl('templates/invoice.png'); + + // Fetch the file content (no auth required — public endpoint) + const response = await fetch(invoiceUrl); + const buffer = await response.arrayBuffer(); + + return { logoUrl, size: buffer.byteLength }; +}; + +export default defineLogicFunction({ + universalIdentifier: 'a1b2c3d4-...', + name: 'send-invoice', + description: 'Sends an invoice with the app logo', + timeoutSeconds: 10, + handler, +}); +``` + +**In a front component:** + +```tsx src/front-components/company-card.tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +export default defineFrontComponent(() => { + const logoUrl = getPublicAssetUrl('logo.png'); + + return App logo; +}); +``` + +The `path` argument is relative to your app's `public/` folder. Both `getPublicAssetUrl('logo.png')` and `getPublicAssetUrl('public/logo.png')` resolve to the same URL — the `public/` prefix is stripped automatically if present. diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/config/roles.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/config/roles.mdx new file mode 100644 index 0000000000..74ffb6ec50 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/config/roles.mdx @@ -0,0 +1,90 @@ +--- +title: Roles & Permissions +description: Declare what objects and fields your app's logic functions and front components can read and write. +icon: shield-halved +--- + +A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role declared as `defaultRoleUniversalIdentifier` in [`defineApplication`](/l/pt/developers/extend/apps/config/application). + +```ts src/roles/restricted-company-role.ts +import { + defineRole, + PermissionFlag, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; + +export default defineRole({ + universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6', + label: 'My new role', + description: 'A role that can be used in your workspace', + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + fieldPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + fieldUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier, + canReadFieldValue: false, + canUpdateFieldValue: false, + }, + ], + permissionFlags: [PermissionFlag.APPLICATIONS], +}); +``` + +## The default function role + +When you scaffold a new app, the CLI creates a default role file: + +```ts src/roles/default-role.ts +import { defineRole, PermissionFlag } from 'twenty-sdk/define'; + +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = + 'b648f87b-1d26-4961-b974-0908fd991061'; + +export default defineRole({ + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + label: 'Default function role', + description: 'Default role for function Twenty client', + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [], + fieldPermissions: [], + permissionFlags: [], +}); +``` + +This role's `universalIdentifier` is referenced from `application-config.ts` as `defaultRoleUniversalIdentifier`: + +* **`*.role.ts`** declares what the role can do. +* **`application-config.ts`** points to that role so your functions inherit its permissions. + +## Best practices + +* Start from the scaffolded role, then progressively restrict it — the default grants broad read access, which is rarely what you want in production. +* Replace `objectPermissions` and `fieldPermissions` with the exact objects and fields your functions actually need. +* `permissionFlags` control access to platform-level capabilities. Keep them minimal. +* See a working example: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts). diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/data/extending-objects.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/data/extending-objects.mdx new file mode 100644 index 0000000000..95c28df1c2 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/data/extending-objects.mdx @@ -0,0 +1,48 @@ +--- +title: Extending Objects +description: Add fields to standard Twenty objects (Person, Company, …) or to objects from other apps using defineField. +icon: wand-magic-sparkles +--- + +Use `defineField()` to add a field to an object you don't own — a standard Twenty object like Person or Company, or an object shipped by another installed app. Unlike inline fields declared inside [`defineObject`](/l/pt/developers/extend/apps/data/objects), standalone fields require an `objectUniversalIdentifier` to specify which object they extend. + +```ts src/fields/company-loyalty-tier.field.ts +import { defineField, FieldType } from 'twenty-sdk/define'; + +export default defineField({ + universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890', + objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object + name: 'loyaltyTier', + type: FieldType.SELECT, + label: 'Loyalty Tier', + icon: 'IconStar', + options: [ + { value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' }, + { value: 'SILVER', label: 'Silver', position: 1, color: 'gray' }, + { value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' }, + ], +}); +``` + +## Key points + +* `objectUniversalIdentifier` identifies the target object. For standard Twenty objects, import the constant from `twenty-sdk`: + + ```ts + import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define'; + + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier + // … + ``` + +* When defining fields **inline inside `defineObject()`**, you do **not** need `objectUniversalIdentifier` — it's inherited from the parent object. + +* `defineField()` is the only way to add fields to objects you didn't create with `defineObject()`. + +* File location is up to you. The convention is `src/fields/\.field.ts`, but the SDK detects fields anywhere in `src/`. + +## Adding a relation to an existing object + +To add a relation field (e.g. linking your custom object to a standard `Person`), use `defineField()` with `FieldType.RELATION`. The pattern is the same as for inline relations but with `objectUniversalIdentifier` set explicitly. See [Relations](/l/pt/developers/extend/apps/data/relations) for the bidirectional pattern. diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/data/objects.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/data/objects.mdx new file mode 100644 index 0000000000..9b544b30ae --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/data/objects.mdx @@ -0,0 +1,93 @@ +--- +title: Objetos +description: Declare new record types — custom tables with their own fields — using defineObject. +icon: tabela +--- + +Custom **objects** are new record types your app adds to a workspace — Post Card, Invoice, Subscription, anything specific to your domain. Each object declares its schema (fields, relations, default values) and a stable universal identifier that survives across syncs and deploys. + +```ts src/objects/post-card.object.ts +import { defineObject, FieldType } from 'twenty-sdk/define'; + +enum PostCardStatus { + DRAFT = 'DRAFT', + SENT = 'SENT', + DELIVERED = 'DELIVERED', + RETURNED = 'RETURNED', +} + +export default defineObject({ + universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', + nameSingular: 'postCard', + namePlural: 'postCards', + labelSingular: 'Post Card', + labelPlural: 'Post Cards', + description: 'A post card object', + icon: 'IconMail', + fields: [ + { + universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b', + name: 'content', + type: FieldType.TEXT, + label: 'Content', + description: "Postcard's content", + icon: 'IconAbc', + }, + { + universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac', + name: 'recipientName', + type: FieldType.FULL_NAME, + label: 'Recipient name', + icon: 'IconUser', + }, + { + universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266', + name: 'recipientAddress', + type: FieldType.ADDRESS, + label: 'Recipient address', + icon: 'IconHome', + }, + { + universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e', + name: 'status', + type: FieldType.SELECT, + label: 'Status', + icon: 'IconSend', + defaultValue: `'${PostCardStatus.DRAFT}'`, + options: [ + { value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' }, + { value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' }, + { value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' }, + { value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' }, + ], + }, + { + universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433', + name: 'deliveredAt', + type: FieldType.DATE_TIME, + label: 'Delivered at', + icon: 'IconCheck', + isNullable: true, + defaultValue: null, + }, + ], +}); +``` + +## 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. +* Inline fields defined here do **not** need an `objectUniversalIdentifier` — it's inherited from the parent object. Use [`defineField()`](/l/pt/developers/extend/apps/data/extending-objects) to add fields to objects you don't own. +* You can scaffold new objects with `yarn twenty add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/l/pt/developers/extend/apps/getting-started/scaffolding). + + +**Base fields are added automatically.** When you define a custom object, Twenty creates standard fields like `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and `deletedAt` for you. You don't need to declare them in your `fields` array — only your custom fields. You can override a default field by declaring one with the same name, but this is rarely a good idea. + + +## O que vem depois + +* **Connect this object to others** — see [Relations](/l/pt/developers/extend/apps/data/relations) for the bidirectional relation pattern. +* **Add fields to objects from other apps** — see [Extending Objects](/l/pt/developers/extend/apps/data/extending-objects) for `defineField()`. +* **Display this object in the UI** — see [Views](/l/pt/developers/extend/apps/layout/views) and [Navigation Menu Items](/l/pt/developers/extend/apps/layout/navigation-menu-items) to put it in the sidebar. diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/data/overview.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/data/overview.mdx new file mode 100644 index 0000000000..b4cdf669e5 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/data/overview.mdx @@ -0,0 +1,52 @@ +--- +title: Overview +description: Shape the data your app adds to a workspace — objects, fields, and relations. +icon: database +--- + +A Twenty app's **data layer** is the data your app *adds* to a workspace — the new record types it declares, the columns it adds to existing objects, and how those records connect to each other. + +```text +┌──────────────────────────────────────────────────┐ +│ Object — a record type, e.g. PostCard │ +│ ├─ Field (name, type, label) │ +│ ├─ Field │ +│ └─ Relation (link to another object) │ +└──────────────────────────────────────────────────┘ + │ + ├── lives in your app, OR + │ + ▼ +┌──────────────────────────────────────────────────┐ +│ Standard / other apps' objects │ +│ └─ Field added by your app via defineField │ +└──────────────────────────────────────────────────┘ +``` + +## In this section + + + + `defineObject` — declare new record types with their own fields. + + + `defineField` — add fields to standard or other apps' objects. + + + Bidirectional `MANY_TO_ONE` / `ONE_TO_MANY` connections between objects. + + + +## Entities at a glance + +| Entity | Purpose | Defined with | +| ------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| **Object** | A new custom record type (e.g. PostCard, Invoice) with its own fields | `defineObject()` | +| **Field** | A column on an object. Standalone fields can extend objects you didn't create (e.g. add `loyaltyTier` to Company) | `defineField()` | +| **Relation** | A bidirectional link between two objects — both sides declared as fields | `defineField()` with `FieldType.RELATION` | + +The SDK detects these via AST analysis at build time, so file organization is up to you — the convention is `src/objects/` and `src/fields/`. Stable `universalIdentifier` UUIDs tie everything together across deploys. + + +Looking for **Application Config** or **Roles & Permissions**? Those describe the app itself rather than the data it adds — they live under [Config](/l/pt/developers/extend/apps/config/overview). Looking for **Connections** (Linear, GitHub, Slack OAuth)? Those exist to be called *from* logic functions and live under [Logic](/l/pt/developers/extend/apps/logic/connections). + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/data/relations.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/data/relations.mdx new file mode 100644 index 0000000000..3a4a49e8cc --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/data/relations.mdx @@ -0,0 +1,160 @@ +--- +title: Relações +description: Connect objects together with bidirectional MANY_TO_ONE / ONE_TO_MANY relations. +icon: diagram-project +--- + +Relations connect two objects together. In Twenty, relations are always **bidirectional** — every relation has two sides, and each side is declared as a field that references the other. + +| Tipo de relação | Descrição | Tem chave estrangeira? | +| --------------- | ----------------------------------------------------------------- | ---------------------- | +| `MANY_TO_ONE` | Muitos registros deste objeto apontam para um registro do destino | Sim (`joinColumnName`) | +| `ONE_TO_MANY` | Um registro deste objeto possui muitos registros do destino | No (the inverse side) | + +## How relations work + +Every relation requires **two fields** that reference each other: + +1. The **MANY_TO_ONE** side — lives on the object that holds the foreign key. +2. The **ONE_TO_MANY** side — lives on the object that owns the collection. + +Both fields use `FieldType.RELATION` and cross-reference each other via `relationTargetFieldMetadataUniversalIdentifier`. + +## Example: Post Card has many Recipients + +A `PostCard` can be sent to many `PostCardRecipient` records. Each recipient belongs to exactly one post card. + +**Step 1: Define the ONE_TO_MANY side on PostCard** (the "one" side): + +```ts src/fields/post-card-recipients-on-post-card.field.ts +import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111'; +// Import from the other side +import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field'; + +export default defineField({ + universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCardRecipients', + label: 'Post Card Recipients', + icon: 'IconUsers', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, +}); +``` + +**Step 2: Define the MANY_TO_ONE side on PostCardRecipient** (the "many" side — holds the foreign key): + +```ts src/fields/post-card-on-post-card-recipient.field.ts +import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222'; +// Import from the other side +import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field'; + +export default defineField({ + universalIdentifier: POST_CARD_FIELD_ID, + objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + icon: 'IconMail', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, +}); +``` + + +**Circular imports:** both relation fields reference each other's `universalIdentifier`. To avoid circular import issues, export your field IDs as named constants from each file and import them in the other. The build system resolves these at compile time. + + +## Relating to standard objects + +To create a relation with a built-in Twenty object (Person, Company, etc.), use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`: + +```ts src/fields/person-on-self-hosting-user.field.ts +import { + defineField, + FieldType, + RelationType, + OnDeleteAction, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; +import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object'; + +export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333'; +export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444'; + +export default defineField({ + universalIdentifier: PERSON_FIELD_ID, + objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'person', + label: 'Person', + description: 'Person matching with the self hosting user', + isNullable: true, + relationTargetObjectMetadataUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, + relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.SET_NULL, + joinColumnName: 'personId', + }, +}); +``` + +## Relation field properties + +| Property | Required | Description | +| ------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------- | +| `type` | Yes | Must be `FieldType.RELATION` | +| `relationTargetObjectMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the target object | +| `relationTargetFieldMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the matching field on the target object | +| `universalSettings.relationType` | Yes | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` | +| `universalSettings.onDelete` | MANY_TO_ONE only | What happens when the referenced record is deleted: `CASCADE`, `SET_NULL`, `RESTRICT`, or `NO_ACTION` | +| `universalSettings.joinColumnName` | MANY_TO_ONE only | Database column name for the foreign key (e.g., `postCardId`) | + +## Inline relation fields + +You can also declare a relation directly inside [`defineObject`](/l/pt/developers/extend/apps/data/objects). When inline, omit `objectUniversalIdentifier` — it's inherited from the parent object: + +```ts +export default defineObject({ + universalIdentifier: '...', + nameSingular: 'postCardRecipient', + // ... + fields: [ + { + universalIdentifier: POST_CARD_FIELD_ID, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, + }, + // … other fields + ], +}); +``` diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/concepts.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/concepts.mdx new file mode 100644 index 0000000000..ee5ae40c18 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/concepts.mdx @@ -0,0 +1,101 @@ +--- +title: Concepts +description: How Twenty apps work — entity model, sandboxing, and the install lifecycle. +icon: sitemap +--- + +Twenty apps are TypeScript packages that extend your workspace with custom objects, logic, UI components, and AI capabilities. They run on the Twenty platform with full sandboxing and permission controls. + +## How apps work + +An app is a collection of **entities** declared using `defineEntity()` functions from the `twenty-sdk` package. The SDK detects these declarations via AST analysis at build time and produces a **manifest** — a complete description of what your app adds to a workspace. These functions validate your configuration at build time and provide IDE autocompletion and type safety. + +``` +your-app/ +├── src/ +│ ├── application-config.ts ← defineApplication (required, one per app) +│ ├── roles/ ← defineRole +│ ├── objects/ ← defineObject +│ ├── fields/ ← defineField +│ ├── logic-functions/ ← defineLogicFunction +│ ├── front-components/ ← defineFrontComponent +│ ├── skills/ ← defineSkill +│ ├── agents/ ← defineAgent +│ ├── views/ ← defineView +│ ├── navigation-menu-items/ ← defineNavigationMenuItem +│ └── page-layouts/ ← definePageLayout +├── public/ ← Static assets (images, icons) +└── package.json +``` + + + **File organization is up to you.** Entity detection is AST-based — the SDK finds `export default defineEntity(...)` calls regardless of where the file lives. The folder structure above is a convention, not a requirement. + + +## Entity types + +| Entity | Purpose | Docs | +| ------------------------ | ------------------------------------------ | ----------------------------------------------------------------------------- | +| **Application** | App identity, default role, variables | [Application Config](/l/pt/developers/extend/apps/config/application) | +| **Role** | Permission sets on objects and fields | [Roles & Permissions](/l/pt/developers/extend/apps/config/roles) | +| **Object** | Custom record types with fields | [Objects](/l/pt/developers/extend/apps/data/objects) | +| **Field** | Add fields to objects from other apps | [Extending Objects](/l/pt/developers/extend/apps/data/extending-objects) | +| **Relation** | Bidirectional links between objects | [Relations](/l/pt/developers/extend/apps/data/relations) | +| **Logic Function** | Server-side TypeScript with triggers | [Logic Functions](/l/pt/developers/extend/apps/logic/logic-functions) | +| **Skill** | Reusable AI agent instructions | [Skills & Agents](/l/pt/developers/extend/apps/logic/skills-and-agents) | +| **Agent** | AI assistants with custom prompts | [Skills & Agents](/l/pt/developers/extend/apps/logic/skills-and-agents) | +| **Connection Provider** | OAuth credentials for third-party APIs | [Connections](/l/pt/developers/extend/apps/logic/connections) | +| **View** | Pre-configured record list views | [Views](/l/pt/developers/extend/apps/layout/views) | +| **Navigation Menu Item** | Custom sidebar entries | [Navigation Menu Items](/l/pt/developers/extend/apps/layout/navigation-menu-items) | +| **Page Layout** | Tabs and widgets on a record's detail page | [Page Layouts](/l/pt/developers/extend/apps/layout/page-layouts) | +| **Front Component** | Sandboxed React UI inside Twenty | [Front Components](/l/pt/developers/extend/apps/layout/front-components) | +| **Command Menu Item** | Quick actions and Cmd+K entries | [Command Menu Items](/l/pt/developers/extend/apps/layout/command-menu-items) | + +## Sandboxing + +* **Logic functions** run in isolated Node.js processes on the server. They only access data through the typed API client, scoped to the app's role permissions. +* **Front components** run in Web Workers using Remote DOM — sandboxed from the main page but rendering native DOM elements (not iframes). They communicate with Twenty via a message-passing host API. +* **Permissions** are enforced at the API level. The runtime token (`TWENTY_APP_ACCESS_TOKEN`) is derived from the role defined in `defineApplication()`. + +## App lifecycle + +``` +┌─────────────────────────────────────────────────────────┐ +│ Development │ +│ npx create-twenty-app → yarn twenty dev (live sync) │ +├─────────────────────────────────────────────────────────┤ +│ Build & Deploy │ +│ yarn twenty build → yarn twenty deploy │ +├─────────────────────────────────────────────────────────┤ +│ Install flow │ +│ upload → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +├─────────────────────────────────────────────────────────┤ +│ Publish │ +│ npm publish → appears in Twenty marketplace │ +└─────────────────────────────────────────────────────────┘ +``` + +* **`yarn twenty dev`** — watches your source files and live-syncs changes to a connected Twenty server. The typed API client is regenerated automatically when the schema changes. +* **`yarn twenty build`** — compiles TypeScript, bundles logic functions and front components with esbuild, and produces a manifest. +* **Pre/post-install hooks** — optional functions that run during installation. See [Install Hooks](/l/pt/developers/extend/apps/config/install-hooks) for details. + +## Next steps + + + + Application identity, default role, and install hooks. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/local-server.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/local-server.mdx new file mode 100644 index 0000000000..a424e5203a --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/local-server.mdx @@ -0,0 +1,72 @@ +--- +title: Local Server +description: Manage the local Twenty Docker server — start, stop, upgrade, parallel test instance, and manual SDK setup. +icon: server +--- + +## Managing the local server + +Use `yarn twenty server` to control the local Twenty container: + +| Command | What it does | +| -------------------------------------- | -------------------------------------------- | +| `yarn twenty server start` | Start the server (pulls the image if needed) | +| `yarn twenty server start --port 3030` | Start on a custom port | +| `yarn twenty server stop` | Stop the server (preserves data) | +| `yarn twenty server status` | Show URL, version, and login credentials | +| `yarn twenty server logs` | Stream server logs | +| `yarn twenty server reset` | Wipe data and start fresh | +| `yarn twenty server upgrade` | Pull the latest `twenty-app-dev` image | +| `yarn twenty server upgrade 2.2.0` | Upgrade to a specific version | + +Data persists across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything. + +## Upgrading the server image + +`yarn twenty server upgrade` pulls the latest image, compares digests, and only recreates the container if anything actually changed. Volumes are preserved — only the container is replaced. If a new image was pulled and the container was running, the upgrade automatically starts a new container; run `yarn twenty server start` afterward to wait for it to become healthy. + +```bash filename="Terminal" +yarn twenty server upgrade # Latest +yarn twenty server upgrade 2.2.0 # Specific version +``` + +Verify the running version with `yarn twenty server status` (it shows the `APP_VERSION` baked into the container). + +## Running a parallel test instance + +Pass `--test` to any `server` command to manage a second, fully isolated instance — useful for integration tests or experiments without touching your main dev data: + +| Command | What it does | +| ----------------------------------- | ----------------------------------------------- | +| `yarn twenty server start --test` | Start the test instance (defaults to port 2021) | +| `yarn twenty server stop --test` | Stop it | +| `yarn twenty server status --test` | Show its status | +| `yarn twenty server logs --test` | Stream its logs | +| `yarn twenty server reset --test` | Wipe its data | +| `yarn twenty server upgrade --test` | Upgrade its image | + +The test instance has its own container (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`), and config — it runs alongside your main instance without conflicts. Combine `--test` with `--port` to override 2021. + +## Manual setup (without the scaffolder) + +Skip the scaffolder if you're adding the SDK to an existing project: + +```bash filename="Terminal" +yarn add twenty-sdk twenty-client-sdk +``` + +Add the script to `package.json`: + +```json filename="package.json" +{ + "scripts": { + "twenty": "twenty" + } +} +``` + +You can now run `yarn twenty dev`, `yarn twenty server start`, and the rest. + + +Don't install `twenty-sdk` globally — pin it per project so each app uses its own version. + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/project-structure.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/project-structure.mdx new file mode 100644 index 0000000000..ad85a70452 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/project-structure.mdx @@ -0,0 +1,40 @@ +--- +title: Project Structure +description: What's inside a scaffolded Twenty app — files, folders, and what each one does. +icon: folder-tree +--- + +A new app generated by `npx create-twenty-app` looks like this: + +```text filename="my-twenty-app/" +my-twenty-app/ + package.json + src/ + application-config.ts # Required — your app's entry point + default-role.ts # Permissions for logic functions + constants/ + universal-identifiers.ts # Auto-generated UUIDs and metadata + __tests__/ + setup-test.ts + app-install.integration-test.ts + .github/workflows/ci.yml # GitHub Actions + public/ # Static assets + vitest.config.ts # Test runner config + tsconfig.json, tsconfig.spec.json + .nvmrc, .yarnrc.yml, .oxlintrc.json + README.md, LLMS.md +``` + +## Key files + +| File / Folder | Purpose | +| ---------------------------------------- | -------------------------------------------------------------- | +| `src/application-config.ts` | **Required.** The main configuration file for your app. | +| `src/default-role.ts` | Default role controlling what your logic functions can access. | +| `src/constants/universal-identifiers.ts` | Auto-generated UUIDs and metadata (display name, description). | +| `src/__tests__/` | Integration tests (setup + example test). | +| `public/` | Static assets (images, fonts) served with your app. | + + +**File organization is up to you.** The folders above are conventions — the SDK detects entities via AST analysis on `export default defineEntity(...)` calls regardless of where the file lives. + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/quick-start.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/quick-start.mdx new file mode 100644 index 0000000000..9ec7abadcb --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/quick-start.mdx @@ -0,0 +1,184 @@ +--- +title: Quick Start +icon: rocket +description: Create your first Twenty app in minutes. +--- + +## Prerequisites + +* **Node.js 24+** — [Download](https://nodejs.org/) +* **Yarn 4** — bundled with Node via Corepack. Enable it: `corepack enable` +* **Docker** — [Download](https://www.docker.com/products/docker-desktop/). Needed to run a local Twenty server. Skip if you already have Twenty running elsewhere. + +Building a Twenty app has three phases. The scaffolder collapses them into one happy-path command, but each phase is a separate concept — when something fails, knowing which phase you're in tells you what to fix. + +| Phase | What you do | Tool | Result | +| ------------------- | ---------------------------------- | ----------------------------- | ----------------------------- | +| **1. Scaffold** | Generate the app's source code | `npx create-twenty-app` | A TypeScript project on disk | +| **2. Run a server** | Start a Twenty server to sync into | Docker + `yarn twenty server` | A running Twenty instance | +| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` | Your changes appear in the UI | + +--- + +## Phase 1 — Scaffold your project + +Create a new app from the template: + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app +``` + +You'll be prompted for a name and description — press **Enter** for the defaults. This generates a TypeScript project in `my-twenty-app/` with a starter `application-config.ts`, a default role, a CI workflow, and an integration test. + +**After this phase:** you have an app's source code on your machine. It isn't running yet — that's Phase 2. + +--- + +## Phase 2 — Run a local Twenty server + +Your app needs a Twenty server to sync into. The server is a full Twenty instance — UI, GraphQL API, PostgreSQL — running locally in Docker. Your local code uploads its definitions to that server, which makes them appear in the UI. + +The scaffolder offers to start one for you: + +> **Would you like to set up a local Twenty instance?** + +* **Yes (recommended)** — pulls the `twentycrm/twenty-app-dev` Docker image and starts it on port `2020`. Make sure Docker is running first. +* **No** — choose this if you already have a Twenty server you want to connect to. You can wire it up later with `yarn twenty remote add`. + +
+ Should start local instance? +
+ +Once the server is up, a browser opens for sign-in. Use the pre-seeded demo account: + +* **Email:** `tim@apple.dev` +* **Password:** `tim@apple.dev` + +
+ Twenty login screen +
+ +Click **Authorize** on the next screen — this gives the CLI access to your workspace. + +
+ Twenty CLI authorization screen +
+ +Your terminal will confirm everything is set up. + +
+ App scaffolded successfully +
+ +**After this phase:** you have a running Twenty server at [http://localhost:2020](http://localhost:2020) with your CLI authorized to sync to it. + + +If Docker isn't installed or running, the scaffolder will tell you the right start command for your OS. Once Docker is up, you can resume with `yarn twenty server start` — no need to re-scaffold. + + +--- + +## Phase 3 — Sync your changes + +This is the inner loop you'll spend most of your time in. + +```bash filename="Terminal" +cd my-twenty-app +yarn twenty dev +``` + +This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a second the server reflects the change. You'll see a live status panel in your terminal. + +For more detailed output (build logs, sync requests, error traces), add `--verbose`. + +
+ Dev mode terminal output +
+ +Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). You should see your app under **Your Apps**. + +
+ Your Apps list showing My twenty app +
+ +Click **My twenty app** to see its **application registration** — a server-level record describing your app (name, identifier, OAuth credentials, source). One registration can be installed across multiple workspaces on the same server. + +
+ Application registration details +
+ +Click **View installed app** to see the workspace install. The **About** tab shows version and management options. + +
+ Installed app +
+ +**After this phase:** you have a live development loop. Edit any file in `src/` and it appears in the UI. + +### One-shot sync for CI and scripts + +Pass `--once` to run a single build + sync and exit — same pipeline, no watcher: + +```bash filename="Terminal" +yarn twenty dev --once +``` + +| Command | Behavior | When to use | +| ------------------------ | ------------------------------------------------------------- | ---------------------------------------------------- | +| `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. | +| `yarn twenty dev --once` | Single build + sync, exits `0` on success, `1` on failure. | CI, pre-commit hooks, AI agents, scripted workflows. | + +Both modes need a server in development mode and an authenticated remote. + + +Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests — use `yarn twenty deploy` to deploy to production servers. See [Publishing](/l/pt/developers/extend/apps/operations/publishing). + + +--- + +## Starting from an example + +Use `--example` to start with a more complete project (custom objects, fields, logic functions, front components): + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app --example postcard +``` + +Examples live in [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). You can also scaffold individual entities into an existing project with `yarn twenty add` — see [Scaffolding](/l/pt/developers/extend/apps/getting-started/scaffolding). + +--- + +## What you can build + +Apps are composed of **entities** — each defined as a TypeScript file with a single `export default`: + +| Entity | What it does | +| ---------------------- | ----------------------------------------------------------------------------------- | +| **Objects & Fields** | Custom data models (Post Card, Invoice, etc.) with typed fields | +| **Logic functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events | +| **Front components** | React components that render inside Twenty's UI (side panel, widgets, command menu) | +| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants | +| **Views & Navigation** | Pre-configured list views and sidebar menu items | +| **Page layouts** | Custom record detail pages with tabs and widgets | + +Full reference: [Concepts](/l/pt/developers/extend/apps/getting-started/concepts). + +## Next steps + + + + Application identity, default role, install hooks, public assets. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/scaffolding.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/scaffolding.mdx new file mode 100644 index 0000000000..b2853cadf2 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/scaffolding.mdx @@ -0,0 +1,58 @@ +--- +title: Scaffolding +description: Generate entity files interactively with yarn twenty add — objects, fields, views, logic functions, and more. +icon: wand-magic-sparkles +--- + +Instead of creating entity files by hand, use the interactive scaffolder: + +```bash filename="Terminal" +yarn twenty add +``` + +It prompts you to pick an entity type and walks you through the required fields, then writes a ready-to-use file with a stable `universalIdentifier` and the correct `defineEntity()` call. + +You can also pass the entity type directly to skip the first prompt: + +```bash filename="Terminal" +yarn twenty add object +yarn twenty add logicFunction +yarn twenty add frontComponent +``` + +## Available entity types + +| Entity type | Command | Generated file | +| -------------------- | ------------------------------------ | ------------------------------------------------------- | +| Object | `yarn twenty add object` | `src/objects/\.ts` | +| Field | `yarn twenty add field` | `src/fields/\.ts` | +| Logic function | `yarn twenty add logicFunction` | `src/logic-functions/\.ts` | +| Front component | `yarn twenty add frontComponent` | `src/front-components/\.tsx` | +| Role | `yarn twenty add role` | `src/roles/\.ts` | +| Skill | `yarn twenty add skill` | `src/skills/\.ts` | +| Agent | `yarn twenty add agent` | `src/agents/\.ts` | +| View | `yarn twenty add view` | `src/views/\.ts` | +| Navigation menu item | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\.ts` | +| Page layout | `yarn twenty add pageLayout` | `src/page-layouts/\.ts` | + +## What the scaffolder generates + +Each entity type has its own template. For example, `yarn twenty add object` asks for: + +1. **Name (singular)** — e.g., `invoice` +2. **Name (plural)** — e.g., `invoices` +3. **Label (singular)** — auto-populated from the name (e.g., `Invoice`) +4. **Label (plural)** — auto-populated (e.g., `Invoices`) +5. **Create a view and navigation item?** — if you answer yes, the scaffolder also generates a matching view and sidebar link for the new object. + +Other entity types have simpler prompts — most only ask for a name. + +The `field` entity type is more detailed: it asks for the field name, label, type (from a list of all available field types like `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.), and the target object's `universalIdentifier`. + +## Custom output path + +Use the `--path` flag to place the generated file in a custom location: + +```bash filename="Terminal" +yarn twenty add logicFunction --path src/custom-folder +``` diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/troubleshooting.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/troubleshooting.mdx new file mode 100644 index 0000000000..9ca59adc8b --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/getting-started/troubleshooting.mdx @@ -0,0 +1,12 @@ +--- +title: Resolução de Problemas +description: Problemas comuns na primeira execução — Docker, versão do Node, Yarn, dependências. +icon: wrench +--- + +* **Erros do Docker** — Certifique-se de que o Docker Desktop (ou o daemon) esteja em execução antes de `yarn twenty server start`. A mensagem de erro mostrará o comando de inicialização correto para o seu sistema operacional. +* **Versão errada do Node** — É necessário 24 ou superior. Verifique com `node -v`. +* **Falta o Yarn 4** — Execute `corepack enable`. +* **Dependências com problemas** — `rm -rf node_modules && yarn install`. + +Travou? Peça ajuda no [Discord da Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322). diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/layout/command-menu-items.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/layout/command-menu-items.mdx new file mode 100644 index 0000000000..17b70e0034 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/layout/command-menu-items.mdx @@ -0,0 +1,144 @@ +--- +title: Command Menu Items +description: Surface front components as quick actions and command menu (Cmd+K) entries with defineCommandMenuItem. +icon: terminal +--- + +A **command menu item** is the bridge between the user and a [front component](/l/pt/developers/extend/apps/layout/front-components). It registers the component in Twenty's command menu (Cmd+K) and, optionally, as a pinned quick-action button in the top-right corner of the page. + +```ts src/command-menu-items/open-dashboard.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + label: 'Open Dashboard', + shortLabel: 'Dashboard', + icon: 'IconLayoutDashboard', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +## Configuration fields + +| Field | Required | Description | +| --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `universalIdentifier` | Yes | Stable unique ID for the command | +| `label` | Yes | Full label shown in the command menu (Cmd+K) | +| `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens | +| `shortLabel` | No | Shorter label displayed on the pinned quick-action button | +| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) | +| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page | +| `availabilityType` | No | Controls where the command appears: `'GLOBAL'` (always available), `'RECORD_SELECTION'` (only when records are selected), or `'FALLBACK'` (shown when no other commands match) | +| `availabilityObjectUniversalIdentifier` | No | Restrict the command to pages of a specific object type (e.g. only on Company records) | +| `conditionalAvailabilityExpression` | No | A boolean expression that dynamically controls visibility (see below) | + +## Headless commands + +A command menu item paired with a [headless front component](/l/pt/developers/extend/apps/layout/front-components#headless-vs-non-headless) is the idiomatic way to ship a one-click action — run code, navigate, or confirm and execute. The Front Components page covers the [SDK Command components](/l/pt/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) that handle the action-and-unmount pattern. + +A typical flow: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +## Conditional availability expressions + +The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions: + +```ts src/command-menu-items/bulk-update.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; +import { + objectPermissions, + everyEquals, +} from 'twenty-sdk/front-component'; + +export default defineCommandMenuItem({ + universalIdentifier: '...', + label: 'Bulk Update', + availabilityType: 'RECORD_SELECTION', + frontComponentUniversalIdentifier: '...', + conditionalAvailabilityExpression: everyEquals( + objectPermissions, + 'canUpdateObjectRecords', + true, + ), +}); +``` + +### Context variables + +These represent the current state of the page: + +| Variable | Type | Description | +| ------------------------------ | --------- | ---------------------------------------------------------------- | +| `pageType` | `string` | Current page type (e.g. `'RecordIndexPage'`, `'RecordShowPage'`) | +| `isInSidePanel` | `boolean` | Whether the component is rendered in a side panel | +| `numberOfSelectedRecords` | `number` | Number of currently selected records | +| `isSelectAll` | `boolean` | Whether "select all" is active | +| `selectedRecords` | `array` | The selected record objects | +| `favoriteRecordIds` | `array` | IDs of favorited records | +| `objectPermissions` | `object` | Permissions for the current object type | +| `targetObjectReadPermissions` | `object` | Read permissions for the target object | +| `targetObjectWritePermissions` | `object` | Write permissions for the target object | +| `featureFlags` | `object` | Active feature flags | +| `objectMetadataItem` | `object` | Metadata of the current object type | +| `hasAnySoftDeleteFilterOnView` | `boolean` | Whether the current view has a soft-delete filter | + +### Operators + +Combine variables into boolean expressions: + +| Operator | Description | +| ----------------------------------- | ----------------------------------------------------------------- | +| `isDefined(value)` | `true` if the value is not null/undefined | +| `isNonEmptyString(value)` | `true` if the value is a non-empty string | +| `includes(array, value)` | `true` if the array contains the value | +| `includesEvery(array, prop, value)` | `true` if every item's property includes the value | +| `every(array, prop)` | `true` if the property is truthy on every item | +| `everyDefined(array, prop)` | `true` if the property is defined on every item | +| `everyEquals(array, prop, value)` | `true` if the property equals the value on every item | +| `some(array, prop)` | `true` if the property is truthy on at least one item | +| `someDefined(array, prop)` | `true` if the property is defined on at least one item | +| `someEquals(array, prop, value)` | `true` if the property equals the value on at least one item | +| `someNonEmptyString(array, prop)` | `true` if the property is a non-empty string on at least one item | +| `none(array, prop)` | `true` if the property is falsy on every item | +| `noneDefined(array, prop)` | `true` if the property is undefined on every item | +| `noneEquals(array, prop, value)` | `true` if the property does not equal the value on any item | diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/layout/front-components.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/layout/front-components.mdx new file mode 100644 index 0000000000..0cf39d9210 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/layout/front-components.mdx @@ -0,0 +1,404 @@ +--- +title: Front Components +description: Build React components that render inside Twenty's UI with sandboxed isolation. +icon: window-maximize +--- + +Front components are React components that render directly inside Twenty's UI. They run in an **isolated Web Worker** using Remote DOM — your code is sandboxed but renders natively in the page, not in an iframe. + +## Where front components can be used + +Front components can render in two locations within Twenty: + +* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu. +* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside [page layouts](/l/pt/developers/extend/apps/layout/page-layouts). When configuring a dashboard or a record page layout, users can add a front component widget. + +A front component on its own isn't reachable from the UI — you need to *surface* it. The two ways to do that are: + +* **Pair it with a [command menu item](/l/pt/developers/extend/apps/layout/command-menu-items)** — registers it in the command menu (Cmd+K) and, optionally, as a pinned quick-action. +* **Embed it as a widget in a [page layout](/l/pt/developers/extend/apps/layout/page-layouts)** — places it on a record's detail page or dashboard. + +## Basic example + +The quickest way to see a front component in action is to pair it with a [`defineCommandMenuItem`](/l/pt/developers/extend/apps/layout/command-menu-items), so it appears as a quick-action button in the top-right corner of the page: + +```tsx src/front-components/hello-world.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; + +const HelloWorld = () => { + return ( +
+

Hello from my app!

+

This component renders inside Twenty.

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', + name: 'hello-world', + description: 'A simple front component', + component: HelloWorld, +}); +``` + +```ts src/command-menu-items/hello-world.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345', + shortLabel: 'Hello', + label: 'Hello World', + icon: 'IconBolt', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +After syncing with `yarn twenty dev` (or running a one-shot `yarn twenty dev --once`), the quick action appears in the top-right corner of the page: + +
+ Quick action button in the top-right corner +
+ +Click it to render the component inline. + +## Configuration fields + +| Field | Required | Description | +| --------------------- | -------- | ------------------------------------------------------------ | +| `universalIdentifier` | Yes | Stable unique ID for this component | +| `component` | Yes | A React component function | +| `name` | No | Display name | +| `description` | No | Description of what the component does | +| `isHeadless` | No | Set to `true` if the component has no visible UI (see below) | + +## Placing a front component on a page + +Beyond commands, you can embed a front component directly into a record page by adding it as a widget in a **page layout**. See [Page Layouts](/l/pt/developers/extend/apps/layout/page-layouts) for details. + +## Headless vs non-headless + +Front components come in two rendering modes controlled by the `isHeadless` option: + +**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted. + +**Headless (`isHeadless: true`)** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below. + +```tsx src/front-components/sync-tracker.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component'; +import { useEffect } from 'react'; + +const SyncTracker = () => { + const recordId = useRecordId(); + + useEffect(() => { + enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' }); + }, [recordId]); + + return null; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'sync-tracker', + description: 'Tracks record views silently', + isHeadless: true, + component: SyncTracker, +}); +``` + +Because the component returns `null`, Twenty skips rendering a container for it — no empty space appears in the layout. The component still has access to all hooks and the host communication API. + +## SDK Command components + +The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done. + +Import them from `twenty-sdk/command`: + +* **`Command`** — Runs an async callback via the `execute` prop. +* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`. +* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`. +* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`. + +Here is a full example of a headless front component using `Command` to run an action from the command menu: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +And an example using `CommandModal` to ask for confirmation before executing: + +```tsx src/front-components/delete-draft.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { CommandModal } from 'twenty-sdk/command'; + +const DeleteDraft = () => { + const execute = async () => { + // perform the deletion + }; + + return ( + + ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456', + name: 'delete-draft', + description: 'Deletes a draft with confirmation', + component: DeleteDraft, + isHeadless: true, +}); +``` + +## Accessing runtime context + +Inside your component, use SDK hooks to access the current user, record, and component instance: + +```tsx src/front-components/record-info.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { + useUserId, + useRecordId, + useFrontComponentId, +} from 'twenty-sdk/front-component'; + +const RecordInfo = () => { + const userId = useUserId(); + const recordId = useRecordId(); + const componentId = useFrontComponentId(); + + return ( +
+

User: {userId}

+

Record: {recordId ?? 'No record context'}

+

Component: {componentId}

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012', + name: 'record-info', + component: RecordInfo, +}); +``` + +Available hooks: + +| Hook | Returns | Description | +| --------------------------------------------- | ------------------ | ---------------------------------------------------------- | +| `useUserId()` | `string` or `null` | The current user's ID | +| `useSelectedRecordIds()` | `string[]` | All selected record IDs (empty array if none selected) | +| `useRecordId()` | `string` or `null` | **Deprecated.** Use `useSelectedRecordIds()` instead | +| `useFrontComponentId()` | `string` | This component instance's ID | +| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function | + +## Host communication API + +Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`: + +| Function | Description | +| ----------------------------------------------- | ----------------------------- | +| `navigate(to, params?, queryParams?, options?)` | Navigate to a page in the app | +| `openSidePanelPage(params)` | Open a side panel | +| `closeSidePanel()` | Close the side panel | +| `openCommandConfirmationModal(params)` | Show a confirmation dialog | +| `enqueueSnackbar(params)` | Show a toast notification | +| `unmountFrontComponent()` | Unmount the component | +| `updateProgress(progress)` | Update a progress indicator | + +Here is an example that uses the host API to show a snackbar and close the side panel after an action completes: + +```tsx src/front-components/archive-record.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const ArchiveRecord = () => { + const recordId = useRecordId(); + + const handleArchive = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { status: 'ARCHIVED' } }, + id: true, + }, + }); + + await enqueueSnackbar({ + message: 'Record archived', + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Archive this record?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678', + name: 'archive-record', + description: 'Archives the current record', + component: ArchiveRecord, +}); +``` + +### Working with multiple records + +Use `useSelectedRecordIds()` to handle multiple selected records. This is useful for bulk operations: + +```tsx src/front-components/bulk-export.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const BulkExport = () => { + const selectedRecordIds = useSelectedRecordIds(); + + const handleExport = async () => { + const client = new CoreApiClient(); + + for (const recordId of selectedRecordIds) { + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { exported: true } }, + id: true, + }, + }); + } + + await enqueueSnackbar({ + message: `Exported ${selectedRecordIds.length} records`, + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Export {selectedRecordIds.length} selected record(s)?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901', + name: 'bulk-export', + description: 'Export selected records', + component: BulkExport, + command: { + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902', + label: 'Bulk Export', + availabilityType: 'RECORD_SELECTION', + conditionalAvailabilityExpression: numberOfSelectedRecords > 0, + }, +}); +``` + +## Public assets + +Front components can access files from the app's `public/` directory using `getPublicAssetUrl`: + +```tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +const Logo = () => Logo; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'logo', + component: Logo, +}); +``` + +See the [public assets section](/l/pt/developers/extend/apps/config/public-assets) for details. + +## Styling + +Front components support multiple styling approaches. You can use: + +* **Inline styles** — `style={{ color: 'red' }}` +* **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more) +* **Emotion** — CSS-in-JS with `@emotion/react` +* **Styled-components** — `styled.div` patterns +* **Tailwind CSS** — utility classes +* **Any CSS-in-JS library** compatible with React + +```tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Button, Tag, Status } from 'twenty-sdk/ui'; + +const StyledWidget = () => { + return ( +
+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456', + name: 'styled-widget', + component: StyledWidget, +}); +``` diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/layout/navigation-menu-items.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/layout/navigation-menu-items.mdx new file mode 100644 index 0000000000..6946751826 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/layout/navigation-menu-items.mdx @@ -0,0 +1,44 @@ +--- +title: Navigation Menu Items +description: Add custom entries to the workspace sidebar — links to saved views or external URLs. +icon: bars +--- + +A **navigation menu item** is an entry in the left sidebar. Use `defineNavigationMenuItem()` to ship custom sidebar links — typically one per [view](/l/pt/developers/extend/apps/layout/views) you ship — or to point at external URLs. + +```ts src/navigation-menu-items/example-navigation-menu-item.ts +import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define'; +import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view'; + +export default defineNavigationMenuItem({ + universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c', + name: 'example-navigation-menu-item', + icon: 'IconList', + color: 'blue', + position: 0, + type: NavigationMenuItemType.VIEW, + viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER, +}); +``` + +## Pontos-chave + +* `type` determines what the menu item links to. Each type pairs with a specific identifier field: + + | Tipo | O que faz | Required field | + | ------------------------------------ | ------------------------------------ | ----------------------------------------------------------------------------- | + | `NavigationMenuItemType.VIEW` | Opens a saved view | `viewUniversalIdentifier` | + | `NavigationMenuItemType.LINK` | Opens an external URL | `link` | + | `NavigationMenuItemType.FOLDER` | Groups nested items under a label | `name` (and child items reference the folder via `folderUniversalIdentifier`) | + | `NavigationMenuItemType.OBJECT` | Opens an object's default index page | `targetObjectUniversalIdentifier` | + | `NavigationMenuItemType.PAGE_LAYOUT` | Opens a standalone page layout | `pageLayoutUniversalIdentifier` | + +* `position` controls ordering in the sidebar. + +* `icon` and `color` are optional and customize how the entry looks. + +* `folderUniversalIdentifier` is also available on any item to nest it inside a `FOLDER`-type parent. + + +**Common pitfall:** creating an object without an associated view + navigation menu item makes that object invisible to users. Unless it's a technical/internal object, every custom object should have a default view *and* a sidebar entry pointing at it. + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/layout/overview.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/layout/overview.mdx new file mode 100644 index 0000000000..7b71c1a240 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/layout/overview.mdx @@ -0,0 +1,56 @@ +--- +title: Overview +description: Place your app inside Twenty's UI — sidebar entries, saved views, record page tabs, and sandboxed React components. +icon: table-columns +--- + +A Twenty app's **layout layer** is everything the user sees: where the app surfaces in the sidebar, which list views it ships, how its record detail pages are arranged, and which custom React components render inside those pages. + +```text + Sidebar Record list Record detail page + ─────── ─────────── ────────────────── + [📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐ + [📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │ + [📋 Inbox ] │ ──────── │ │ [Notes ] │ + ▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab + │ │ Acme │ │ │ adds a tab... + └ defineNavi- │ … │ │ ┌────────────────┐ │ + gationMenu- └────▲─────┘ │ │ │ │ + Item points │ │ │ React UI │◀── …with a + to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent + └ defineView │ │ a Worker) │ │ widget inside + picks columns │ └────────────────┘ │ + and filters └─────────────────────┘ +``` + +## In this section + + + + `defineView` — saved list configurations: visible columns, filters, groups. + + + `defineNavigationMenuItem` — sidebar entries pointing at views or external URLs. + + + `definePageLayout` and `definePageLayoutTab` — tabs and widgets on a record's detail page. + + + `defineFrontComponent` — sandboxed React components that render inside Twenty. + + + `defineCommandMenuItem` — register front components as Cmd+K entries and quick actions. + + + +## Where the app surfaces + +| Surface | What it controls | Entity | +| --------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------- | +| **Sidebar** | A custom entry linking to a saved view or external URL | `defineNavigationMenuItem` | +| **Record list** | A saved configuration for an object — visible columns, order, filters, groups | `defineView` | +| **Record detail page** | The tabs and widgets on a record page (your own object's, or a standard one) | `definePageLayout`, `definePageLayoutTab` | +| **Inside any of the above** | A custom React widget — buttons, forms, dashboards, integrations | `defineFrontComponent` | +| **Command menu (Cmd+K)** | A pinned quick action or hidden command | `defineCommandMenuItem` | + +Front components run inside an isolated Web Worker using Remote DOM — they render *natively* in the page (not inside an iframe), but cannot reach the host page or DOM directly. Communication with Twenty happens through a message-passing host API. diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/layout/page-layouts.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/layout/page-layouts.mdx new file mode 100644 index 0000000000..262c319aa4 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/layout/page-layouts.mdx @@ -0,0 +1,102 @@ +--- +title: Page Layouts +description: Customize record detail pages — tabs, widgets, and where front components render — using definePageLayout and definePageLayoutTab. +icon: table-columns +--- + +A **page layout** controls how a record's detail page is arranged: which tabs appear and what widgets they contain. Use `definePageLayout()` to declare a layout for an object you own, or `definePageLayoutTab()` to add a single tab to a layout that already exists (yours or a standard Twenty one). + +| Use case | Entity | +| ---------------------------------------------------------------------- | --------------------- | +| Define the entire layout for a record page on an object you own | `definePageLayout` | +| Add one tab to an existing layout (your own object, or a standard one) | `definePageLayoutTab` | + +## definePageLayout + +Use this when you own the entire detail page — typically for a custom object you defined yourself. + +```ts src/page-layouts/example-record-page-layout.ts +import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +export default definePageLayout({ + universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134', + name: 'Example Record Page', + type: 'RECORD_PAGE', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + tabs: [ + { + universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5', + title: 'Hello World', + position: 50, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], + }, + ], +}); +``` + +### Key points + +* `type` is typically `'RECORD_PAGE'` to customize the detail view of a specific object. +* `objectUniversalIdentifier` specifies which object this layout applies to. +* Each `tab` defines a section of the page with a `title`, `position`, and `layoutMode` (`CANVAS` for free-form layout). +* Each `widget` inside a tab can render a [front component](/l/pt/developers/extend/apps/layout/front-components), a relation list, or other built-in widget types. +* `position` on tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones. + +## definePageLayoutTab + +Use this when you only want to **add** a tab to an existing layout — for example, an analytics tab on the standard Company page, or an AI summary tab attached to your own object's layout. + +```ts src/page-layouts/example-extra-tab.ts +import { + definePageLayoutTab, + PageLayoutTabLayoutMode, +} from 'twenty-sdk/define'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = + '20202020-ab01-4001-8001-c0aba11c0100'; + +export default definePageLayoutTab({ + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001', + pageLayoutUniversalIdentifier: + COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER, + title: 'Hello World', + position: 1000, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], +}); +``` + +### Key points + +* `pageLayoutUniversalIdentifier` is **required** and must point to a page layout that already exists at install time — either a standard Twenty layout or one defined by your own app. Cross-app references to layouts owned by another installed app are not supported today. When the parent layout is missing, installation fails with a clear validation error. +* `widgets` are scoped to this tab only — they reference [front components](/l/pt/developers/extend/apps/layout/front-components), views, etc. exactly like widgets defined inline in `definePageLayout`. +* `position` controls ordering against existing tabs on the targeted layout. Pick a value that places your tab where you want it relative to built-in tabs. +* Use this instead of `definePageLayout` when you only want to add to an existing layout. Use `definePageLayout` when you own the entire layout. diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/layout/views.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/layout/views.mdx new file mode 100644 index 0000000000..2659946f87 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/layout/views.mdx @@ -0,0 +1,43 @@ +--- +title: Views +description: Ship pre-configured saved views — column order, filters, groups — for objects in your app. +icon: list +--- + +A **view** is a saved configuration for how records of an object are displayed: which fields appear, their order, whether they're visible, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app — typically a default index view for each custom object you create. + +```ts src/views/example-view.ts +import { defineView, ViewKey } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; + +export default defineView({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'All example items', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + icon: 'IconList', + key: ViewKey.INDEX, + position: 0, + fields: [ + { + universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0', + fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER, + position: 0, + isVisible: true, + size: 200, + }, + ], +}); +``` + +## Key points + +* `objectUniversalIdentifier` specifies which object this view applies to. It can be a custom object you defined or a standard Twenty object. +* `key` determines the view type — `ViewKey.INDEX` is the main list view for the object. +* `fields` controls which columns appear and in what order. Each field references a `fieldMetadataUniversalIdentifier`. +* You can also declare `filters`, `filterGroups`, `groups`, and `fieldGroups` for advanced configurations. +* `position` controls ordering when multiple views exist for the same object. + +## How views show up in the UI + +A view by itself isn't reachable from the sidebar. To make it appear there, pair it with a [navigation menu item](/l/pt/developers/extend/apps/layout/navigation-menu-items) of type `VIEW` that points at the view's `universalIdentifier`. That's the canonical pattern: every custom object typically ships a default view + a sidebar entry that opens it. diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/logic/connections.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/logic/connections.mdx new file mode 100644 index 0000000000..1a99c59742 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/logic/connections.mdx @@ -0,0 +1,193 @@ +--- +title: Connections +description: Let your app act on a user's behalf in third-party services via OAuth. +icon: plug +--- + +Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API. + +Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate. + + + + + +A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace. + +A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials. + +```ts src/connection-providers/linear-connection.ts +import { defineConnectionProvider } from 'twenty-sdk/define'; + +export default defineConnectionProvider({ + universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f', + name: 'linear', + displayName: 'Linear', + icon: 'IconBrandLinear', + type: 'oauth', + oauth: { + authorizationEndpoint: 'https://linear.app/oauth/authorize', + tokenEndpoint: 'https://api.linear.app/oauth/token', + scopes: ['read', 'write'], + // These must match keys in `defineApplication.serverVariables` below. + clientIdVariable: 'LINEAR_CLIENT_ID', + clientSecretVariable: 'LINEAR_CLIENT_SECRET', + // Optional: defaults to 'json'. Some providers (Linear, Slack) want + // 'form-urlencoded' for the token request. + tokenRequestContentType: 'form-urlencoded', + // Optional: defaults to true. Disable only if the provider rejects PKCE. + usePkce: false, + // Optional: extra query params on the authorize URL. + // authorizationParams: { prompt: 'consent' }, + // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect. + // revokeEndpoint: 'https://example.com/oauth/revoke', + }, +}); +``` + +```ts src/application.config.ts +import { defineApplication } from 'twenty-sdk/define'; + +export default defineApplication({ + universalIdentifier: '...', + displayName: 'Linear', + description: 'Connect Linear to Twenty.', + defaultRoleUniversalIdentifier: '...', + // OAuth client credentials live on the app registration (one OAuth app per + // Twenty server, configured by the admin) — not per-workspace. Declare them + // as serverVariables so the admin can fill them in once for all installs. + serverVariables: { + LINEAR_CLIENT_ID: { + description: 'OAuth client ID from your Linear OAuth application.', + isSecret: false, + isRequired: true, + }, + LINEAR_CLIENT_SECRET: { + description: 'OAuth client secret from your Linear OAuth application.', + isSecret: true, + isRequired: true, + }, + }, +}); +``` + +Key points: + +* `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`). +* `displayName` shows in the per-app settings tab and in the AI tool list. +* `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo. +* Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server. +* Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled. +* `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`. + +The OAuth callback URL your provider needs to whitelist is: + +``` +https:///apps/oauth/callback +``` + + + + + +Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens. + +```ts src/logic-functions/handlers/create-linear-issue-handler.ts +import { listConnections } from 'twenty-sdk/logic-function'; + +export const createLinearIssueHandler = async (input: { + teamId?: string; + title?: string; +}) => { + if (!input.teamId || !input.title) { + return { success: false, error: 'teamId and title are required' }; + } + + const connections = await listConnections({ providerName: 'linear' }); + + // Workspace-shared credentials win when present; fall back to the first + // user-visibility one. For HTTP-route triggers you typically pick the + // request user's connection via event.userWorkspaceId instead. + const connection = + connections.find((c) => c.visibility === 'workspace') ?? connections[0]; + + if (!connection) { + return { + success: false, + error: + 'Linear is not connected. Open the app settings and click "Add connection".', + }; + } + + // Use connection.accessToken to call the third-party API. + const response = await fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + Authorization: `Bearer ${connection.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`, + }), + }); + + return { success: response.ok }; +}; +``` + +Each connection has: + +| Field | Description | +| ----------------- | -------------------------------------------------------------------------------------------------------- | +| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one | +| `visibility` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) | +| `scopes` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) | +| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers | +| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) | +| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) | +| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect | + +Key points: + +* Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers. +* The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set). +* `getConnection(id)` is the single-row equivalent. + + + + + +When a user clicks "Add connection," they're prompted to pick a visibility: + +* **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not. +* **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user. + +Use the right one for each handler: + +```ts +// HTTP-route trigger — prefer the request user's own connection. +const conn = + connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ?? + connections.find((c) => c.visibility === 'workspace'); + +// Cron trigger — no request user; only shared credentials are sensible. +const conn = connections.find((c) => c.visibility === 'workspace'); +``` + +Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side. + + + + + +For each connection provider, the server admin needs to register an OAuth app at the third party first. + +1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new). +2. Set the **Redirect URI** to `\/apps/oauth/callback`. +3. Copy the generated **Client ID** and **Client Secret**. +4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`. +5. Workspace members can then add connections from the per-app **Connections** section. + + + + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/logic/logic-functions.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/logic/logic-functions.mdx new file mode 100644 index 0000000000..cd72fdeeb5 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/logic/logic-functions.mdx @@ -0,0 +1,375 @@ +--- +title: Logic Functions +description: Define server-side TypeScript functions with HTTP, cron, and database event triggers. +icon: bolt +--- + +Logic functions are server-side TypeScript functions that run on the Twenty platform. They can be triggered by HTTP requests, cron schedules, or database events — and can also be exposed as tools for AI agents. + + + + +Each function file uses `defineLogicFunction()` to export a configuration with a handler and optional triggers. + +```ts src/logic-functions/createPostCard.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define'; +import { CoreApiClient, type Person } from 'twenty-client-sdk/core'; + +const handler = async (params: RoutePayload) => { + const client = new CoreApiClient(); + const body = (params.body ?? {}) as { name?: string }; + const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'; + + const result = await client.mutation({ + createPostCard: { + __args: { data: { name } }, + id: true, + name: true, + }, + }); + return result; +}; + +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'create-new-post-card', + timeoutSeconds: 2, + handler, + httpRouteTriggerSettings: { + path: '/post-card/create', + httpMethod: 'POST', + isAuthRequired: true, + }, + /*databaseEventTriggerSettings: { + eventName: 'people.created', + },*/ + /*cronTriggerSettings: { + pattern: '0 0 1 1 *', + },*/ +}); +``` + +Available trigger types: +* **httpRoute**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**: +> e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create` +* **cron**: Runs your function on a schedule using a CRON expression. +* **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function. +> e.g. `person.updated`, `*.created`, `company.*` + + +You can also manually execute a function using the CLI: + +```bash filename="Terminal" +yarn twenty exec -n create-new-post-card -p '{"key": "value"}' +``` + +```bash filename="Terminal" +yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + +You can watch logs with: + +```bash filename="Terminal" +yarn twenty logs +``` + + +#### Route trigger payload + +When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the +[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). +Import the `RoutePayload` type from `twenty-sdk`: + +```ts +import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define'; + +const handler = async (event: RoutePayload) => { + const { headers, queryStringParameters, pathParameters, body } = event; + const { method, path } = event.requestContext.http; + + return { message: 'Success' }; +}; +``` + +The `RoutePayload` type has the following structure: + + | Property | Type | Description | Example | + | ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | + | `headers` | `Record\` | HTTP headers (only those listed in `forwardedRequestHeaders`) | see section below | + | `queryStringParameters` | `Record\` | Query string parameters (multiple values joined with commas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` | + | `pathParameters` | `Record\` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` | + | `body` | `object \| null` | Parsed request body (JSON) | `{ id: 1 }` -> `{ id: 1 }` | + | `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | | + | `isBase64Encoded` | `boolean` | Whether the body is base64 encoded | | + | `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) | | + | `requestContext.http.path` | `string` | Raw request path | | + + +#### forwardedRequestHeaders + +By default, HTTP headers from incoming requests are **not** passed to your logic function for security reasons. +To access specific headers, list them in the `forwardedRequestHeaders` array: + +```ts +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'webhook-handler', + handler, + httpRouteTriggerSettings: { + path: '/webhook', + httpMethod: 'POST', + isAuthRequired: false, + forwardedRequestHeaders: ['x-webhook-signature', 'content-type'], + }, +}); +``` + +In your handler, access the forwarded headers like this: + +```ts +const handler = async (event: RoutePayload) => { + const signature = event.headers['x-webhook-signature']; + const contentType = event.headers['content-type']; + + // Validate webhook signature... + return { received: true }; +}; +``` + + +Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`). + + +#### Exposing a function as an AI tool or workflow action + +Logic functions can be exposed on two surfaces, each with its own trigger: + +* **`toolTriggerSettings`** — makes the function discoverable by Twenty's AI features (chat, MCP, function calling). Uses standard JSON Schema, the format LLMs natively understand. +* **`workflowActionTriggerSettings`** — makes the function appear as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper field editors, variable pickers, and labels. + +A function can opt into one, the other, or both. They sit alongside `cronTriggerSettings`, `databaseEventTriggerSettings`, and `httpRouteTriggerSettings` — same pattern, same shape. + +```ts src/logic-functions/enrich-company.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const handler = async (params: { companyName: string; domain?: string }) => { + const client = new CoreApiClient(); + + 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, + toolTriggerSettings: {}, +}); +``` + +Key points: + +* A function can mix surfaces — declare both `toolTriggerSettings` and `workflowActionTriggerSettings` to expose it in chat AND in the workflow builder. +* `toolTriggerSettings.inputSchema` and `workflowActionTriggerSettings.inputSchema` are both optional. When omitted, the manifest builder infers them from the handler source code (JSON Schema for the AI tool, Twenty's `InputSchema` for the workflow action). Provide one explicitly when you want richer typing — for example, with `FieldMetadataType`-aware fields like `CURRENCY` or `RELATION` for the workflow builder, or with `description` fields the AI agent can read: + +```ts +export default defineLogicFunction({ + ..., + toolTriggerSettings: { + inputSchema: { + 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'], + }, + }, +}); +``` + + +**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. + + + + + + +**Install hooks** — pre-install and post-install handlers — share this runtime but are declared with their own define functions and don't take trigger settings. See [Install Hooks](/l/pt/developers/extend/apps/config/install-hooks) for `definePreInstallLogicFunction` and `definePostInstallLogicFunction`. + + +## Typed API clients (twenty-client-sdk) + +The `twenty-client-sdk` package provides two typed GraphQL clients for interacting with the Twenty API from your logic functions and front components. + +| Client | Import | Endpoint | Generated? | +| ------------------- | ---------------------------- | ---------------------------------------------- | ---------------------- | +| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — workspace data (records, objects) | Yes, at dev/build time | +| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — workspace config, file uploads | No, ships pre-built | + + + + +`CoreApiClient` is the main client for querying and mutating workspace data. It is **generated from your workspace schema** during `yarn twenty dev` or `yarn twenty build`, so it is fully typed to match your objects and fields. + +```ts +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const client = new CoreApiClient(); + +// Query records +const { companies } = await client.query({ + companies: { + edges: { + node: { + id: true, + name: true, + domainName: { + primaryLinkLabel: true, + primaryLinkUrl: true, + }, + }, + }, + }, +}); + +// Create a record +const { createCompany } = await client.mutation({ + createCompany: { + __args: { + data: { + name: 'Acme Corp', + }, + }, + id: true, + name: true, + }, +}); +``` + +The client uses a selection-set syntax: pass `true` to include a field, use `__args` for arguments, and nest objects for relations. You get full autocompletion and type checking based on your workspace schema. + + +**CoreApiClient is generated at dev/build time.** If you use it without running `yarn twenty dev` or `yarn twenty build` first, it throws an error. The generation happens automatically — the CLI introspects your workspace's GraphQL schema and generates a typed client using `@genql/cli`. + + +#### Using CoreSchema for type annotations + +`CoreSchema` provides TypeScript types matching your workspace objects — useful for typing component state or function parameters: + +```ts +import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core'; +import { useState } from 'react'; + +const [company, setCompany] = useState< + Pick | undefined +>(undefined); + +const client = new CoreApiClient(); +const result = await client.query({ + company: { + __args: { filter: { position: { eq: 1 } } }, + id: true, + name: true, + }, +}); +setCompany(result.company); +``` + + + + +`MetadataApiClient` ships pre-built with the SDK (no generation required). It queries the `/metadata` endpoint for workspace configuration, applications, and file uploads. + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; + +const metadataClient = new MetadataApiClient(); + +// List first 10 objects in the workspace +const { objects } = await metadataClient.query({ + objects: { + edges: { + node: { + id: true, + nameSingular: true, + namePlural: true, + labelSingular: true, + isCustom: true, + }, + }, + __args: { + filter: {}, + paging: { first: 10 }, + }, + }, +}); +``` + +#### Uploading files + +`MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields: + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import * as fs from 'fs'; + +const metadataClient = new MetadataApiClient(); + +const fileBuffer = fs.readFileSync('./invoice.pdf'); + +const uploadedFile = await metadataClient.uploadFile( + fileBuffer, // file contents as a Buffer + 'invoice.pdf', // filename + 'application/pdf', // MIME type + '58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier +); + +console.log(uploadedFile); +// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' } +``` + +| Parameter | Type | Description | +| ---------------------------------- | -------- | --------------------------------------------------------------- | +| `fileBuffer` | `Buffer` | The raw file contents | +| `filename` | `string` | The name of the file (used for storage and display) | +| `contentType` | `string` | MIME type (defaults to `application/octet-stream` if omitted) | +| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object | + +Key points: +* Uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed. +* The returned `url` is a signed URL you can use to access the uploaded file. + + + + + + When your code runs on Twenty (logic functions or front components), the platform injects credentials as environment variables: + + * `TWENTY_API_URL` — Base URL of the Twenty API + * `TWENTY_APP_ACCESS_TOKEN` — Short-lived key scoped to your application's default function role + + You do **not** need to pass these to the clients — they read from `process.env` automatically. The API key's permissions are determined by the role referenced in `defaultRoleUniversalIdentifier` in your `application-config.ts`. + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/logic/overview.mdx new file mode 100644 index 0000000000..8874b249ed --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/logic/overview.mdx @@ -0,0 +1,55 @@ +--- +title: Overview +description: Server-side TypeScript that runs inside Twenty — triggered by HTTP routes, cron schedules, database events, AI tools, or workflow actions. +icon: bolt +--- + +A Twenty app's **logic layer** is the code that *runs* — server-side TypeScript handlers reacting to HTTP requests, cron schedules, and record changes; AI skills and agents that live inside the workspace; and OAuth connections that let your functions act on a user's behalf in third-party services. + +```text + ┌─ HTTP route ──┐ + │ Cron schedule │ + │ Database event │ ┌────────────────────┐ + triggers ─┤ AI tool call ├─────▶│ Logic function │ + │ Workflow action │ │ (your handler) │ + │ Manual exec │ └────────────────────┘ + └────────────────────┘ │ + ▼ + ┌────────────────────────────┐ + │ Twenty API (records) │ + │ Third-party API │ + │ (via Connection token) │ + └────────────────────────────┘ +``` + +## In this section + + + + The core building block — trigger types, payloads, and the typed API client. + + + Reusable AI agent instructions and assistants with custom system prompts. + + + OAuth credentials your app holds for third-party services — Linear, GitHub, Slack, and more. + + + +## Trigger types at a glance + +A logic function picks one or more triggers — every entry below is a separate field on `defineLogicFunction()`: + +| Trigger | When it runs | Setting | +| ------------------- | ---------------------------------------------------------- | ------------------------------- | +| **HTTP route** | A request hits your `/s/\` endpoint | `httpRouteTriggerSettings` | +| **Cron** | A CRON expression matches | `cronTriggerSettings` | +| **Database event** | A workspace record is created, updated, or deleted | `databaseEventTriggerSettings` | +| **AI tool** | A Twenty AI feature decides to call your function | `toolTriggerSettings` | +| **Workflow action** | A workflow step invokes your function | `workflowActionTriggerSettings` | + +Functions run sandboxed in isolated Node.js processes and access the workspace through a typed API client scoped to the role declared on [`defineApplication()`](/l/pt/developers/extend/apps/config/application). + + +**Install-time hooks** — code that runs before or after the install — share this runtime but use their own define functions and live under [Config → Install Hooks](/l/pt/developers/extend/apps/config/install-hooks). + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/logic/skills-and-agents.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/logic/skills-and-agents.mdx new file mode 100644 index 0000000000..82a66ef20f --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/logic/skills-and-agents.mdx @@ -0,0 +1,69 @@ +--- +title: Skills & Agents +description: Define AI skills and agents for your app. +icon: robot +--- + + + Skills and agents are currently in alpha. The feature works but is still evolving. + + +Apps can define AI capabilities that live inside the workspace — reusable skill instructions and agents with custom system prompts. + + + + +Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation: + +```ts src/skills/example-skill.ts +import { defineSkill } from 'twenty-sdk/define'; + +export default defineSkill({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'sales-outreach', + label: 'Sales Outreach', + description: 'Guides the AI agent through a structured sales outreach process', + icon: 'IconBrain', + content: `You are a sales outreach assistant. When reaching out to a prospect: +1. Research the company and recent news +2. Identify the prospect's role and likely pain points +3. Draft a personalized message referencing specific details +4. Keep the tone professional but conversational`, +}); +``` + +Key points: +* `name` is a unique identifier string for the skill (kebab-case recommended). +* `label` is the human-readable display name shown in the UI. +* `content` contains the skill instructions — this is the text the AI agent uses. +* `icon` (optional) sets the icon displayed in the UI. +* `description` (optional) provides additional context about the skill's purpose. + + + + +Agents are AI assistants that live inside your workspace. Use `defineAgent()` to create agents with a custom system prompt: + +```ts src/agents/example-agent.ts +import { defineAgent } from 'twenty-sdk/define'; + +export default defineAgent({ + universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123', + name: 'sales-assistant', + label: 'Sales Assistant', + description: 'Helps the sales team draft outreach emails and research prospects', + icon: 'IconRobot', + prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.', +}); +``` + +Key points: +* `name` is the unique identifier string for the agent (kebab-case recommended). +* `label` is the display name shown in the UI. +* `prompt` is the system prompt that defines the agent's behavior. +* `description` (optional) provides context about what the agent does. +* `icon` (optional) sets the icon displayed in the UI. +* `modelId` (optional) overrides the default AI model used by the agent. + + + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/operations/cli.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/operations/cli.mdx new file mode 100644 index 0000000000..0cccebdbb5 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/operations/cli.mdx @@ -0,0 +1,78 @@ +--- +title: CLI +description: yarn twenty commands for executing functions, streaming logs, managing app installations, and switching remotes. +icon: terminal +--- + +Beyond `dev`, `build`, `add`, and `typecheck`, the `yarn twenty` CLI provides commands for executing functions, viewing logs, and managing app installations. + +## Executing functions (`yarn twenty exec`) + +Run a logic function manually without triggering it via HTTP, cron, or database event: + +```bash filename="Terminal" +# Execute by function name +yarn twenty exec -n create-new-post-card + +# Execute by universalIdentifier +yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf + +# Pass a JSON payload +yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}' + +# Execute the post-install function +yarn twenty exec --postInstall +``` + +## Viewing function logs (`yarn twenty logs`) + +Stream execution logs for your app's logic functions: + +```bash filename="Terminal" +# Stream all function logs +yarn twenty logs + +# Filter by function name +yarn twenty logs -n create-new-post-card + +# Filter by universalIdentifier +yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + + +This is different from `yarn twenty server logs`, which shows the Docker container logs. `yarn twenty logs` shows your app's function execution logs from the Twenty server. + + +## Uninstalling an app (`yarn twenty uninstall`) + +Remove your app from the active workspace: + +```bash filename="Terminal" +yarn twenty uninstall + +# Skip the confirmation prompt +yarn twenty uninstall --yes +``` + +## Managing remotes + +A **remote** is a Twenty server that your app connects to. During setup, the scaffolder creates one for you automatically. You can add more remotes or switch between them at any time. + +```bash filename="Terminal" +# Add a new remote (opens a browser for OAuth login) +yarn twenty remote add + +# Connect to a local Twenty server (auto-detects port 2020 or 3000) +yarn twenty remote add --local + +# Add a remote non-interactively (useful for CI) +yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote + +# List all configured remotes +yarn twenty remote list + +# Switch the active remote +yarn twenty remote switch +``` + +Your credentials are stored in `~/.twenty/config.json`. diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/operations/overview.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/operations/overview.mdx new file mode 100644 index 0000000000..a4dba84ba2 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/operations/overview.mdx @@ -0,0 +1,29 @@ +--- +title: Overview +description: Build, test, and ship your app — CLI commands, integration tests, CI, and publishing to a server or to npm. +icon: rocket +--- + +The **operations layer** is everything you do *to* your app rather than *with* it: invoking CLI commands, running integration tests against a real Twenty server, configuring CI, and shipping releases — either as a tarball deployed to a single server or as an npm package listed in the marketplace. + +```text + develop ─▶ test ─▶ build ─▶ deploy / publish + ─────── ──── ───── ───────────────── + yarn yarn yarn yarn twenty deploy (tarball → one server) + twenty test twenty + dev build yarn twenty publish (npm → marketplace) +``` + +## In this section + + + + `yarn twenty` reference — exec, logs, uninstall, remotes. + + + Vitest setup, integration tests, type checking, CI workflow. + + + Build, deploy a tarball, publish to npm, install. + + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/operations/publishing.mdx new file mode 100644 index 0000000000..5d2b3b531c --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/operations/publishing.mdx @@ -0,0 +1,295 @@ +--- +title: Publishing +icon: upload +description: Distribute your Twenty app to the marketplace or deploy it internally. +--- + +## Overview + +Once your app is [built and tested locally](/l/pt/developers/extend/apps/getting-started/concepts), you have two paths for distributing it: + +* **Deploy a tarball** — upload your app directly to a specific Twenty server for internal or private use. +* **Publish to npm** — list your app in the Twenty marketplace for any workspace to discover and install. + +Both paths start from the same **build** step. + +## Building your app + +Run the build command to compile your app and generate a distribution-ready `manifest.json`: + +```bash filename="Terminal" +yarn twenty build +``` + +This compiles TypeScript sources, transpiles logic functions and front components, and writes everything to `.twenty/output/`. Add `--tarball` to also produce a `.tgz` package for manual distribution or the deploy command. + +## Deploying to a server (tarball) + +For apps you don't want publicly available — proprietary tools, enterprise-only integrations, or experimental builds — you can deploy a tarball directly to a Twenty server. + +### Prerequisites + +Before deploying, you need a configured remote pointing to the target server. Remotes store the server URL and authentication credentials locally in `~/.twenty/config.json`. + +Add a remote: + +```bash filename="Terminal" +yarn twenty remote add --api-url https://your-twenty-server.com --as production +``` + +### Deploying + +Build and upload your app to the server in one step: + +```bash filename="Terminal" +yarn twenty deploy +# To deploy to a specific remote: +# yarn twenty deploy --remote production +``` + +### Sharing a deployed app + + +Sharing private (tarball) apps across workspaces is an **Enterprise** feature. The **Distribution** tab will show an upgrade prompt instead of the share controls until your workspace has a valid Enterprise key. See [Settings > Admin Panel > Enterprise](/settings/admin-panel#enterprise) to activate it. + + +Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. Once your workspace is on the Enterprise plan, you can share a deployed app like this: + +1. Go to **Settings > Applications > Registrations** and open your app +2. In the **Distribution** tab, click **Copy share link** +3. Share this link with users on other workspaces — it takes them directly to the app's install page + +The share link uses the server's base URL (without any workspace subdomain) so it works for any workspace on the server. + +### Version management + +When updating an already deployed tarball app, the server requires the `version` in `package.json` to be **strictly higher** (per [semver](https://semver.org) ordering) than the currently deployed version. Re-deploying the same version, or pushing a lower one, is rejected before the tarball is stored — you'll see a `VERSION_ALREADY_EXISTS` error from the CLI. + +To release an update: + +1. Bump the `version` field in your `package.json` (e.g. `1.2.3` → `1.2.4`, `1.3.0`, or `2.0.0`) +2. Run `yarn twenty deploy` (or `yarn twenty deploy --remote production`) +3. Workspaces that have the app installed will see the upgrade available in their settings + + +Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allowed, and a final release like `1.0.0` is correctly recognized as higher than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver string. + + +{/* TODO: add screenshot of the Upgrade button */} + +### Server version compatibility + +If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`: + +```json filename="package.json" +{ + "name": "twenty-my-app", + "version": "1.0.0", + "engines": { + "node": "^24.5.0", + "twenty": ">=2.3.0" + } +} +``` + +The value is a standard [semver range](https://github.com/npm/node-semver#ranges). Common patterns: + +| Range | Meaning | +| ---------------------------------- | ------------------------------------------ | +| `>=2.3.0` | Any server from 2.3.0 onward | +| `>=2.3.0 \<3.0.0` | 2.3.0 or later, but below the next major | +| `^2.3.0` | Same as `>=2.3.0 \<3.0.0` | + +**What happens at deploy and install time:** + +* If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version. +* If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps). +* If the server has no `APP_VERSION` configured, the check is skipped. + + +The server is the authoritative check — it validates `engines.twenty` on both tarball upload and workspace install. If you deploy a tarball out-of-band or install from the marketplace, the server still enforces compatibility. + + +## Automated CI/CD (scaffolded workflows) + +Apps generated with `create-twenty-app` ship with two GitHub Actions workflows out of the box, under `.github/workflows/`. They are ready to run as soon as you push the repo to GitHub — no extra setup is needed for CI, and CD only requires a single secret. + +### CI — `ci.yml` + +Runs integration tests on every push to `main` and every pull request. + +**What it does:** + +1. Checks out your app's source. +2. Spawns an isolated Twenty test instance using the `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` composite action (the CI equivalent of `yarn twenty server start --test`). +3. Enables Corepack, sets up Node.js from your `.nvmrc`, and installs dependencies with `yarn install --immutable`. +4. Runs `yarn test`, passing `TWENTY_API_URL` and `TWENTY_API_KEY` from the spawned instance so your tests can talk to a real server. + +**Config knobs:** + +* `TWENTY_VERSION` (env, defaults to `latest`) — pin the Twenty server version used in CI by editing this in `ci.yml`. +* Concurrency is grouped by `github.ref` and cancels in-progress runs on new pushes. + +No secrets are required — the test instance is ephemeral and lives only for the duration of the job. + +### CD — `cd.yml` + +Deploys your app to a configured Twenty server on every push to `main`, and optionally from a pull request when the `deploy` label is applied. + +**What it does:** + +1. Checks out the PR head (for labeled PRs) or the pushed commit. +2. Runs `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — the CI equivalent of `yarn twenty deploy`. +3. Runs `twentyhq/twenty/.github/actions/install-twenty-app@main` so the newly deployed version is installed into the target workspace. + +**Required configuration:** + +| Setting | Where | Purpose | +| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `TWENTY_DEPLOY_URL` | `env` in `cd.yml` (defaults to `http://localhost:3000`) | The Twenty server to deploy to. Change this to your real server URL before first use. | +| `TWENTY_DEPLOY_API_KEY` | GitHub repo **Settings → Secrets and variables → Actions** | API key with deploy permission on the target server. | + + +The default `TWENTY_DEPLOY_URL` of `http://localhost:3000` is a placeholder — it will not reach anything from a GitHub-hosted runner. Update it to your server's public URL (or use a self-hosted runner with network access) before enabling CD. + + +**Triggering a preview deploy from a PR:** + +Add the `deploy` label to a pull request. The `if:` guard in `cd.yml` will run the job for that PR using the PR's head commit, letting you validate a change on the target server before merging. + +### Pinning the reusable actions + +Both workflows reference reusable actions at `@main`, so action updates in the `twentyhq/twenty` repo are picked up automatically. If you want deterministic builds, replace `@main` with a commit SHA or release tag on each `uses:` line. + +## Publishing to npm + +Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twenty workspace can browse, install, and upgrade marketplace apps directly from the UI. + +### Requirements + +* An [npm](https://www.npmjs.com) account +* The `twenty-app` keyword in your `package.json` `keywords` array (add it manually — it is not included by default in the `create-twenty-app` template) + +```json filename="package.json" +{ + "name": "twenty-app-postcard-sender", + "version": "1.0.0", + "keywords": ["twenty-app"] +} +``` + +### Marketplace metadata + +The `defineApplication()` config supports optional fields that control how your app appears in the marketplace. Use `logoUrl` and `screenshots` to reference images from the `public/` folder: + +```ts src/application-config.ts +export default defineApplication({ + universalIdentifier: '...', + displayName: 'My App', + description: 'A great app', + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + logoUrl: 'public/logo.png', + screenshots: [ + 'public/screenshot-1.png', + 'public/screenshot-2.png', + ], +}); +``` + +See the [defineApplication accordion](/l/pt/developers/extend/apps/config/application#marketplace-metadata) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.). + +#### Recommended screenshot dimensions + +The marketplace renders `screenshots` in a fixed `8:5` container (for example, `1600×1000 px`). + + +Screenshots of any aspect ratio are displayed in full and are never cropped, but anything significantly taller or narrower than `8:5` will show empty bands on the sides. + + +### Publish + +```bash filename="Terminal" +yarn twenty publish +``` + +To publish under a specific dist-tag (e.g., `beta` or `next`): + +```bash filename="Terminal" +yarn twenty publish --tag beta +``` + +### How marketplace discovery works + +The Twenty server syncs its marketplace catalog from the npm registry **every hour**. + +You can trigger the sync immediately instead of waiting: + +```bash filename="Terminal" +yarn twenty server catalog-sync +# To target a specific remote: +# yarn twenty server catalog-sync --remote production +``` + +The metadata shown in the marketplace comes from your `defineApplication()` config — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`. + + +If your app does not define an `aboutDescription` in `defineApplication()`, the marketplace will automatically use your package's `README.md` from npm as the about page content. This means you can maintain a single README for both npm and the Twenty marketplace. If you want a different description in the marketplace, explicitly set `aboutDescription`. + + +### CI publishing + +Use this GitHub Actions workflow to publish automatically on every release (uses [OIDC](https://docs.npmjs.com/trusted-publishers)): + +```yaml filename=".github/workflows/publish.yml" +name: Publish +on: + release: + types: [published] + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: https://registry.npmjs.org + - run: yarn install --immutable + - run: npx twenty build + - run: npm publish --provenance --access public + working-directory: .twenty/output +``` + +For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `yarn twenty build`, then `npm publish` from `.twenty/output`. + + +**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions. + + +## Installing apps + +Once an app is published (npm) or deployed (tarball), workspaces can install it through the UI. + +Go to the **Settings > Applications** page in Twenty, where both marketplace and tarball-deployed apps can be browsed and installed. + +{/* TODO: add screenshot of the UI when the app is registered */} + +You can also install apps from the command line: + +```bash filename="Terminal" +yarn twenty install +``` + + +The server enforces semver versioning on install, mirroring the rules on deploy: + +* Installing the same version that is already installed in your workspace is rejected with an `APP_ALREADY_INSTALLED` error. +* Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error. + +To install a newer version, deploy or publish it first, then re-run `yarn twenty install`. + diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/operations/testing.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/operations/testing.mdx new file mode 100644 index 0000000000..b1959be0ed --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/operations/testing.mdx @@ -0,0 +1,301 @@ +--- +title: Testing +description: Vitest setup, integration tests against a real Twenty server, type checking, and CI with GitHub Actions. +icon: flask +--- + +The SDK provides programmatic APIs that let you build, deploy, install, and uninstall your app from test code. Combined with [Vitest](https://vitest.dev/) and the typed API clients, you can write integration tests that verify your app works end-to-end against a real Twenty server. + +## Using npm packages + +You can install and use any npm package in your app. Both logic functions and front components are bundled with [esbuild](https://esbuild.github.io/), which inlines all dependencies into the output — no `node_modules` are needed at runtime. + +### Installing a package + +```bash filename="Terminal" +yarn add axios +``` + +Then import it in your code: + +```ts src/logic-functions/fetch-data.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import axios from 'axios'; + +const handler = async (): Promise => { + const { data } = await axios.get('https://api.example.com/data'); + + return { data }; +}; + +export default defineLogicFunction({ + universalIdentifier: '...', + name: 'fetch-data', + description: 'Fetches data from an external API', + timeoutSeconds: 10, + handler, +}); +``` + +The same works for front components: + +```tsx src/front-components/chart.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { format } from 'date-fns'; + +const DateWidget = () => { + return

Today is {format(new Date(), 'MMMM do, yyyy')}

; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### How bundling works + +The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle. + +**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed. + +**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment. + +Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server. + +## Setup + +The scaffolded app already includes Vitest. If you set it up manually, install the dependencies: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Create a `vitest.config.ts` at the root of your app: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Create a setup file that verifies the server is reachable before tests run: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## Programmatic SDK APIs + +The `twenty-sdk/cli` subpath exports functions you can call directly from test code: + +| Function | Description | +| -------------- | ------------------------------------------- | +| `appBuild` | Build the app and optionally pack a tarball | +| `appDeploy` | Upload a tarball to the server | +| `appInstall` | Install the app on the active workspace | +| `appUninstall` | Uninstall the app from the active workspace | + +Each function returns a result object with `success: boolean` and either `data` or `error`. + +## Writing an integration test + +Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Running tests + +Make sure your local Twenty server is running, then: + +```bash filename="Terminal" +yarn test +``` + +Or in watch mode during development: + +```bash filename="Terminal" +yarn test:watch +``` + +## Type checking + +You can also run type checking on your app without running tests: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +This runs `tsc --noEmit` and reports any type errors. + +## CI with GitHub Actions + +The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests. + +The workflow: + +1. Checks out your code +2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action +3. Installs dependencies with `yarn install --immutable` +4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub. + +To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow. diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/config/application.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..92393ec357 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/config/application.mdx @@ -0,0 +1,65 @@ +--- +title: Application Config +description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. +icon: rocket +--- + +Every app must have exactly one `defineApplication` call. It declares: + +* **Identity** — universal identifier, display name, description. +* **Permissions** — which role its logic functions and front components run under. +* **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. +* **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/l/ro/developers/extend/apps/logic/logic-functions). + +```ts src/application-config.ts +import { defineApplication } from 'twenty-sdk/define'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; + +export default defineApplication({ + universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', + displayName: 'My Twenty App', + description: 'My first Twenty app', + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', + description: 'Default recipient name for postcards', + value: 'Jane Doe', + isSecret: false, + }, + }, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). +* `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/l/ro/developers/extend/apps/config/roles). +* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. + +## Default function role + +The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: + +* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. +* The typed API client is restricted to the permissions granted to that role. +* Follow least-privilege: declare only the permissions your functions need. + +When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/l/ro/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/l/ro/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `author` | Author or company name | +| `category` | App category for marketplace filtering | +| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) | +| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) | +| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | +| `websiteUrl` | Link to your website | +| `termsUrl` | Link to terms of service | +| `emailSupport` | Support email address | +| `issueReportUrl` | Link to issue tracker | diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..1c7eaa0011 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/config/install-hooks.mdx @@ -0,0 +1,206 @@ +--- +title: Install Hooks +description: Run logic before or after the install — seed data, back up records, validate the upgrade. +icon: wrench +--- + +Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/l/ro/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload`, but they're declared with their own define functions — `definePostInstallLogicFunction()` and `definePreInstallLogicFunction()` — and live outside the normal trigger model (HTTP, cron, database events). + +Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build will error if more than one of either is detected. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ install flow │ +│ │ +│ upload package → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +│ │ +│ old schema visible new schema visible │ +└─────────────────────────────────────────────────────────────┘ +``` + + + + +A post-install function runs automatically once your app has finished installing on a workspace. Serverul o execută **după** ce metadatele aplicației au fost sincronizate și clientul SDK a fost generat, astfel încât spațiul de lucru este complet pregătit pentru utilizare, iar noua schemă este disponibilă. Cazuri tipice de utilizare includ popularea cu date implicite, crearea de înregistrări inițiale, configurarea setărilor spațiului de lucru sau provizionarea resurselor în cadrul serviciilor terților. + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Post install logic function executed successfully!', payload.previousVersion); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Runs after installation to set up the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + shouldRunSynchronously: false, + handler, +}); +``` + +Puteți, de asemenea, să executați manual funcția post-instalare oricând folosind CLI: + +```bash filename="Terminal" +yarn twenty exec --postInstall +``` + +Puncte cheie: +* Funcțiile de post-instalare folosesc `definePostInstallLogicFunction()` — o variantă specializată care omite setările de declanșare (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`). +* Handlerul primește un `InstallPayload` cu `{ previousVersion?: string; newVersion: string }` — `newVersion` este versiunea care este instalată, iar `previousVersion` este versiunea instalată anterior (sau `undefined` la o instalare nouă). Folosiți aceste valori pentru a distinge instalările noi de actualizări și pentru a rula logică de migrare specifică versiunii. +* **Când rulează hook-ul**: doar la instalări noi, în mod implicit. Transmiteți `shouldRunOnVersionUpgrade: true` dacă doriți să ruleze și atunci când aplicația este actualizată de la o versiune anterioară. Când este omis, indicatorul are implicit valoarea `false`, iar actualizările sar peste hook. +* **Model de execuție — implicit asincron, sincron opțional**: indicatorul `shouldRunSynchronously` controlează *modul în care* este executat post-install. + * `shouldRunSynchronously: false` *(implicit)* — hook-ul este **pus în coadă în message queue** cu `retryLimit: 3` și rulează asincron într-un worker. Răspunsul la instalare revine imediat ce jobul este pus în coadă, astfel încât un handler lent sau care eșuează nu blochează apelantul. Workerul va reîncerca de până la trei ori. **Folosiți acest mod pentru joburi de lungă durată** — popularea unor seturi mari de date, apelarea API-urilor lente ale terților, provizionarea resurselor externe, orice ar putea depăși o fereastră rezonabilă de răspuns HTTP. + * `shouldRunSynchronously: true` — hook-ul este executat **inline în timpul fluxului de instalare** (același executor ca pre-install). Cererea de instalare blochează până când handlerul se termină, iar dacă acesta aruncă o eroare, apelantul instalării primește un `POST_INSTALL_ERROR`. Fără reîncercări automate. **Folosiți acest mod pentru sarcini rapide, care trebuie să se finalizeze înainte de răspuns** — de exemplu, emiterea unei erori de validare către utilizator sau o configurare rapidă de care clientul va depinde imediat după ce apelul de instalare revine. Reține că migrarea metadatelor a fost deja aplicată până când rulează post-install, astfel încât un eșec în modul sincron **nu** anulează modificările de schemă — doar expune eroarea. +* Asigurați-vă că handlerul dvs. este idempotent. În modul asincron, coada poate reîncerca de până la trei ori; în oricare mod, hook-ul poate rula din nou la actualizări când `shouldRunOnVersionUpgrade: true`. +* Variabilele de mediu `APPLICATION_ID`, `APP_ACCESS_TOKEN` și `API_URL` sunt disponibile în interiorul handlerului (la fel ca în orice altă funcție logică), astfel încât puteți apela API-ul Twenty cu un token de acces al aplicației limitat la aplicația dvs. +* Este permisă o singură funcție de post-instalare per aplicație. Construirea manifestului va genera o eroare dacă este detectată mai mult de una. +* The function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in [`defineApplication()`](/l/ro/developers/extend/apps/config/application). +* 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. +* **Nu se execută în modul dev**: când o aplicație este înregistrată local (prin `yarn twenty dev`), serverul sare complet peste fluxul de instalare și sincronizează fișierele direct prin watcher-ul CLI — astfel încât post-install nu rulează niciodată în modul dev, indiferent de `shouldRunSynchronously`. Folosiți `yarn twenty exec --postInstall` pentru a-l declanșa manual într-un workspace care rulează. + + + + +A pre-install function runs automatically during installation, **before the workspace metadata migration is applied**. Are aceeași structură a payload-ului ca post-install (`InstallPayload`), dar este plasată mai devreme în fluxul de instalare, astfel încât poate pregăti starea de care depinde migrarea iminentă — utilizări tipice includ realizarea unui backup al datelor, validarea compatibilității cu noua schemă sau arhivarea înregistrărilor care urmează să fie restructurate sau eliminate. + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Pre install logic function executed successfully!', payload.previousVersion); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Runs before installation to prepare the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +Puteți, de asemenea, să executați manual funcția de pre-instalare oricând folosind CLI: + +```bash filename="Terminal" +yarn twenty exec --preInstall +``` + +Puncte cheie: +* Funcțiile de pre-instalare folosesc `definePreInstallLogicFunction()` — aceeași configurare specializată ca pentru post-install, doar că atașată la un alt punct din ciclul de viață. +* Atât handlerele de pre-install, cât și cele de post-install primesc același tip `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importați-l o singură dată și reutilizați-l pentru ambele hook-uri. +* **Când rulează hook-ul**: poziționat chiar înainte de migrarea metadatelor workspace-ului (`synchronizeFromManifest`). Înainte de execuție, serverul rulează un "sync redus", pur aditiv, care înregistrează funcția de pre-instalare a versiunii **noi** în metadatele workspace-ului — nimic altceva nu este atins — și apoi o execută. Deoarece acest sync este doar aditiv, obiectele, câmpurile și datele versiunii precedente sunt încă intacte când rulează handlerul dvs.: puteți citi și face backup în siguranță stării pre-migrare. +* **Model de execuție**: pre-install este executat **sincron** și **blochează instalarea**. Dacă handlerul aruncă o eroare, instalarea este întreruptă înainte ca orice modificări de schemă să fie aplicate — workspace-ul rămâne la versiunea anterioară într-o stare consistentă. Acest lucru este intenționat: pre-install este ultima dvs. șansă de a refuza o actualizare riscantă. +* La fel ca la post-install, este permisă o singură funcție de pre-instalare per aplicație. Este atașată automat la manifestul aplicației sub `preInstallLogicFunction` în timpul build-ului. +* **Nu se execută în modul dev**: la fel ca post-install — fluxul de instalare este sărit complet pentru aplicațiile înregistrate local, astfel încât pre-install nu rulează niciodată sub `yarn twenty dev`. Folosiți `yarn twenty exec --preInstall` pentru a-l declanșa manual. + + + + +Ambele hook-uri fac parte din același flux de instalare și primesc același `InstallPayload`. Diferența constă în **momentul** în care rulează în raport cu migrarea metadatelor workspace-ului, iar asta schimbă ce date pot atinge în siguranță. + +Pre-install este întotdeauna **sincron** (blochează instalarea și o poate întrerupe). Post-install este **implicit asincron** — pus în coadă pe un worker cu reîncercări automate — dar poate opta pentru execuție sincronă cu `shouldRunSynchronously: true`. Consultați acordeonul `definePostInstallLogicFunction` de mai sus pentru când să folosiți fiecare mod. + +**Folosiți `post-install` pentru orice are nevoie ca noua schemă să existe.** Acesta este cazul obișnuit: + +* Popularea datelor implicite (crearea înregistrărilor inițiale, a vizualizărilor implicite, a conținutului demo) pentru obiectele și câmpurile adăugate recent. +* Înregistrarea webhook-urilor la servicii terțe, acum că aplicația are acreditările sale. +* Apelarea propriului tău API pentru a finaliza configurarea care depinde de metadatele sincronizate. +* Logică idempotentă de tipul "asigurați-vă că acest lucru există" care ar trebui să reconcilieze starea la fiecare actualizare — combină cu `shouldRunOnVersionUpgrade: true`. + +Exemplu — populează o înregistrare `PostCard` implicită după instalare: + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion }: InstallPayload): Promise => { + if (previousVersion) return; // fresh installs only + + const client = createClient(); + await client.postCard.create({ + data: { title: 'Welcome to Postcard', content: 'Your first card!' }, + }); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Seeds a welcome post card after install.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + handler, +}); +``` + +**Folosiți `pre-install` atunci când o migrare altfel ar distruge sau ar corupe datele existente.** Deoarece pre-install rulează pe schema *anterioară* și eșecul său anulează actualizarea, acesta este locul potrivit pentru orice este riscant: + +* **Crearea unui backup al datelor care urmează să fie eliminate sau restructurate** — de exemplu, elimini un câmp în v2 și trebuie să-i copiezi valorile într-un alt câmp sau să le exporți în stocare înainte de rularea migrării. +* **Arhivarea înregistrărilor pe care o nouă constrângere le-ar invalida** — de exemplu, un câmp devine `NOT NULL` și trebuie mai întâi să ștergi sau să corectezi rândurile cu valori nule. +* **Validarea compatibilității și refuzarea actualizării dacă datele curente nu pot fi migrate fără probleme** — aruncă din handler și instalarea se oprește fără ca modificări să fie aplicate. Aceasta este mai sigur decât să descoperi incompatibilitatea în mijlocul migrării. +* **Redenumirea sau schimbarea cheilor datelor** înaintea unei modificări de schemă care ar pierde asocierile. + +Exemplu — arhivează înregistrări înainte de o migrare distructivă: + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise => { + // Only the 1.x → 2.x upgrade drops the legacy `notes` field. + if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) { + return; + } + + const client = createClient(); + const legacyRecords = await client.postCard.findMany({ + where: { notes: { isNotNull: true } }, + }); + + if (legacyRecords.length === 0) return; + + // Copy legacy `notes` into the new `description` field before the migration + // drops the `notes` column. If this fails, the upgrade is aborted and the + // workspace stays on v1 with all data intact. + await Promise.all( + legacyRecords.map((record) => + client.postCard.update({ + where: { id: record.id }, + data: { description: record.notes }, + }), + ), + ); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Backs up legacy notes into description before the v2 migration.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +**Regulă practică:** + +| Vrei să... | Folosiți | +| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | +| Populați date implicite, configurați workspace-ul, înregistrați resurse externe | `post-install` | +| Rulați populări de durată sau apeluri către terți care nu ar trebui să blocheze răspunsul la instalare | `post-install` (implicit — `shouldRunSynchronously: false`, cu reîncercări ale workerului) | +| Rulați o configurare rapidă de care apelantul va depinde imediat după ce apelul de instalare revine | `post-install` cu `shouldRunSynchronously: true` | +| Citești sau faci backup datelor pe care migrarea iminentă le-ar pierde | `pre-install` | +| Respingeți o actualizare care ar corupe datele existente | `pre-install` (aruncă din handler) | +| Rulați o reconciliere la fiecare actualizare | `post-install` cu `shouldRunOnVersionUpgrade: true` | +| Faceți o configurare unică doar la prima instalare | `post-install` cu `shouldRunOnVersionUpgrade: false` (implicit) | + + +Dacă aveți dubii, alegeți implicit **post-install**. Apelați la pre-install doar când migrarea în sine este distructivă și trebuie să interceptați starea anterioară înainte să dispară. + + + + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/config/overview.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/config/overview.mdx new file mode 100644 index 0000000000..8ca444b593 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/config/overview.mdx @@ -0,0 +1,51 @@ +--- +title: Overview +description: Configure the app itself — its identity, default permissions, and what runs at install time. +icon: screwdriver-wrench +--- + +A Twenty app's **config layer** is what describes the app *to the platform* — its identity, the permissions it holds, and the code that runs during install or upgrade. These declarations don't add new data shapes or runtime behavior; they tell Twenty *who the app is* and *how to set it up*. + +```text +┌────────────────────────────────────────────────────────┐ +│ Application — identity, default role, variables, │ +│ marketplace metadata │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Role — what the app's logic functions can read │ │ +│ │ and write (referenced by Application) │ │ +│ └──────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────┘ + │ + ▼ (at install / upgrade time) + ┌──────────────────────────────────┐ + │ Pre-install hook │ before metadata migration + └──────────────────────────────────┘ + ┌──────────────────────────────────┐ + │ Post-install hook │ after metadata migration + └──────────────────────────────────┘ +``` + +## In this section + + + + `defineApplication` — identity, default role, variables, marketplace metadata. + + + `defineRole` — declare what your app's logic functions can read and write. + + + `definePreInstallLogicFunction` and `definePostInstallLogicFunction` — back up data, seed defaults, validate upgrades. + + + +## How the pieces relate + +* **Application** is the entry point. Every app has exactly one `defineApplication()` call, and it points at one **Role** as its default. +* The **Role** controls what the app's logic functions and front components can read and write. Follow least-privilege: only grant the permissions your code actually needs. +* **Install Hooks** run during install or upgrade — pre-install before the metadata migration (so it can refuse a risky upgrade), post-install after the migration (so it can seed default data against the new schema). + + +Install hooks share the [logic function](/l/ro/developers/extend/apps/logic/logic-functions) runtime — same handler signature, same environment variables, same typed API client — but they're declared with their own define functions and live outside the regular trigger model (HTTP, cron, database events). + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/config/public-assets.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/config/public-assets.mdx new file mode 100644 index 0000000000..00d5645751 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/config/public-assets.mdx @@ -0,0 +1,59 @@ +--- +title: Public Assets +description: Ship static files — images, icons, fonts — alongside your app via the public/ folder. +icon: folder-open +--- + +Folderul `public/` din rădăcina aplicației conține fișiere statice — imagini, pictograme, fonturi sau orice alte resurse de care are nevoie aplicația la rulare. Aceste fișiere sunt incluse automat în build-uri, sincronizate în timpul modului de dezvoltare și încărcate pe server. + +Fișierele plasate în `public/` sunt: + +* **Accesibile public** — odată sincronizate pe server, resursele sunt servite la un URL public. Nu este necesară autentificarea pentru a le accesa. +* **Disponibile în componentele frontend** — folosiți URL-urile resurselor pentru a afișa imagini, pictograme sau orice media în componentele React. +* **Disponibile în funcțiile logice** — referiți URL-urile resurselor în e-mailuri, răspunsuri API sau orice logică pe server. +* **Utilizate pentru metadatele marketplace-ului** — câmpurile `logoUrl` și `screenshots` din `defineApplication()` fac referire la fișiere din acest folder (de ex., `public/logo.png`). Acestea sunt afișate în marketplace când aplicația este publicată. +* **Sincronizate automat în modul de dezvoltare** — când adăugați, actualizați sau ștergeți un fișier în `public/`, acesta este sincronizat automat cu serverul. Nu este nevoie de repornire. +* **Incluse în build-uri** — `yarn twenty build` împachetează toate resursele publice în outputul de distribuție. + +## Accesarea resurselor publice cu `getPublicAssetUrl` + +Utilizați helperul `getPublicAssetUrl` din `twenty-sdk` pentru a obține URL-ul complet al unui fișier din directorul `public/`. Funcționează atât în funcții logice, cât și în componente frontend. + +**Într-o funcție logică:** + +```ts src/logic-functions/send-invoice.ts +import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define'; + +const handler = async (): Promise => { + const logoUrl = getPublicAssetUrl('logo.png'); + const invoiceUrl = getPublicAssetUrl('templates/invoice.png'); + + // Fetch the file content (no auth required — public endpoint) + const response = await fetch(invoiceUrl); + const buffer = await response.arrayBuffer(); + + return { logoUrl, size: buffer.byteLength }; +}; + +export default defineLogicFunction({ + universalIdentifier: 'a1b2c3d4-...', + name: 'send-invoice', + description: 'Sends an invoice with the app logo', + timeoutSeconds: 10, + handler, +}); +``` + +**Într-o componentă frontend:** + +```tsx src/front-components/company-card.tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +export default defineFrontComponent(() => { + const logoUrl = getPublicAssetUrl('logo.png'); + + return App logo; +}); +``` + +Argumentul `path` este relativ la folderul `public/` al aplicației. Atât `getPublicAssetUrl('logo.png')`, cât și `getPublicAssetUrl('public/logo.png')` se rezolvă la același URL — prefixul `public/` este eliminat automat dacă este prezent. diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/config/roles.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/config/roles.mdx new file mode 100644 index 0000000000..9130e2a6ee --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/config/roles.mdx @@ -0,0 +1,90 @@ +--- +title: Roles & Permissions +description: Declare what objects and fields your app's logic functions and front components can read and write. +icon: shield-halved +--- + +A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role declared as `defaultRoleUniversalIdentifier` in [`defineApplication`](/l/ro/developers/extend/apps/config/application). + +```ts src/roles/restricted-company-role.ts +import { + defineRole, + PermissionFlag, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; + +export default defineRole({ + universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6', + label: 'My new role', + description: 'A role that can be used in your workspace', + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + fieldPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + fieldUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier, + canReadFieldValue: false, + canUpdateFieldValue: false, + }, + ], + permissionFlags: [PermissionFlag.APPLICATIONS], +}); +``` + +## The default function role + +When you scaffold a new app, the CLI creates a default role file: + +```ts src/roles/default-role.ts +import { defineRole, PermissionFlag } from 'twenty-sdk/define'; + +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = + 'b648f87b-1d26-4961-b974-0908fd991061'; + +export default defineRole({ + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + label: 'Default function role', + description: 'Default role for function Twenty client', + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [], + fieldPermissions: [], + permissionFlags: [], +}); +``` + +This role's `universalIdentifier` is referenced from `application-config.ts` as `defaultRoleUniversalIdentifier`: + +* **`*.role.ts`** declares what the role can do. +* **`application-config.ts`** points to that role so your functions inherit its permissions. + +## Best practices + +* Start from the scaffolded role, then progressively restrict it — the default grants broad read access, which is rarely what you want in production. +* Replace `objectPermissions` and `fieldPermissions` with the exact objects and fields your functions actually need. +* `permissionFlags` control access to platform-level capabilities. Keep them minimal. +* See a working example: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts). diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/data/extending-objects.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/data/extending-objects.mdx new file mode 100644 index 0000000000..5c2a318482 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/data/extending-objects.mdx @@ -0,0 +1,48 @@ +--- +title: Extending Objects +description: Add fields to standard Twenty objects (Person, Company, …) or to objects from other apps using defineField. +icon: wand-magic-sparkles +--- + +Use `defineField()` to add a field to an object you don't own — a standard Twenty object like Person or Company, or an object shipped by another installed app. Unlike inline fields declared inside [`defineObject`](/l/ro/developers/extend/apps/data/objects), standalone fields require an `objectUniversalIdentifier` to specify which object they extend. + +```ts src/fields/company-loyalty-tier.field.ts +import { defineField, FieldType } from 'twenty-sdk/define'; + +export default defineField({ + universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890', + objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object + name: 'loyaltyTier', + type: FieldType.SELECT, + label: 'Loyalty Tier', + icon: 'IconStar', + options: [ + { value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' }, + { value: 'SILVER', label: 'Silver', position: 1, color: 'gray' }, + { value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' }, + ], +}); +``` + +## Key points + +* `objectUniversalIdentifier` identifies the target object. For standard Twenty objects, import the constant from `twenty-sdk`: + + ```ts + import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define'; + + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier + // … + ``` + +* When defining fields **inline inside `defineObject()`**, you do **not** need `objectUniversalIdentifier` — it's inherited from the parent object. + +* `defineField()` is the only way to add fields to objects you didn't create with `defineObject()`. + +* File location is up to you. The convention is `src/fields/\.field.ts`, but the SDK detects fields anywhere in `src/`. + +## Adding a relation to an existing object + +To add a relation field (e.g. linking your custom object to a standard `Person`), use `defineField()` with `FieldType.RELATION`. The pattern is the same as for inline relations but with `objectUniversalIdentifier` set explicitly. See [Relations](/l/ro/developers/extend/apps/data/relations) for the bidirectional pattern. diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/data/objects.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/data/objects.mdx new file mode 100644 index 0000000000..512ee7506c --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/data/objects.mdx @@ -0,0 +1,93 @@ +--- +title: Obiecte +description: Declare new record types — custom tables with their own fields — using defineObject. +icon: tabel +--- + +Custom **objects** are new record types your app adds to a workspace — Post Card, Invoice, Subscription, anything specific to your domain. Each object declares its schema (fields, relations, default values) and a stable universal identifier that survives across syncs and deploys. + +```ts src/objects/post-card.object.ts +import { defineObject, FieldType } from 'twenty-sdk/define'; + +enum PostCardStatus { + DRAFT = 'DRAFT', + SENT = 'SENT', + DELIVERED = 'DELIVERED', + RETURNED = 'RETURNED', +} + +export default defineObject({ + universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', + nameSingular: 'postCard', + namePlural: 'postCards', + labelSingular: 'Post Card', + labelPlural: 'Post Cards', + description: 'A post card object', + icon: 'IconMail', + fields: [ + { + universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b', + name: 'content', + type: FieldType.TEXT, + label: 'Content', + description: "Postcard's content", + icon: 'IconAbc', + }, + { + universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac', + name: 'recipientName', + type: FieldType.FULL_NAME, + label: 'Recipient name', + icon: 'IconUser', + }, + { + universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266', + name: 'recipientAddress', + type: FieldType.ADDRESS, + label: 'Recipient address', + icon: 'IconHome', + }, + { + universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e', + name: 'status', + type: FieldType.SELECT, + label: 'Status', + icon: 'IconSend', + defaultValue: `'${PostCardStatus.DRAFT}'`, + options: [ + { value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' }, + { value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' }, + { value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' }, + { value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' }, + ], + }, + { + universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433', + name: 'deliveredAt', + type: FieldType.DATE_TIME, + label: 'Delivered at', + icon: 'IconCheck', + isNullable: true, + defaultValue: null, + }, + ], +}); +``` + +## 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. +* Inline fields defined here do **not** need an `objectUniversalIdentifier` — it's inherited from the parent object. Use [`defineField()`](/l/ro/developers/extend/apps/data/extending-objects) to add fields to objects you don't own. +* You can scaffold new objects with `yarn twenty add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/l/ro/developers/extend/apps/getting-started/scaffolding). + + +**Base fields are added automatically.** When you define a custom object, Twenty creates standard fields like `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and `deletedAt` for you. You don't need to declare them in your `fields` array — only your custom fields. You can override a default field by declaring one with the same name, but this is rarely a good idea. + + +## Ce urmează + +* **Connect this object to others** — see [Relations](/l/ro/developers/extend/apps/data/relations) for the bidirectional relation pattern. +* **Add fields to objects from other apps** — see [Extending Objects](/l/ro/developers/extend/apps/data/extending-objects) for `defineField()`. +* **Display this object in the UI** — see [Views](/l/ro/developers/extend/apps/layout/views) and [Navigation Menu Items](/l/ro/developers/extend/apps/layout/navigation-menu-items) to put it in the sidebar. diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/data/overview.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/data/overview.mdx new file mode 100644 index 0000000000..0aea8f784e --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/data/overview.mdx @@ -0,0 +1,52 @@ +--- +title: Overview +description: Shape the data your app adds to a workspace — objects, fields, and relations. +icon: database +--- + +A Twenty app's **data layer** is the data your app *adds* to a workspace — the new record types it declares, the columns it adds to existing objects, and how those records connect to each other. + +```text +┌──────────────────────────────────────────────────┐ +│ Object — a record type, e.g. PostCard │ +│ ├─ Field (name, type, label) │ +│ ├─ Field │ +│ └─ Relation (link to another object) │ +└──────────────────────────────────────────────────┘ + │ + ├── lives in your app, OR + │ + ▼ +┌──────────────────────────────────────────────────┐ +│ Standard / other apps' objects │ +│ └─ Field added by your app via defineField │ +└──────────────────────────────────────────────────┘ +``` + +## In this section + + + + `defineObject` — declare new record types with their own fields. + + + `defineField` — add fields to standard or other apps' objects. + + + Bidirectional `MANY_TO_ONE` / `ONE_TO_MANY` connections between objects. + + + +## Entities at a glance + +| Entity | Purpose | Defined with | +| ------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| **Object** | A new custom record type (e.g. PostCard, Invoice) with its own fields | `defineObject()` | +| **Field** | A column on an object. Standalone fields can extend objects you didn't create (e.g. add `loyaltyTier` to Company) | `defineField()` | +| **Relation** | A bidirectional link between two objects — both sides declared as fields | `defineField()` with `FieldType.RELATION` | + +The SDK detects these via AST analysis at build time, so file organization is up to you — the convention is `src/objects/` and `src/fields/`. Stable `universalIdentifier` UUIDs tie everything together across deploys. + + +Looking for **Application Config** or **Roles & Permissions**? Those describe the app itself rather than the data it adds — they live under [Config](/l/ro/developers/extend/apps/config/overview). Looking for **Connections** (Linear, GitHub, Slack OAuth)? Those exist to be called *from* logic functions and live under [Logic](/l/ro/developers/extend/apps/logic/connections). + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/data/relations.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/data/relations.mdx new file mode 100644 index 0000000000..edd3648c8e --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/data/relations.mdx @@ -0,0 +1,160 @@ +--- +title: Relații +description: Connect objects together with bidirectional MANY_TO_ONE / ONE_TO_MANY relations. +icon: diagram-project +--- + +Relations connect two objects together. In Twenty, relations are always **bidirectional** — every relation has two sides, and each side is declared as a field that references the other. + +| Tip relație | Descriere | Are cheie străină? | +| ------------- | ---------------------------------------------------------------------------------- | --------------------- | +| `MANY_TO_ONE` | Multe înregistrări ale acestui obiect indică către o singură înregistrare a țintei | Da (`joinColumnName`) | +| `ONE_TO_MANY` | O înregistrare a acestui obiect are multe înregistrări ale țintei | No (the inverse side) | + +## Cum funcționează relațiile + +Fiecare relație necesită **două câmpuri** care se referențiază reciproc: + +1. Partea **MANY_TO_ONE** — se află pe obiectul care deține cheia străină. +2. Partea **ONE_TO_MANY** — se află pe obiectul care deține colecția. + +Ambele câmpuri folosesc `FieldType.RELATION` și se referențiază încrucișat prin `relationTargetFieldMetadataUniversalIdentifier`. + +## Exemplu: Post Card are mulți destinatari + +A `PostCard` can be sent to many `PostCardRecipient` records. Fiecare destinatar aparține exact unui Post Card. + +**Pasul 1: Definiți partea ONE_TO_MANY pe PostCard** (partea "one"): + +```ts src/fields/post-card-recipients-on-post-card.field.ts +import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111'; +// Import from the other side +import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field'; + +export default defineField({ + universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCardRecipients', + label: 'Post Card Recipients', + icon: 'IconUsers', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, +}); +``` + +**Pasul 2: Definiți partea MANY_TO_ONE pe PostCardRecipient** (partea "many" — deține cheia străină): + +```ts src/fields/post-card-on-post-card-recipient.field.ts +import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222'; +// Import from the other side +import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field'; + +export default defineField({ + universalIdentifier: POST_CARD_FIELD_ID, + objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + icon: 'IconMail', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, +}); +``` + + +**Circular imports:** both relation fields reference each other's `universalIdentifier`. To avoid circular import issues, export your field IDs as named constants from each file and import them in the other. Sistemul de build le rezolvă în timpul compilării. + + +## Relaționarea cu obiectele standard + +Pentru a crea o relație cu un obiect Twenty încorporat (Person, Company etc.), utilizați `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`: + +```ts src/fields/person-on-self-hosting-user.field.ts +import { + defineField, + FieldType, + RelationType, + OnDeleteAction, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; +import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object'; + +export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333'; +export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444'; + +export default defineField({ + universalIdentifier: PERSON_FIELD_ID, + objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'person', + label: 'Person', + description: 'Person matching with the self hosting user', + isNullable: true, + relationTargetObjectMetadataUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, + relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.SET_NULL, + joinColumnName: 'personId', + }, +}); +``` + +## Proprietăți ale câmpului de relație + +| Proprietate | Obligatoriu | Descriere | +| ------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------- | +| `tip` | Da | Trebuie să fie `FieldType.RELATION` | +| `relationTargetObjectMetadataUniversalIdentifier` | Da | `universalIdentifier` al obiectului țintă | +| `relationTargetFieldMetadataUniversalIdentifier` | Da | `universalIdentifier` al câmpului corespunzător de pe obiectul țintă | +| `universalSettings.relationType` | Da | `RelationType.MANY_TO_ONE` sau `RelationType.ONE_TO_MANY` | +| `universalSettings.onDelete` | Doar MANY_TO_ONE | Ce se întâmplă atunci când înregistrarea referențiată este ștearsă: `CASCADE`, `SET_NULL`, `RESTRICT` sau `NO_ACTION` | +| `universalSettings.joinColumnName` | Doar MANY_TO_ONE | Numele coloanei din baza de date pentru cheia străină (de ex., `postCardId`) | + +## Inline relation fields + +You can also declare a relation directly inside [`defineObject`](/l/ro/developers/extend/apps/data/objects). When inline, omit `objectUniversalIdentifier` — it's inherited from the parent object: + +```ts +export default defineObject({ + universalIdentifier: '...', + nameSingular: 'postCardRecipient', + // ... + fields: [ + { + universalIdentifier: POST_CARD_FIELD_ID, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, + }, + // … other fields + ], +}); +``` diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/concepts.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/concepts.mdx new file mode 100644 index 0000000000..8227d50f38 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/concepts.mdx @@ -0,0 +1,101 @@ +--- +title: Concepts +description: How Twenty apps work — entity model, sandboxing, and the install lifecycle. +icon: sitemap +--- + +Aplicațiile Twenty sunt pachete TypeScript care vă extind spațiul de lucru cu obiecte personalizate, logică, componente UI și capabilități AI. Acestea rulează pe platforma Twenty, cu izolare completă (sandboxing) și controale de permisiuni. + +## Cum funcționează aplicațiile + +O aplicație este o colecție de **entități** declarate folosind funcțiile `defineEntity()` din pachetul `twenty-sdk`. SDK-ul detectează aceste declarații prin analiză AST în timpul construirii și produce un **manifest** — o descriere completă a ceea ce aplicația dvs. adaugă unui spațiu de lucru. Aceste funcții validează configurația în timpul build-ului și oferă completare automată în IDE și siguranța tipurilor. + +``` +your-app/ +├── src/ +│ ├── application-config.ts ← defineApplication (required, one per app) +│ ├── roles/ ← defineRole +│ ├── objects/ ← defineObject +│ ├── fields/ ← defineField +│ ├── logic-functions/ ← defineLogicFunction +│ ├── front-components/ ← defineFrontComponent +│ ├── skills/ ← defineSkill +│ ├── agents/ ← defineAgent +│ ├── views/ ← defineView +│ ├── navigation-menu-items/ ← defineNavigationMenuItem +│ └── page-layouts/ ← definePageLayout +├── public/ ← Static assets (images, icons) +└── package.json +``` + + + **Organizarea fișierelor ține de dvs.** Detectarea entităților este bazată pe AST — SDK-ul găsește apelurile `export default defineEntity(...)` indiferent unde se află fișierul. Structura de foldere de mai sus este o convenție, nu o cerință. + + +## Tipuri de entități + +| Entitate | Scop | Documentație | +| -------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------- | +| **Aplicație** | App identity, default role, variables | [Application Config](/l/ro/developers/extend/apps/config/application) | +| **Rol** | Permission sets on objects and fields | [Roles & Permissions](/l/ro/developers/extend/apps/config/roles) | +| **Obiect** | Custom record types with fields | [Objects](/l/ro/developers/extend/apps/data/objects) | +| **Câmp** | Add fields to objects from other apps | [Extending Objects](/l/ro/developers/extend/apps/data/extending-objects) | +| **Relation** | Bidirectional links between objects | [Relations](/l/ro/developers/extend/apps/data/relations) | +| **Funcție logică** | TypeScript pe partea de server cu declanșatoare | [Funcții logice](/l/ro/developers/extend/apps/logic/logic-functions) | +| **Abilitate** | Instrucțiuni reutilizabile pentru agenți AI | [Abilități și agenți](/l/ro/developers/extend/apps/logic/skills-and-agents) | +| **Agent** | Agenți AI cu prompturi personalizate | [Abilități și agenți](/l/ro/developers/extend/apps/logic/skills-and-agents) | +| **Connection Provider** | OAuth credentials for third-party APIs | [Connections](/l/ro/developers/extend/apps/logic/connections) | +| **Vizualizare** | Vizualizări preconfigurate ale listelor de înregistrări | [Views](/l/ro/developers/extend/apps/layout/views) | +| **Element de meniu de navigare** | Intrări personalizate în bara laterală | [Navigation Menu Items](/l/ro/developers/extend/apps/layout/navigation-menu-items) | +| **Layout pagină** | Tabs and widgets on a record's detail page | [Page Layouts](/l/ro/developers/extend/apps/layout/page-layouts) | +| **Componentă front-end** | Sandboxed React UI inside Twenty | [Componente front-end](/l/ro/developers/extend/apps/layout/front-components) | +| **Command Menu Item** | Quick actions and Cmd+K entries | [Command Menu Items](/l/ro/developers/extend/apps/layout/command-menu-items) | + +## Izolare (sandboxing) + +* **Funcțiile logice** rulează în procese Node.js izolate pe server. Acestea accesează datele doar prin clientul API tipizat, limitat de permisiunile rolului aplicației. +* **Componentele front-end** rulează în Web Workers folosind Remote DOM — izolate de pagina principală, dar randând elemente DOM native (nu iframes). Acestea comunică cu Twenty printr-un API al gazdei bazat pe transmiterea de mesaje. +* **Permisiunile** sunt aplicate la nivelul API-ului. Tokenul de rulare (`TWENTY_APP_ACCESS_TOKEN`) este derivat din rolul definit în `defineApplication()`. + +## Ciclul de viață al aplicației + +``` +┌─────────────────────────────────────────────────────────┐ +│ Development │ +│ npx create-twenty-app → yarn twenty dev (live sync) │ +├─────────────────────────────────────────────────────────┤ +│ Build & Deploy │ +│ yarn twenty build → yarn twenty deploy │ +├─────────────────────────────────────────────────────────┤ +│ Install flow │ +│ upload → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +├─────────────────────────────────────────────────────────┤ +│ Publish │ +│ npm publish → appears in Twenty marketplace │ +└─────────────────────────────────────────────────────────┘ +``` + +* **`yarn twenty dev`** — monitorizează fișierele sursă și sincronizează în timp real modificările către un server Twenty conectat. Clientul API tipizat este regenerat automat atunci când schema se schimbă. +* **`yarn twenty build`** — compilează TypeScript, împachetează funcțiile logice și componentele front-end cu esbuild și produce un manifest. +* **Pre/post-install hooks** — optional functions that run during installation. See [Install Hooks](/l/ro/developers/extend/apps/config/install-hooks) for details. + +## Pașii următori + + + + Application identity, default role, and install hooks. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/local-server.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/local-server.mdx new file mode 100644 index 0000000000..62102fef1f --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/local-server.mdx @@ -0,0 +1,72 @@ +--- +title: Local Server +description: Manage the local Twenty Docker server — start, stop, upgrade, parallel test instance, and manual SDK setup. +icon: server +--- + +## Gestionarea serverului local + +Folosiți `yarn twenty server` pentru a controla containerul Twenty local: + +| Comandă | Ce face | +| -------------------------------------- | ------------------------------------------------------------ | +| `yarn twenty server start` | Pornește serverul (descarcă imaginea dacă este necesar) | +| `yarn twenty server start --port 3030` | Pornește pe un port personalizat | +| `yarn twenty server stop` | Oprește serverul (păstrează datele) | +| `yarn twenty server status` | Afișează URL-ul, versiunea și credențialele de autentificare | +| `yarn twenty server logs` | Transmite în flux jurnalele serverului | +| `yarn twenty server reset` | Șterge datele și pornește de la zero | +| `yarn twenty server upgrade` | Descarcă cea mai recentă imagine `twenty-app-dev` | +| `yarn twenty server upgrade 2.2.0` | Actualizează la o versiune specifică | + +Datele persistă între reporniri în două volume Docker (`twenty-app-dev-data` pentru PostgreSQL, `twenty-app-dev-storage` pentru fișiere). Folosiți `reset` pentru a șterge totul. + +## Actualizarea imaginii serverului + +`yarn twenty server upgrade` descarcă cea mai recentă imagine, compară digest-urile și recreează containerul doar dacă s-a schimbat ceva. Volumele de date sunt păstrate — doar containerul este înlocuit. Dacă a fost descărcată o imagine nouă și containerul rula, actualizarea pornește automat un container nou; rulați apoi `yarn twenty server start` pentru a aștepta până când devine funcțional. + +```bash filename="Terminal" +yarn twenty server upgrade # Latest +yarn twenty server upgrade 2.2.0 # Specific version +``` + +Puteți verifica versiunea care rulează cu `yarn twenty server status` (aceasta afișează `APP_VERSION` încorporat în container). + +## Rularea unei instanțe de test în paralel + +Adăugați `--test` la orice comandă `server` pentru a gestiona o a doua instanță, complet izolată — utilă pentru teste de integrare sau pentru a experimenta fără a atinge datele principale de dezvoltare: + +| Comandă | Ce face | +| ----------------------------------- | --------------------------------------------------- | +| `yarn twenty server start --test` | Pornește instanța de test (implicit pe portul 2021) | +| `yarn twenty server stop --test` | Opriți-o | +| `yarn twenty server status --test` | Afișați-i starea | +| `yarn twenty server logs --test` | Transmiteți în flux jurnalele sale | +| `yarn twenty server reset --test` | Ștergeți-i datele | +| `yarn twenty server upgrade --test` | Actualizați-i imaginea | + +Instanța de test are propriul container (`twenty-app-dev-test`), propriile volume (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) și propria configurație — rulează alături de instanța principală, fără conflicte. Combinați `--test` cu `--port` pentru a înlocui portul 2021. + +## Configurare manuală (fără generator) + +Săriți peste generatorul de schelet dacă adăugați SDK-ul într-un proiect existent: + +```bash filename="Terminal" +yarn add twenty-sdk twenty-client-sdk +``` + +Adăugați scriptul în `package.json`: + +```json filename="package.json" +{ + "scripts": { + "twenty": "twenty" + } +} +``` + +Acum puteți rula `yarn twenty dev`, `yarn twenty server start` și restul. + + +Nu instalați `twenty-sdk` global — fixați-l per proiect astfel încât fiecare aplicație să folosească propria versiune. + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/project-structure.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/project-structure.mdx new file mode 100644 index 0000000000..2afc7d8835 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/project-structure.mdx @@ -0,0 +1,40 @@ +--- +title: Project Structure +description: What's inside a scaffolded Twenty app — files, folders, and what each one does. +icon: folder-tree +--- + +A new app generated by `npx create-twenty-app` looks like this: + +```text filename="my-twenty-app/" +my-twenty-app/ + package.json + src/ + application-config.ts # Required — your app's entry point + default-role.ts # Permissions for logic functions + constants/ + universal-identifiers.ts # Auto-generated UUIDs and metadata + __tests__/ + setup-test.ts + app-install.integration-test.ts + .github/workflows/ci.yml # GitHub Actions + public/ # Static assets + vitest.config.ts # Test runner config + tsconfig.json, tsconfig.spec.json + .nvmrc, .yarnrc.yml, .oxlintrc.json + README.md, LLMS.md +``` + +## Fișiere cheie + +| Fișier / Folder | Scop | +| ---------------------------------------- | -------------------------------------------------------------------- | +| `src/application-config.ts` | **Necesar.** Fișierul principal de configurare pentru aplicație. | +| `src/default-role.ts` | Rol implicit care controlează la ce pot avea acces funcțiile logice. | +| `src/constants/universal-identifiers.ts` | UUID-uri generate automat și metadate (nume afișat, descriere). | +| `src/__tests__/` | Teste de integrare (configurare + test exemplu). | +| `public/` | Resurse statice (imagini, fonturi) servite împreună cu aplicația. | + + +**File organization is up to you.** The folders above are conventions — the SDK detects entities via AST analysis on `export default defineEntity(...)` calls regardless of where the file lives. + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/quick-start.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/quick-start.mdx new file mode 100644 index 0000000000..5b4250bf55 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/quick-start.mdx @@ -0,0 +1,184 @@ +--- +title: Pornire rapidă +icon: rocket +description: Creați prima dvs. aplicație Twenty în câteva minute. +--- + +## Cerințe + +* **Node.js 24+** — [Descărcați](https://nodejs.org/) +* **Yarn 4** — vine împreună cu Node prin Corepack. Activați-l: `corepack enable` +* **Docker** — [Descărcați](https://www.docker.com/products/docker-desktop/). Necesar pentru a rula un server Twenty local. Omiteți dacă rulați deja Twenty în altă parte. + +Crearea unei aplicații Twenty are trei faze. Generatorul le reunește într-o singură comandă pe calea optimă, dar fiecare fază este un concept separat — când ceva eșuează, dacă știți în ce fază sunteți, știți ce trebuie să corectați. + +| Fază | Ce faceți | Instrument | Rezultat | +| ----------------------- | ------------------------------------------------ | ----------------------------- | ------------------------------ | +| **1. Creați scheletul** | Generați codul sursă al aplicației | `npx create-twenty-app` | Un proiect TypeScript pe disc | +| **2. Rulați un server** | Porniți un server Twenty cu care să sincronizați | Docker + `yarn twenty server` | O instanță Twenty care rulează | +| **3. Sincronizați** | Sincronizați în timp real codul cu serverul | `yarn twenty dev` | Modificările apar în UI | + +--- + +## Faza 1 — Creați scheletul proiectului + +Creați o nouă aplicație din șablon: + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app +``` + +Vi se va cere un nume și o descriere — apăsați **Enter** pentru valorile implicite. Aceasta generează un proiect TypeScript în `my-twenty-app/` cu un fișier inițial `application-config.ts`, un rol implicit, un flux de lucru CI și un test de integrare. + +**După această fază:** aveți codul sursă al aplicației pe mașina dvs. Încă nu rulează — aceasta este Faza 2. + +--- + +## Faza 2 — Rulați un server Twenty local + +Aplicația are nevoie de un server Twenty cu care să se sincronizeze. Serverul este o instanță Twenty completă — UI, API GraphQL, PostgreSQL — care rulează local în Docker. Codul local încarcă definițiile pe acel server, făcându-le să apară în UI. + +Generatorul de schelet vă propune să pornească unul pentru dvs.: + +> **Doriți să configurați o instanță Twenty locală?** + +* **Yes (recomandat)** — descarcă imaginea Docker `twentycrm/twenty-app-dev` și o pornește pe portul `2020`. Asigurați-vă mai întâi că Docker rulează. +* **No** — alegeți această opțiune dacă aveți deja un server Twenty la care doriți să vă conectați. Îl puteți conecta ulterior cu `yarn twenty remote add`. + +
+ Porniți instanța locală? +
+ +După ce serverul pornește, se deschide un browser pentru autentificare. Folosiți contul demo preconfigurat: + +* **E-mail:** `tim@apple.dev` +* **Parolă:** `tim@apple.dev` + +
+ Ecranul de autentificare Twenty +
+ +Faceți clic pe **Authorize** pe ecranul următor — aceasta oferă CLI-ului acces la spațiul dvs. de lucru. + +
+ Ecranul de autorizare Twenty CLI +
+ +Terminalul va confirma că totul este configurat. + +
+ Aplicația a fost creată cu succes +
+ +**După această fază:** aveți un server Twenty care rulează la [http://localhost:2020](http://localhost:2020), iar CLI-ul dvs. este autorizat să sincronizeze cu acesta. + + +Dacă Docker nu este instalat sau nu rulează, generatorul de schelet vă va indica comanda corectă de pornire pentru sistemul dvs. de operare. După ce Docker rulează, puteți continua cu `yarn twenty server start` — nu este nevoie să recreați scheletul. + + +--- + +## Faza 3 — Sincronizați modificările + +Aceasta este bucla internă în care veți petrece cea mai mare parte a timpului. + +```bash filename="Terminal" +cd my-twenty-app +yarn twenty dev +``` + +Aceasta monitorizează `src/`, reconstruiește la fiecare modificare și sincronizează rezultatul pe server. Editați un fișier, salvați, iar în decurs de o secundă serverul reflectă modificarea. Veți vedea în terminal un panou de stare în timp real. + +Pentru o ieșire mai detaliată (jurnale de build, cereri de sincronizare, urme ale erorilor), adăugați `--verbose`. + +
+ Ieșirea terminalului în modul de dezvoltare +
+ +Deschideți [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Ar trebui să vedeți aplicația dvs. listată la **Your Apps**. + +
+ Lista Your Apps care afișează My twenty app +
+ +Faceți clic pe **My twenty app** pentru a vedea **înregistrarea aplicației** — o înregistrare la nivel de server care descrie aplicația dvs. (nume, identificator, credențiale OAuth, sursă). O singură înregistrare poate fi instalată în mai multe spații de lucru pe același server. + +
+ Detalii despre înregistrarea aplicației +
+ +Faceți clic pe **View installed app** pentru a vedea instalarea în spațiul de lucru. Fila **About** afișează versiunea și opțiunile de administrare. + +
+ Aplicație instalată +
+ +**După această fază:** aveți o buclă de dezvoltare în timp real. Editați orice fișier în `src/` și acesta apare în UI. + +### Sincronizare unică pentru CI și scripturi + +Adăugați `--once` pentru a rula un singur build + sync și a ieși — același flux, fără watcher: + +```bash filename="Terminal" +yarn twenty dev --once +``` + +| Comandă | Comportament | When to use | +| ------------------------ | ------------------------------------------------------------------------------------ | --------------------------------------------------------------- | +| `yarn twenty dev` | Monitorizează și resincronizează la fiecare modificare. Rulează până când îl opriți. | Dezvoltare locală interactivă. | +| `yarn twenty dev --once` | Un singur build + sync, iese cu `0` la succes, `1` la eșec. | CI, hook-uri pre-commit, agenți AI, fluxuri de lucru scriptate. | + +Ambele moduri necesită un server în modul de dezvoltare și un remote autentificat. + + +Modul de dezvoltare este disponibil doar pe instanțele Twenty care rulează în modul development (`NODE_ENV=development`). Instanțele de producție resping cererile de sincronizare din modul de dezvoltare — folosiți `yarn twenty deploy` pentru a implementa pe serverele de producție. See [Publishing](/l/ro/developers/extend/apps/operations/publishing). + + +--- + +## Pornind de la un exemplu + +Folosiți `--example` pentru a începe cu un proiect mai complet (obiecte personalizate, câmpuri, funcții logice, componente front-end): + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app --example postcard +``` + +Exemplele se află în [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). You can also scaffold individual entities into an existing project with `yarn twenty add` — see [Scaffolding](/l/ro/developers/extend/apps/getting-started/scaffolding). + +--- + +## Ce puteți construi + +Aplicațiile sunt compuse din **entități** — fiecare definită într-un fișier TypeScript cu un singur `export default`: + +| Entitate | Ce face | +| --------------------------- | ----------------------------------------------------------------------------------------------- | +| **Obiecte și câmpuri** | Modele de date personalizate (carte poștală, factură etc.) cu câmpuri tipizate | +| **Funcții logice** | TypeScript pe server declanșat de rute HTTP, programări cron sau evenimente din baza de date | +| **Componente front-end** | Componente React care se afișează în UI-ul Twenty (panou lateral, widgeturi, meniul de comenzi) | +| **Abilități și agenți** | Capabilități AI — instrucțiuni reutilizabile și asistenți autonomi | +| **Vizualizări și navigare** | Vizualizări de listă preconfigurate și elemente de meniu în bara laterală | +| **Layouturi de pagină** | Pagini personalizate de detalii ale înregistrărilor cu file și widgeturi | + +Full reference: [Concepts](/l/ro/developers/extend/apps/getting-started/concepts). + +## Pașii următori + + + + Application identity, default role, install hooks, public assets. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/scaffolding.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/scaffolding.mdx new file mode 100644 index 0000000000..b2853cadf2 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/scaffolding.mdx @@ -0,0 +1,58 @@ +--- +title: Scaffolding +description: Generate entity files interactively with yarn twenty add — objects, fields, views, logic functions, and more. +icon: wand-magic-sparkles +--- + +Instead of creating entity files by hand, use the interactive scaffolder: + +```bash filename="Terminal" +yarn twenty add +``` + +It prompts you to pick an entity type and walks you through the required fields, then writes a ready-to-use file with a stable `universalIdentifier` and the correct `defineEntity()` call. + +You can also pass the entity type directly to skip the first prompt: + +```bash filename="Terminal" +yarn twenty add object +yarn twenty add logicFunction +yarn twenty add frontComponent +``` + +## Available entity types + +| Entity type | Command | Generated file | +| -------------------- | ------------------------------------ | ------------------------------------------------------- | +| Object | `yarn twenty add object` | `src/objects/\.ts` | +| Field | `yarn twenty add field` | `src/fields/\.ts` | +| Logic function | `yarn twenty add logicFunction` | `src/logic-functions/\.ts` | +| Front component | `yarn twenty add frontComponent` | `src/front-components/\.tsx` | +| Role | `yarn twenty add role` | `src/roles/\.ts` | +| Skill | `yarn twenty add skill` | `src/skills/\.ts` | +| Agent | `yarn twenty add agent` | `src/agents/\.ts` | +| View | `yarn twenty add view` | `src/views/\.ts` | +| Navigation menu item | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\.ts` | +| Page layout | `yarn twenty add pageLayout` | `src/page-layouts/\.ts` | + +## What the scaffolder generates + +Each entity type has its own template. For example, `yarn twenty add object` asks for: + +1. **Name (singular)** — e.g., `invoice` +2. **Name (plural)** — e.g., `invoices` +3. **Label (singular)** — auto-populated from the name (e.g., `Invoice`) +4. **Label (plural)** — auto-populated (e.g., `Invoices`) +5. **Create a view and navigation item?** — if you answer yes, the scaffolder also generates a matching view and sidebar link for the new object. + +Other entity types have simpler prompts — most only ask for a name. + +The `field` entity type is more detailed: it asks for the field name, label, type (from a list of all available field types like `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.), and the target object's `universalIdentifier`. + +## Custom output path + +Use the `--path` flag to place the generated file in a custom location: + +```bash filename="Terminal" +yarn twenty add logicFunction --path src/custom-folder +``` diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/troubleshooting.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/troubleshooting.mdx new file mode 100644 index 0000000000..90a429b604 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/getting-started/troubleshooting.mdx @@ -0,0 +1,12 @@ +--- +title: Depanare +description: Probleme comune la prima rulare — Docker, versiunea Node, Yarn, dependențe. +icon: wrench +--- + +* **Erori Docker** — Asigurați-vă că Docker Desktop (sau daemonul) rulează înainte de `yarn twenty server start`. Mesajul de eroare va afișa comanda corectă de pornire pentru sistemul dvs. de operare. +* **Versiune Node greșită** — Aveți nevoie de 24+. Verificați cu `node -v`. +* **Lipsește Yarn 4** — Rulați `corepack enable`. +* **Dependențe nefuncționale** — `rm -rf node_modules && yarn install`. + +Blocat? Întrebați pe [Discordul Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322). diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/layout/command-menu-items.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/layout/command-menu-items.mdx new file mode 100644 index 0000000000..84c29929b1 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/layout/command-menu-items.mdx @@ -0,0 +1,144 @@ +--- +title: Command Menu Items +description: Surface front components as quick actions and command menu (Cmd+K) entries with defineCommandMenuItem. +icon: terminal +--- + +A **command menu item** is the bridge between the user and a [front component](/l/ro/developers/extend/apps/layout/front-components). It registers the component in Twenty's command menu (Cmd+K) and, optionally, as a pinned quick-action button in the top-right corner of the page. + +```ts src/command-menu-items/open-dashboard.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + label: 'Open Dashboard', + shortLabel: 'Dashboard', + icon: 'IconLayoutDashboard', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +## Configuration fields + +| Field | Required | Description | +| --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `universalIdentifier` | Yes | Stable unique ID for the command | +| `label` | Yes | Full label shown in the command menu (Cmd+K) | +| `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens | +| `shortLabel` | No | Shorter label displayed on the pinned quick-action button | +| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) | +| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page | +| `availabilityType` | No | Controls where the command appears: `'GLOBAL'` (always available), `'RECORD_SELECTION'` (only when records are selected), or `'FALLBACK'` (shown when no other commands match) | +| `availabilityObjectUniversalIdentifier` | No | Restrict the command to pages of a specific object type (e.g. only on Company records) | +| `conditionalAvailabilityExpression` | No | A boolean expression that dynamically controls visibility (see below) | + +## Headless commands + +A command menu item paired with a [headless front component](/l/ro/developers/extend/apps/layout/front-components#headless-vs-non-headless) is the idiomatic way to ship a one-click action — run code, navigate, or confirm and execute. The Front Components page covers the [SDK Command components](/l/ro/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) that handle the action-and-unmount pattern. + +A typical flow: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +## Conditional availability expressions + +The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions: + +```ts src/command-menu-items/bulk-update.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; +import { + objectPermissions, + everyEquals, +} from 'twenty-sdk/front-component'; + +export default defineCommandMenuItem({ + universalIdentifier: '...', + label: 'Bulk Update', + availabilityType: 'RECORD_SELECTION', + frontComponentUniversalIdentifier: '...', + conditionalAvailabilityExpression: everyEquals( + objectPermissions, + 'canUpdateObjectRecords', + true, + ), +}); +``` + +### Context variables + +These represent the current state of the page: + +| Variable | Type | Description | +| ------------------------------ | --------- | ---------------------------------------------------------------- | +| `pageType` | `string` | Current page type (e.g. `'RecordIndexPage'`, `'RecordShowPage'`) | +| `isInSidePanel` | `boolean` | Whether the component is rendered in a side panel | +| `numberOfSelectedRecords` | `number` | Number of currently selected records | +| `isSelectAll` | `boolean` | Whether "select all" is active | +| `selectedRecords` | `array` | The selected record objects | +| `favoriteRecordIds` | `array` | IDs of favorited records | +| `objectPermissions` | `object` | Permissions for the current object type | +| `targetObjectReadPermissions` | `object` | Read permissions for the target object | +| `targetObjectWritePermissions` | `object` | Write permissions for the target object | +| `featureFlags` | `object` | Active feature flags | +| `objectMetadataItem` | `object` | Metadata of the current object type | +| `hasAnySoftDeleteFilterOnView` | `boolean` | Whether the current view has a soft-delete filter | + +### Operators + +Combine variables into boolean expressions: + +| Operator | Description | +| ----------------------------------- | ----------------------------------------------------------------- | +| `isDefined(value)` | `true` if the value is not null/undefined | +| `isNonEmptyString(value)` | `true` if the value is a non-empty string | +| `includes(array, value)` | `true` if the array contains the value | +| `includesEvery(array, prop, value)` | `true` if every item's property includes the value | +| `every(array, prop)` | `true` if the property is truthy on every item | +| `everyDefined(array, prop)` | `true` if the property is defined on every item | +| `everyEquals(array, prop, value)` | `true` if the property equals the value on every item | +| `some(array, prop)` | `true` if the property is truthy on at least one item | +| `someDefined(array, prop)` | `true` if the property is defined on at least one item | +| `someEquals(array, prop, value)` | `true` if the property equals the value on at least one item | +| `someNonEmptyString(array, prop)` | `true` if the property is a non-empty string on at least one item | +| `none(array, prop)` | `true` if the property is falsy on every item | +| `noneDefined(array, prop)` | `true` if the property is undefined on every item | +| `noneEquals(array, prop, value)` | `true` if the property does not equal the value on any item | diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/layout/front-components.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/layout/front-components.mdx new file mode 100644 index 0000000000..ff1763260e --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/layout/front-components.mdx @@ -0,0 +1,404 @@ +--- +title: Componente front-end +description: Construiți componente React care se afișează în interfața Twenty, cu izolare în sandbox. +icon: window-maximize +--- + +Componentele front-end sunt componente React care se afișează direct în interfața Twenty. Rulează într-un **Web Worker** izolat folosind Remote DOM — codul este izolat (sandboxed), dar se redă nativ în pagină, nu într-un iframe. + +## Unde pot fi utilizate componentele frontale + +Componentele frontale pot fi afișate în două locații în cadrul Twenty: + +* **Panou lateral** — Componentele frontale care nu sunt headless se deschid în panoul lateral din dreapta. Acesta este comportamentul implicit atunci când o componentă frontală este declanșată din meniul de comenzi. +* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside [page layouts](/l/ro/developers/extend/apps/layout/page-layouts). La configurarea unui tablou de bord sau a machetei unei pagini de înregistrare, utilizatorii pot adăuga un widget de componentă frontală. + +A front component on its own isn't reachable from the UI — you need to *surface* it. The two ways to do that are: + +* **Pair it with a [command menu item](/l/ro/developers/extend/apps/layout/command-menu-items)** — registers it in the command menu (Cmd+K) and, optionally, as a pinned quick-action. +* **Embed it as a widget in a [page layout](/l/ro/developers/extend/apps/layout/page-layouts)** — places it on a record's detail page or dashboard. + +## Exemplu de bază + +The quickest way to see a front component in action is to pair it with a [`defineCommandMenuItem`](/l/ro/developers/extend/apps/layout/command-menu-items), so it appears as a quick-action button in the top-right corner of the page: + +```tsx src/front-components/hello-world.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; + +const HelloWorld = () => { + return ( +
+

Hello from my app!

+

This component renders inside Twenty.

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', + name: 'hello-world', + description: 'A simple front component', + component: HelloWorld, +}); +``` + +```ts src/command-menu-items/hello-world.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345', + shortLabel: 'Hello', + label: 'Hello World', + icon: 'IconBolt', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +După sincronizarea cu `yarn twenty dev` (sau prin rularea comenzii `yarn twenty dev --once` o singură dată), acțiunea rapidă apare în colțul din dreapta sus al paginii: + +
+ Buton de acțiune rapidă în colțul din dreapta sus +
+ +Faceți clic pe el pentru a afișa componenta inline. + +## Câmpuri de configurare + +| Câmp | Obligatoriu | Descriere | +| --------------------- | ----------- | --------------------------------------------------------------------------- | +| `universalIdentifier` | Da | ID unic stabil pentru această componentă | +| `component` | Da | O funcție de componentă React | +| `name` | Nu | Nume afișat | +| `description` | Nu | Descriere a ceea ce face componenta | +| `isHeadless` | Nu | Setați la `true` dacă componenta nu are interfață vizibilă (vedeți mai jos) | + +## Plasarea unei componente front-end pe o pagină + +Dincolo de comenzi, puteți încorpora o componentă front-end direct într-o pagină de înregistrare adăugând-o ca widget într-un **layout de pagină**. See [Page Layouts](/l/ro/developers/extend/apps/layout/page-layouts) for details. + +## Headless vs non-headless + +Componentele frontale au două moduri de randare controlate de opțiunea `isHeadless`: + +**Non-headless (implicit)** — Componenta afișează o interfață vizibilă. Când este declanșat din meniul de comenzi, se deschide în panoul lateral. Acesta este comportamentul implicit când `isHeadless` este `false` sau omis. + +**Headless (`isHeadless: true`)** — Componenta se montează invizibil în fundal. Nu deschide panoul lateral. Componentele headless sunt concepute pentru acțiuni care execută logică și apoi se demontează — de exemplu, rularea unei sarcini asincrone, navigarea la o pagină sau afișarea unui modal de confirmare. Se potrivesc în mod natural cu componentele Command din SDK descrise mai jos. + +```tsx src/front-components/sync-tracker.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component'; +import { useEffect } from 'react'; + +const SyncTracker = () => { + const recordId = useRecordId(); + + useEffect(() => { + enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' }); + }, [recordId]); + + return null; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'sync-tracker', + description: 'Tracks record views silently', + isHeadless: true, + component: SyncTracker, +}); +``` + +Deoarece componenta returnează `null`, Twenty omite redarea unui container pentru ea — nu apare spațiu gol în layout. Componenta are în continuare acces la toate hook-urile și la API-ul de comunicare cu gazda. + +## Componentele Command din SDK + +Pachetul `twenty-sdk` oferă patru componente ajutătoare Command, concepute pentru componente front-end headless. Fiecare componentă execută o acțiune la montare, gestionează erorile afișând o notificare snackbar și demontează automat componenta de interfață la final. + +Importă-le din `twenty-sdk/command`: + +* **`Command`** — Rulează un callback asincron prin prop-ul `execute`. +* **`CommandLink`** — Navighează către o rută a aplicației. Props: `to`, `params`, `queryParams`, `options`. +* **`CommandModal`** — Deschide un modal de confirmare. Dacă utilizatorul confirmă, execută callback-ul `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`. +* **`CommandOpenSidePanelPage`** — Deschide o anumită pagină din panoul lateral. Props: `page`, `pageTitle`, `pageIcon`. + +Iată un exemplu complet de componentă front-end headless care folosește `Command` pentru a rula o acțiune din meniul de comenzi: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +Și un exemplu care folosește `CommandModal` pentru a cere confirmarea înainte de execuție: + +```tsx src/front-components/delete-draft.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { CommandModal } from 'twenty-sdk/command'; + +const DeleteDraft = () => { + const execute = async () => { + // perform the deletion + }; + + return ( + + ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456', + name: 'delete-draft', + description: 'Deletes a draft with confirmation', + component: DeleteDraft, + isHeadless: true, +}); +``` + +## Accesarea contextului de rulare + +În interiorul componentei, folosiți hook-urile SDK pentru a accesa utilizatorul curent, înregistrarea curentă și instanța componentei: + +```tsx src/front-components/record-info.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { + useUserId, + useRecordId, + useFrontComponentId, +} from 'twenty-sdk/front-component'; + +const RecordInfo = () => { + const userId = useUserId(); + const recordId = useRecordId(); + const componentId = useFrontComponentId(); + + return ( +
+

User: {userId}

+

Record: {recordId ?? 'No record context'}

+

Component: {componentId}

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012', + name: 'record-info', + component: RecordInfo, +}); +``` + +Hook-uri disponibile: + +| Hook | Returnează | Descriere | +| --------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------- | +| `useUserId()` | `string` sau `null` | ID-ul utilizatorului curent | +| `useSelectedRecordIds()` | `string[]` | Toate ID-urile înregistrărilor selectate (array gol dacă nu este selectată niciuna) | +| `useRecordId()` | `string` sau `null` | **Învechit.** Folosiți `useSelectedRecordIds()` în schimb | +| `useFrontComponentId()` | `string` | ID-ul acestei instanțe de componentă | +| `useFrontComponentExecutionContext(selector)` | variază | Accesați întregul context de execuție cu o funcție selector | + +## API-ul de comunicare cu gazda + +Componentele front-end pot declanșa navigare, ferestre modale și notificări folosind funcții din `twenty-sdk`: + +| Funcție | Descriere | +| ----------------------------------------------- | ----------------------------------- | +| `navigate(to, params?, queryParams?, options?)` | Navigați la o pagină din aplicație | +| `openSidePanelPage(params)` | Deschideți un panou lateral | +| `closeSidePanel()` | Închideți panoul lateral | +| `openCommandConfirmationModal(params)` | Afișați un dialog de confirmare | +| `enqueueSnackbar(params)` | Afișați o notificare tip toast | +| `unmountFrontComponent()` | Demontați componenta | +| `updateProgress(progress)` | Actualizați un indicator de progres | + +Iată un exemplu care folosește API-ul gazdă pentru a afișa un snackbar și a închide panoul lateral după finalizarea unei acțiuni: + +```tsx src/front-components/archive-record.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const ArchiveRecord = () => { + const recordId = useRecordId(); + + const handleArchive = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { status: 'ARCHIVED' } }, + id: true, + }, + }); + + await enqueueSnackbar({ + message: 'Record archived', + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Archive this record?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678', + name: 'archive-record', + description: 'Archives the current record', + component: ArchiveRecord, +}); +``` + +### Lucrul cu mai multe înregistrări + +Folosiți `useSelectedRecordIds()` pentru a gestiona mai multe înregistrări selectate. Acest lucru este util pentru operațiuni în masă: + +```tsx src/front-components/bulk-export.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const BulkExport = () => { + const selectedRecordIds = useSelectedRecordIds(); + + const handleExport = async () => { + const client = new CoreApiClient(); + + for (const recordId of selectedRecordIds) { + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { exported: true } }, + id: true, + }, + }); + } + + await enqueueSnackbar({ + message: `Exported ${selectedRecordIds.length} records`, + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Export {selectedRecordIds.length} selected record(s)?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901', + name: 'bulk-export', + description: 'Export selected records', + component: BulkExport, + command: { + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902', + label: 'Bulk Export', + availabilityType: 'RECORD_SELECTION', + conditionalAvailabilityExpression: numberOfSelectedRecords > 0, + }, +}); +``` + +## Resurse publice + +Componentele front-end pot accesa fișiere din directorul `public/` al aplicației folosind `getPublicAssetUrl`: + +```tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +const Logo = () => Logo; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'logo', + component: Logo, +}); +``` + +Consultați [secțiunea despre resurse publice](/l/ro/developers/extend/apps/config/public-assets) pentru detalii. + +## Stilizare + +Componentele front-end acceptă mai multe abordări de stilizare. Puteți folosi: + +* **Stiluri inline** — `style={{ color: 'red' }}` +* **Componente Twenty UI** — import din `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar și altele) +* **Emotion** — CSS-in-JS cu `@emotion/react` +* **Styled-components** — pattern-uri `styled.div` +* **Tailwind CSS** — clase utilitare +* **Orice bibliotecă CSS-in-JS** compatibilă cu React + +```tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Button, Tag, Status } from 'twenty-sdk/ui'; + +const StyledWidget = () => { + return ( +
+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456', + name: 'styled-widget', + component: StyledWidget, +}); +``` diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/layout/navigation-menu-items.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/layout/navigation-menu-items.mdx new file mode 100644 index 0000000000..d1491a9301 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/layout/navigation-menu-items.mdx @@ -0,0 +1,44 @@ +--- +title: Navigation Menu Items +description: Add custom entries to the workspace sidebar — links to saved views or external URLs. +icon: bars +--- + +A **navigation menu item** is an entry in the left sidebar. Use `defineNavigationMenuItem()` to ship custom sidebar links — typically one per [view](/l/ro/developers/extend/apps/layout/views) you ship — or to point at external URLs. + +```ts src/navigation-menu-items/example-navigation-menu-item.ts +import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define'; +import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view'; + +export default defineNavigationMenuItem({ + universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c', + name: 'example-navigation-menu-item', + icon: 'IconList', + color: 'blue', + position: 0, + type: NavigationMenuItemType.VIEW, + viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER, +}); +``` + +## Puncte cheie + +* `type` determines what the menu item links to. Each type pairs with a specific identifier field: + + | Tip | Ce face | Required field | + | ------------------------------------ | ------------------------------------ | ----------------------------------------------------------------------------- | + | `NavigationMenuItemType.VIEW` | Opens a saved view | `viewUniversalIdentifier` | + | `NavigationMenuItemType.LINK` | Opens an external URL | `link` | + | `NavigationMenuItemType.FOLDER` | Groups nested items under a label | `name` (and child items reference the folder via `folderUniversalIdentifier`) | + | `NavigationMenuItemType.OBJECT` | Opens an object's default index page | `targetObjectUniversalIdentifier` | + | `NavigationMenuItemType.PAGE_LAYOUT` | Opens a standalone page layout | `pageLayoutUniversalIdentifier` | + +* `position` controls ordering in the sidebar. + +* `icon` and `color` are optional and customize how the entry looks. + +* `folderUniversalIdentifier` is also available on any item to nest it inside a `FOLDER`-type parent. + + +**Common pitfall:** creating an object without an associated view + navigation menu item makes that object invisible to users. Unless it's a technical/internal object, every custom object should have a default view *and* a sidebar entry pointing at it. + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/layout/overview.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/layout/overview.mdx new file mode 100644 index 0000000000..629f068c94 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/layout/overview.mdx @@ -0,0 +1,56 @@ +--- +title: Overview +description: Place your app inside Twenty's UI — sidebar entries, saved views, record page tabs, and sandboxed React components. +icon: table-columns +--- + +A Twenty app's **layout layer** is everything the user sees: where the app surfaces in the sidebar, which list views it ships, how its record detail pages are arranged, and which custom React components render inside those pages. + +```text + Sidebar Record list Record detail page + ─────── ─────────── ────────────────── + [📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐ + [📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │ + [📋 Inbox ] │ ──────── │ │ [Notes ] │ + ▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab + │ │ Acme │ │ │ adds a tab... + └ defineNavi- │ … │ │ ┌────────────────┐ │ + gationMenu- └────▲─────┘ │ │ │ │ + Item points │ │ │ React UI │◀── …with a + to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent + └ defineView │ │ a Worker) │ │ widget inside + picks columns │ └────────────────┘ │ + and filters └─────────────────────┘ +``` + +## In this section + + + + `defineView` — saved list configurations: visible columns, filters, groups. + + + `defineNavigationMenuItem` — sidebar entries pointing at views or external URLs. + + + `definePageLayout` and `definePageLayoutTab` — tabs and widgets on a record's detail page. + + + `defineFrontComponent` — sandboxed React components that render inside Twenty. + + + `defineCommandMenuItem` — register front components as Cmd+K entries and quick actions. + + + +## Where the app surfaces + +| Surface | What it controls | Entity | +| --------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------- | +| **Sidebar** | A custom entry linking to a saved view or external URL | `defineNavigationMenuItem` | +| **Record list** | A saved configuration for an object — visible columns, order, filters, groups | `defineView` | +| **Record detail page** | The tabs and widgets on a record page (your own object's, or a standard one) | `definePageLayout`, `definePageLayoutTab` | +| **Inside any of the above** | A custom React widget — buttons, forms, dashboards, integrations | `defineFrontComponent` | +| **Command menu (Cmd+K)** | A pinned quick action or hidden command | `defineCommandMenuItem` | + +Front components run inside an isolated Web Worker using Remote DOM — they render *natively* in the page (not inside an iframe), but cannot reach the host page or DOM directly. Communication with Twenty happens through a message-passing host API. diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/layout/page-layouts.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/layout/page-layouts.mdx new file mode 100644 index 0000000000..5ef971d102 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/layout/page-layouts.mdx @@ -0,0 +1,102 @@ +--- +title: Layouturi de pagină +description: Customize record detail pages — tabs, widgets, and where front components render — using definePageLayout and definePageLayoutTab. +icon: table-columns +--- + +A **page layout** controls how a record's detail page is arranged: which tabs appear and what widgets they contain. Use `definePageLayout()` to declare a layout for an object you own, or `definePageLayoutTab()` to add a single tab to a layout that already exists (yours or a standard Twenty one). + +| Use case | Entitate | +| ---------------------------------------------------------------------- | --------------------- | +| Define the entire layout for a record page on an object you own | `definePageLayout` | +| Add one tab to an existing layout (your own object, or a standard one) | `definePageLayoutTab` | + +## definePageLayout + +Use this when you own the entire detail page — typically for a custom object you defined yourself. + +```ts src/page-layouts/example-record-page-layout.ts +import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +export default definePageLayout({ + universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134', + name: 'Example Record Page', + type: 'RECORD_PAGE', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + tabs: [ + { + universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5', + title: 'Hello World', + position: 50, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], + }, + ], +}); +``` + +### Puncte cheie + +* `type` este de obicei `'RECORD_PAGE'` pentru a personaliza vizualizarea de detaliu a unui obiect specific. +* `objectUniversalIdentifier` specifică la ce obiect se aplică această machetă. +* Fiecare `tab` definește o secțiune a paginii cu un `title`, `position` și `layoutMode` (`CANVAS` pentru layout liber). +* Each `widget` inside a tab can render a [front component](/l/ro/developers/extend/apps/layout/front-components), a relation list, or other built-in widget types. +* `position` pe file le controlează ordinea. Folosiți valori mai mari (de ex., 50) pentru a plasa filele personalizate după cele integrate. + +## definePageLayoutTab + +Use this when you only want to **add** a tab to an existing layout — for example, an analytics tab on the standard Company page, or an AI summary tab attached to your own object's layout. + +```ts src/page-layouts/example-extra-tab.ts +import { + definePageLayoutTab, + PageLayoutTabLayoutMode, +} from 'twenty-sdk/define'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = + '20202020-ab01-4001-8001-c0aba11c0100'; + +export default definePageLayoutTab({ + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001', + pageLayoutUniversalIdentifier: + COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER, + title: 'Hello World', + position: 1000, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], +}); +``` + +### Puncte cheie + +* `pageLayoutUniversalIdentifier` is **required** and must point to a page layout that already exists at install time — either a standard Twenty layout or one defined by your own app. Cross-app references to layouts owned by another installed app are not supported today. When the parent layout is missing, installation fails with a clear validation error. +* `widgets` are scoped to this tab only — they reference [front components](/l/ro/developers/extend/apps/layout/front-components), views, etc. exactly like widgets defined inline in `definePageLayout`. +* `position` controlează ordonarea în raport cu filele existente din layoutul țintă. Alege o valoare care să plaseze fila ta acolo unde dorești, relativ la filele predefinite. +* Use this instead of `definePageLayout` when you only want to add to an existing layout. Use `definePageLayout` when you own the entire layout. diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/layout/views.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/layout/views.mdx new file mode 100644 index 0000000000..0aea5c31f1 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/layout/views.mdx @@ -0,0 +1,43 @@ +--- +title: Vizualizări +description: Ship pre-configured saved views — column order, filters, groups — for objects in your app. +icon: list +--- + +A **view** is a saved configuration for how records of an object are displayed: which fields appear, their order, whether they're visible, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app — typically a default index view for each custom object you create. + +```ts src/views/example-view.ts +import { defineView, ViewKey } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; + +export default defineView({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'All example items', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + icon: 'IconList', + key: ViewKey.INDEX, + position: 0, + fields: [ + { + universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0', + fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER, + position: 0, + isVisible: true, + size: 200, + }, + ], +}); +``` + +## Puncte cheie + +* `objectUniversalIdentifier` specifică la ce obiect se aplică această vizualizare. It can be a custom object you defined or a standard Twenty object. +* `key` determines the view type — `ViewKey.INDEX` is the main list view for the object. +* `fields` controls which columns appear and in what order. Fiecare câmp face referire la un `fieldMetadataUniversalIdentifier`. +* You can also declare `filters`, `filterGroups`, `groups`, and `fieldGroups` for advanced configurations. +* `position` controls ordering when multiple views exist for the same object. + +## How views show up in the UI + +A view by itself isn't reachable from the sidebar. To make it appear there, pair it with a [navigation menu item](/l/ro/developers/extend/apps/layout/navigation-menu-items) of type `VIEW` that points at the view's `universalIdentifier`. That's the canonical pattern: every custom object typically ships a default view + a sidebar entry that opens it. diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/logic/connections.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/logic/connections.mdx new file mode 100644 index 0000000000..1a99c59742 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/logic/connections.mdx @@ -0,0 +1,193 @@ +--- +title: Connections +description: Let your app act on a user's behalf in third-party services via OAuth. +icon: plug +--- + +Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API. + +Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate. + + + + + +A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace. + +A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials. + +```ts src/connection-providers/linear-connection.ts +import { defineConnectionProvider } from 'twenty-sdk/define'; + +export default defineConnectionProvider({ + universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f', + name: 'linear', + displayName: 'Linear', + icon: 'IconBrandLinear', + type: 'oauth', + oauth: { + authorizationEndpoint: 'https://linear.app/oauth/authorize', + tokenEndpoint: 'https://api.linear.app/oauth/token', + scopes: ['read', 'write'], + // These must match keys in `defineApplication.serverVariables` below. + clientIdVariable: 'LINEAR_CLIENT_ID', + clientSecretVariable: 'LINEAR_CLIENT_SECRET', + // Optional: defaults to 'json'. Some providers (Linear, Slack) want + // 'form-urlencoded' for the token request. + tokenRequestContentType: 'form-urlencoded', + // Optional: defaults to true. Disable only if the provider rejects PKCE. + usePkce: false, + // Optional: extra query params on the authorize URL. + // authorizationParams: { prompt: 'consent' }, + // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect. + // revokeEndpoint: 'https://example.com/oauth/revoke', + }, +}); +``` + +```ts src/application.config.ts +import { defineApplication } from 'twenty-sdk/define'; + +export default defineApplication({ + universalIdentifier: '...', + displayName: 'Linear', + description: 'Connect Linear to Twenty.', + defaultRoleUniversalIdentifier: '...', + // OAuth client credentials live on the app registration (one OAuth app per + // Twenty server, configured by the admin) — not per-workspace. Declare them + // as serverVariables so the admin can fill them in once for all installs. + serverVariables: { + LINEAR_CLIENT_ID: { + description: 'OAuth client ID from your Linear OAuth application.', + isSecret: false, + isRequired: true, + }, + LINEAR_CLIENT_SECRET: { + description: 'OAuth client secret from your Linear OAuth application.', + isSecret: true, + isRequired: true, + }, + }, +}); +``` + +Key points: + +* `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`). +* `displayName` shows in the per-app settings tab and in the AI tool list. +* `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo. +* Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server. +* Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled. +* `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`. + +The OAuth callback URL your provider needs to whitelist is: + +``` +https:///apps/oauth/callback +``` + + + + + +Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens. + +```ts src/logic-functions/handlers/create-linear-issue-handler.ts +import { listConnections } from 'twenty-sdk/logic-function'; + +export const createLinearIssueHandler = async (input: { + teamId?: string; + title?: string; +}) => { + if (!input.teamId || !input.title) { + return { success: false, error: 'teamId and title are required' }; + } + + const connections = await listConnections({ providerName: 'linear' }); + + // Workspace-shared credentials win when present; fall back to the first + // user-visibility one. For HTTP-route triggers you typically pick the + // request user's connection via event.userWorkspaceId instead. + const connection = + connections.find((c) => c.visibility === 'workspace') ?? connections[0]; + + if (!connection) { + return { + success: false, + error: + 'Linear is not connected. Open the app settings and click "Add connection".', + }; + } + + // Use connection.accessToken to call the third-party API. + const response = await fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + Authorization: `Bearer ${connection.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`, + }), + }); + + return { success: response.ok }; +}; +``` + +Each connection has: + +| Field | Description | +| ----------------- | -------------------------------------------------------------------------------------------------------- | +| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one | +| `visibility` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) | +| `scopes` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) | +| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers | +| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) | +| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) | +| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect | + +Key points: + +* Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers. +* The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set). +* `getConnection(id)` is the single-row equivalent. + + + + + +When a user clicks "Add connection," they're prompted to pick a visibility: + +* **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not. +* **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user. + +Use the right one for each handler: + +```ts +// HTTP-route trigger — prefer the request user's own connection. +const conn = + connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ?? + connections.find((c) => c.visibility === 'workspace'); + +// Cron trigger — no request user; only shared credentials are sensible. +const conn = connections.find((c) => c.visibility === 'workspace'); +``` + +Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side. + + + + + +For each connection provider, the server admin needs to register an OAuth app at the third party first. + +1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new). +2. Set the **Redirect URI** to `\/apps/oauth/callback`. +3. Copy the generated **Client ID** and **Client Secret**. +4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`. +5. Workspace members can then add connections from the per-app **Connections** section. + + + + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/logic/logic-functions.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/logic/logic-functions.mdx new file mode 100644 index 0000000000..be6ab12343 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/logic/logic-functions.mdx @@ -0,0 +1,375 @@ +--- +title: Funcții logice +description: Definește funcții TypeScript pe partea de server cu declanșatoare HTTP, cron și de evenimente din baza de date. +icon: bolt +--- + +Funcțiile de logică sunt funcții TypeScript pe partea de server care rulează pe platforma Twenty. Acestea pot fi declanșate de solicitări HTTP, programări cron sau evenimente din baza de date — și pot fi, de asemenea, expuse ca instrumente pentru agenți AI. + + + + +Fiecare fișier de funcție folosește `defineLogicFunction()` pentru a exporta o configurație cu un handler și declanșatoare opționale. + +```ts src/logic-functions/createPostCard.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define'; +import { CoreApiClient, type Person } from 'twenty-client-sdk/core'; + +const handler = async (params: RoutePayload) => { + const client = new CoreApiClient(); + const body = (params.body ?? {}) as { name?: string }; + const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'; + + const result = await client.mutation({ + createPostCard: { + __args: { data: { name } }, + id: true, + name: true, + }, + }); + return result; +}; + +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'create-new-post-card', + timeoutSeconds: 2, + handler, + httpRouteTriggerSettings: { + path: '/post-card/create', + httpMethod: 'POST', + isAuthRequired: true, + }, + /*databaseEventTriggerSettings: { + eventName: 'people.created', + },*/ + /*cronTriggerSettings: { + pattern: '0 0 1 1 *', + },*/ +}); +``` + +Tipuri de declanșatoare disponibile: +* **httpRoute**: Expune funcția pe o cale și metodă HTTP **sub endpoint-ul `/s/`**: +> de ex. `path: '/post-card/create'` este apelabil la `https://your-twenty-server.com/s/post-card/create` +* **cron**: Rulează funcția pe un program folosind o expresie CRON. +* **databaseEvent**: Rulează la evenimentele ciclului de viață ale obiectelor din spațiul de lucru. Când operațiunea evenimentului este `updated`, câmpurile specifice de urmărit pot fi specificate în array-ul `updatedFields`. Dacă este lăsat nedefinit sau gol, orice actualizare va declanșa funcția. +> de ex. `person.updated`, `*.created`, `company.*` + + +Puteți, de asemenea, să executați manual o funcție folosind CLI: + +```bash filename="Terminal" +yarn twenty exec -n create-new-post-card -p '{"key": "value"}' +``` + +```bash filename="Terminal" +yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + +Puteți urmări jurnalele cu: + +```bash filename="Terminal" +yarn twenty logs +``` + + +#### Payload-ul declanșatorului de rută + +Când un declanșator de rută invocă funcția logică, aceasta primește un obiect `RoutePayload` care urmează +[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). +Importați tipul `RoutePayload` din `twenty-sdk`: + +```ts +import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define'; + +const handler = async (event: RoutePayload) => { + const { headers, queryStringParameters, pathParameters, body } = event; + const { method, path } = event.requestContext.http; + + return { message: 'Success' }; +}; +``` + +Tipul `RoutePayload` are următoarea structură: + + | Proprietate | Tip | Descriere | Exemplu | + | ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | + | `headers` | `Record\` | Anteturi HTTP (doar cele listate în `forwardedRequestHeaders`) | consultați secțiunea de mai jos | + | `queryStringParameters` | `Record\` | Parametri query string (valorile multiple unite cu virgule) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` | + | `pathParameters` | `Record\` | Parametri de cale extrași din modelul rutei | `/users/:id`, `/users/123` -> `{ id: '123' }` | + | `body` | `object \| null` | Corpul cererii analizat (JSON) | `{ id: 1 }` -> `{ id: 1 }` | + | `rawBody` | `string \| undefined` | Corpul original al cererii în UTF-8, înainte de parsarea JSON. Util pentru verificarea semnăturilor de tip HMAC pentru webhook-uri (de exemplu, `X-Hub-Signature-256` de la GitHub, Stripe). `undefined` atunci când mediul de execuție nu a păstrat-o. | | + | `isBase64Encoded` | `boolean` | Indică dacă corpul este codificat în base64 | | + | `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) | | + | `requestContext.http.path` | `string` | Calea brută a cererii | | + + +#### forwardedRequestHeaders + +Î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, listați-le explicit în array-ul `forwardedRequestHeaders`: + +```ts +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'webhook-handler', + handler, + httpRouteTriggerSettings: { + path: '/webhook', + httpMethod: 'POST', + isAuthRequired: false, + forwardedRequestHeaders: ['x-webhook-signature', 'content-type'], + }, +}); +``` + +În handler, accesați anteturile transmise mai departe astfel: + +```ts +const handler = async (event: RoutePayload) => { + const signature = event.headers['x-webhook-signature']; + const contentType = event.headers['content-type']; + + // Validate webhook signature... + return { received: true }; +}; +``` + + +Numele anteturilor sunt normalizate la litere mici. Accesați-le folosind chei cu litere mici (de exemplu, `event.headers['content-type']`). + + +#### Expunerea unei funcții ca instrument AI sau ca acțiune în fluxul de lucru + +Funcțiile logice pot fi expuse în două locuri, fiecare cu propriul declanșator: + +* **`toolTriggerSettings`** — face funcția descoperibilă de către funcționalitățile AI ale Twenty (chat, MCP, apelarea de funcții). Folosește JSON Schema standard, formatul pe care LLM-urile îl înțeleg nativ. +* **`workflowActionTriggerSettings`** — determină ca funcția să apară ca un pas în constructorul vizual de fluxuri de lucru. Folosește `InputSchema` bogat al Twenty, astfel încât constructorul să poată afișa editori de câmp adecvați, selectoare de variabile și etichete. + +O funcție poate opta pentru una, cealaltă sau ambele. Acestea stau alături de `cronTriggerSettings`, `databaseEventTriggerSettings` și `httpRouteTriggerSettings` — același tipar, aceeași formă. + +```ts src/logic-functions/enrich-company.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const handler = async (params: { companyName: string; domain?: string }) => { + const client = new CoreApiClient(); + + 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, + toolTriggerSettings: {}, +}); +``` + +Puncte cheie: + +* O funcție poate combina suprafețele — declară atât `toolTriggerSettings`, cât și `workflowActionTriggerSettings` pentru a o expune atât în chat, cât și în constructorul de fluxuri de lucru. +* `toolTriggerSettings.inputSchema` și `workflowActionTriggerSettings.inputSchema` sunt ambele opționale. Când sunt omise, generatorul de manifest le deduce din codul sursă al handlerului (JSON Schema pentru instrumentul AI, `InputSchema` al Twenty pentru acțiunea de flux de lucru). Furnizează unul în mod explicit atunci când dorești o tipizare mai bogată — de exemplu, cu câmpuri compatibile cu `FieldMetadataType`, precum `CURRENCY` sau `RELATION` pentru constructorul de fluxuri de lucru, sau cu câmpuri `description` pe care agentul AI le poate citi: + +```ts +export default defineLogicFunction({ + ..., + toolTriggerSettings: { + inputSchema: { + 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'], + }, + }, +}); +``` + + +**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. + + + + + + +**Install hooks** — pre-install and post-install handlers — share this runtime but are declared with their own define functions and don't take trigger settings. See [Install Hooks](/l/ro/developers/extend/apps/config/install-hooks) for `definePreInstallLogicFunction` and `definePostInstallLogicFunction`. + + +## Clienți API tipizați (twenty-client-sdk) + +Pachetul `twenty-client-sdk` oferă doi clienți GraphQL tipați pentru a interacționa cu API-ul Twenty din funcțiile de logică și componentele Front. + +| Client | Importați | Endpoint | Generat? | +| ------------------- | ---------------------------- | ------------------------------------------------------------------- | ---------------------------- | +| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — date ale spațiului de lucru (înregistrări, obiecte) | Da, în timpul dev/build | +| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configurarea spațiului de lucru, încărcări de fișiere | Nu, este livrat preconstruit | + + + + +`CoreApiClient` este clientul principal pentru interogarea și modificarea datelor din spațiul de lucru. Este generat din schema spațiului de lucru în timpul `yarn twenty dev` sau `yarn twenty build`, astfel încât este complet tipizat pentru a corespunde obiectelor și câmpurilor dvs. + +```ts +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const client = new CoreApiClient(); + +// Query records +const { companies } = await client.query({ + companies: { + edges: { + node: { + id: true, + name: true, + domainName: { + primaryLinkLabel: true, + primaryLinkUrl: true, + }, + }, + }, + }, +}); + +// Create a record +const { createCompany } = await client.mutation({ + createCompany: { + __args: { + data: { + name: 'Acme Corp', + }, + }, + id: true, + name: true, + }, +}); +``` + +Clientul folosește o sintaxă de tip selection-set: transmiteți `true` pentru a include un câmp, folosiți `__args` pentru argumente și imbricați obiecte pentru relații. Obțineți autocompletare și verificare a tipurilor complete, pe baza schemei spațiului dvs. de lucru. + + +**CoreApiClient este generat în timpul dev/build.** Dacă îl utilizați fără a rula mai întâi `yarn twenty dev` sau `yarn twenty build`, va arunca o eroare. Generarea are loc automat — CLI inspectează schema GraphQL a spațiului dvs. de lucru și generează un client tipizat folosind `@genql/cli`. + + +#### Folosirea CoreSchema pentru adnotări de tip + +`CoreSchema` oferă tipuri TypeScript care corespund obiectelor din spațiul dvs. de lucru — utile pentru tiparea stării componentelor sau a parametrilor funcțiilor: + +```ts +import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core'; +import { useState } from 'react'; + +const [company, setCompany] = useState< + Pick | undefined +>(undefined); + +const client = new CoreApiClient(); +const result = await client.query({ + company: { + __args: { filter: { position: { eq: 1 } } }, + id: true, + name: true, + }, +}); +setCompany(result.company); +``` + + + + +`MetadataApiClient` este livrat preconstruit împreună cu SDK-ul (nu este necesară generarea). Interoghează endpointul `/metadata` pentru configurarea spațiului de lucru, aplicații și încărcări de fișiere. + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; + +const metadataClient = new MetadataApiClient(); + +// List first 10 objects in the workspace +const { objects } = await metadataClient.query({ + objects: { + edges: { + node: { + id: true, + nameSingular: true, + namePlural: true, + labelSingular: true, + isCustom: true, + }, + }, + __args: { + filter: {}, + paging: { first: 10 }, + }, + }, +}); +``` + +#### Încărcarea fișierelor + +`MetadataApiClient` include o metodă `uploadFile` pentru atașarea fișierelor la câmpuri de tip fișier: + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import * as fs from 'fs'; + +const metadataClient = new MetadataApiClient(); + +const fileBuffer = fs.readFileSync('./invoice.pdf'); + +const uploadedFile = await metadataClient.uploadFile( + fileBuffer, // file contents as a Buffer + 'invoice.pdf', // filename + 'application/pdf', // MIME type + '58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier +); + +console.log(uploadedFile); +// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' } +``` + +| Parametru | Tip | Descriere | +| ---------------------------------- | -------- | ------------------------------------------------------------------- | +| `fileBuffer` | `Buffer` | Conținutul brut al fișierului | +| `filename` | `string` | Numele fișierului (folosit pentru stocare și afișare) | +| `contentType` | `string` | Tipul MIME (implicit `application/octet-stream` dacă este omis) | +| `fieldMetadataUniversalIdentifier` | `string` | `universalIdentifier` al câmpului de tip fișier de pe obiectul dvs. | + +Puncte cheie: +* Folosește `universalIdentifier` al câmpului (nu ID-ul specific spațiului de lucru), astfel încât codul dvs. de încărcare funcționează în orice spațiu de lucru în care aplicația dvs. este instalată. +* `url` returnat este un URL semnat pe care îl puteți folosi pentru a accesa fișierul încărcat. + + + + + + Când codul dvs. rulează pe Twenty (funcții de logică sau componente Front), platforma injectează acreditările ca variabile de mediu: + + * `TWENTY_API_URL` — URL-ul de bază al API-ului Twenty + * `TWENTY_APP_ACCESS_TOKEN` — Cheie cu durată scurtă, limitată la rolul implicit de funcție al aplicației + + Nu trebuie să le transmiteți clienților — aceștia citesc automat din `process.env`. Permisiunile cheii API sunt determinate de rolul referențiat în `defaultRoleUniversalIdentifier` din `application-config.ts`. + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/logic/overview.mdx new file mode 100644 index 0000000000..dfd8ffe748 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/logic/overview.mdx @@ -0,0 +1,55 @@ +--- +title: Prezentare generală +description: Server-side TypeScript that runs inside Twenty — triggered by HTTP routes, cron schedules, database events, AI tools, or workflow actions. +icon: bolt +--- + +A Twenty app's **logic layer** is the code that *runs* — server-side TypeScript handlers reacting to HTTP requests, cron schedules, and record changes; AI skills and agents that live inside the workspace; and OAuth connections that let your functions act on a user's behalf in third-party services. + +```text + ┌─ HTTP route ──┐ + │ Cron schedule │ + │ Database event │ ┌────────────────────┐ + triggers ─┤ AI tool call ├─────▶│ Logic function │ + │ Workflow action │ │ (your handler) │ + │ Manual exec │ └────────────────────┘ + └────────────────────┘ │ + ▼ + ┌────────────────────────────┐ + │ Twenty API (records) │ + │ Third-party API │ + │ (via Connection token) │ + └────────────────────────────┘ +``` + +## In this section + + + + The core building block — trigger types, payloads, and the typed API client. + + + Reusable AI agent instructions and assistants with custom system prompts. + + + OAuth credentials your app holds for third-party services — Linear, GitHub, Slack, and more. + + + +## Trigger types at a glance + +A logic function picks one or more triggers — every entry below is a separate field on `defineLogicFunction()`: + +| Declanșator | When it runs | Setare | +| -------------------------- | ---------------------------------------------------------- | ------------------------------- | +| **HTTP route** | A request hits your `/s/\` endpoint | `httpRouteTriggerSettings` | +| **Cron** | A CRON expression matches | `cronTriggerSettings` | +| **Eveniment baza de date** | A workspace record is created, updated, or deleted | `databaseEventTriggerSettings` | +| **AI tool** | A Twenty AI feature decides to call your function | `toolTriggerSettings` | +| **Acțiune Workflow** | A workflow step invokes your function | `workflowActionTriggerSettings` | + +Functions run sandboxed in isolated Node.js processes and access the workspace through a typed API client scoped to the role declared on [`defineApplication()`](/l/ro/developers/extend/apps/config/application). + + +**Install-time hooks** — code that runs before or after the install — share this runtime but use their own define functions and live under [Config → Install Hooks](/l/ro/developers/extend/apps/config/install-hooks). + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/logic/skills-and-agents.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/logic/skills-and-agents.mdx new file mode 100644 index 0000000000..82a66ef20f --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/logic/skills-and-agents.mdx @@ -0,0 +1,69 @@ +--- +title: Skills & Agents +description: Define AI skills and agents for your app. +icon: robot +--- + + + Skills and agents are currently in alpha. The feature works but is still evolving. + + +Apps can define AI capabilities that live inside the workspace — reusable skill instructions and agents with custom system prompts. + + + + +Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation: + +```ts src/skills/example-skill.ts +import { defineSkill } from 'twenty-sdk/define'; + +export default defineSkill({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'sales-outreach', + label: 'Sales Outreach', + description: 'Guides the AI agent through a structured sales outreach process', + icon: 'IconBrain', + content: `You are a sales outreach assistant. When reaching out to a prospect: +1. Research the company and recent news +2. Identify the prospect's role and likely pain points +3. Draft a personalized message referencing specific details +4. Keep the tone professional but conversational`, +}); +``` + +Key points: +* `name` is a unique identifier string for the skill (kebab-case recommended). +* `label` is the human-readable display name shown in the UI. +* `content` contains the skill instructions — this is the text the AI agent uses. +* `icon` (optional) sets the icon displayed in the UI. +* `description` (optional) provides additional context about the skill's purpose. + + + + +Agents are AI assistants that live inside your workspace. Use `defineAgent()` to create agents with a custom system prompt: + +```ts src/agents/example-agent.ts +import { defineAgent } from 'twenty-sdk/define'; + +export default defineAgent({ + universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123', + name: 'sales-assistant', + label: 'Sales Assistant', + description: 'Helps the sales team draft outreach emails and research prospects', + icon: 'IconRobot', + prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.', +}); +``` + +Key points: +* `name` is the unique identifier string for the agent (kebab-case recommended). +* `label` is the display name shown in the UI. +* `prompt` is the system prompt that defines the agent's behavior. +* `description` (optional) provides context about what the agent does. +* `icon` (optional) sets the icon displayed in the UI. +* `modelId` (optional) overrides the default AI model used by the agent. + + + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/operations/cli.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/operations/cli.mdx new file mode 100644 index 0000000000..0cccebdbb5 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/operations/cli.mdx @@ -0,0 +1,78 @@ +--- +title: CLI +description: yarn twenty commands for executing functions, streaming logs, managing app installations, and switching remotes. +icon: terminal +--- + +Beyond `dev`, `build`, `add`, and `typecheck`, the `yarn twenty` CLI provides commands for executing functions, viewing logs, and managing app installations. + +## Executing functions (`yarn twenty exec`) + +Run a logic function manually without triggering it via HTTP, cron, or database event: + +```bash filename="Terminal" +# Execute by function name +yarn twenty exec -n create-new-post-card + +# Execute by universalIdentifier +yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf + +# Pass a JSON payload +yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}' + +# Execute the post-install function +yarn twenty exec --postInstall +``` + +## Viewing function logs (`yarn twenty logs`) + +Stream execution logs for your app's logic functions: + +```bash filename="Terminal" +# Stream all function logs +yarn twenty logs + +# Filter by function name +yarn twenty logs -n create-new-post-card + +# Filter by universalIdentifier +yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + + +This is different from `yarn twenty server logs`, which shows the Docker container logs. `yarn twenty logs` shows your app's function execution logs from the Twenty server. + + +## Uninstalling an app (`yarn twenty uninstall`) + +Remove your app from the active workspace: + +```bash filename="Terminal" +yarn twenty uninstall + +# Skip the confirmation prompt +yarn twenty uninstall --yes +``` + +## Managing remotes + +A **remote** is a Twenty server that your app connects to. During setup, the scaffolder creates one for you automatically. You can add more remotes or switch between them at any time. + +```bash filename="Terminal" +# Add a new remote (opens a browser for OAuth login) +yarn twenty remote add + +# Connect to a local Twenty server (auto-detects port 2020 or 3000) +yarn twenty remote add --local + +# Add a remote non-interactively (useful for CI) +yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote + +# List all configured remotes +yarn twenty remote list + +# Switch the active remote +yarn twenty remote switch +``` + +Your credentials are stored in `~/.twenty/config.json`. diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/operations/overview.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/operations/overview.mdx new file mode 100644 index 0000000000..848b7d74a1 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/operations/overview.mdx @@ -0,0 +1,29 @@ +--- +title: Prezentare generală +description: Build, test, and ship your app — CLI commands, integration tests, CI, and publishing to a server or to npm. +icon: rocket +--- + +The **operations layer** is everything you do *to* your app rather than *with* it: invoking CLI commands, running integration tests against a real Twenty server, configuring CI, and shipping releases — either as a tarball deployed to a single server or as an npm package listed in the marketplace. + +```text + develop ─▶ test ─▶ build ─▶ deploy / publish + ─────── ──── ───── ───────────────── + yarn yarn yarn yarn twenty deploy (tarball → one server) + twenty test twenty + dev build yarn twenty publish (npm → marketplace) +``` + +## In this section + + + + `yarn twenty` reference — exec, logs, uninstall, remotes. + + + Vitest setup, integration tests, type checking, CI workflow. + + + Build, deploy a tarball, publish to npm, install. + + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/operations/publishing.mdx new file mode 100644 index 0000000000..dabed6978a --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/operations/publishing.mdx @@ -0,0 +1,295 @@ +--- +title: Publicare +icon: încarcă +description: Distribuie aplicația ta Twenty în marketplace sau implementeaz-o intern. +--- + +## Prezentare generală + +După ce aplicația ta este [construită și testată local](/l/ro/developers/extend/apps/getting-started/concepts), ai două căi pentru distribuire: + +* **Implementează un tarball** — încarcă aplicația direct pe un server Twenty anume pentru uz intern sau privat. +* **Publică pe npm** — listează aplicația ta în marketplace-ul Twenty pentru ca orice spațiu de lucru să o poată descoperi și instala. + +Ambele căi pornesc din aceeași etapă de **build**. + +## Construirea aplicației + +Rulează comanda `build` pentru a compila aplicația și a genera un `manifest.json` pregătit pentru distribuire: + +```bash filename="Terminal" +yarn twenty build +``` + +Aceasta compilează sursele TypeScript, transpilează funcțiile de logică și componentele de front-end și scrie totul în `.twenty/output/`. Adaugă `--tarball` pentru a produce și un pachet `.tgz` pentru distribuire manuală sau pentru comanda de deploy. + +## Implementare pe un server (tarball) + +Pentru aplicațiile pe care nu le dorești disponibile public — instrumente proprietare, integrări doar pentru enterprise sau build-uri experimentale — poți implementa un tarball direct pe un server Twenty. + +### Cerințe + +Înainte de implementare, ai nevoie de un remote configurat care să indice serverul țintă. Remote-urile stochează local URL-ul serverului și credențialele de autentificare în `~/.twenty/config.json`. + +Adaugă un remote: + +```bash filename="Terminal" +yarn twenty remote add --api-url https://your-twenty-server.com --as production +``` + +### Implementare + +Construiește și încarcă aplicația ta pe server într-un singur pas: + +```bash filename="Terminal" +yarn twenty deploy +# To deploy to a specific remote: +# yarn twenty deploy --remote production +``` + +### Partajarea unei aplicații implementate + + +Partajarea aplicațiilor private (tarball) între spații de lucru este o funcționalitate **Enterprise**. Fila **Distribuție** va afișa un mesaj de actualizare în locul controalelor de partajare până când spațiul tău de lucru are o cheie Enterprise validă. Mergi la [Setări > Panou de administrare > Enterprise](/settings/admin-panel#enterprise) pentru a o activa. + + +Aplicațiile tarball nu sunt listate în marketplace-ul public, astfel încât alte spații de lucru de pe același server nu le vor descoperi prin navigare. După ce spațiul tău de lucru este pe planul Enterprise, poți partaja o aplicație implementată astfel: + +1. Mergi la **Setări > Aplicații > Înregistrări** și deschide aplicația ta +2. În fila **Distribuție**, fă clic pe **Copiază linkul de partajare** +3. Partajează acest link cu utilizatori din alte spații de lucru — îi duce direct la pagina de instalare a aplicației + +Linkul de partajare folosește URL-ul de bază al serverului (fără niciun subdomeniu de spațiu de lucru), astfel încât funcționează pentru orice spațiu de lucru de pe server. + +### Gestionarea versiunilor + +Când actualizezi o aplicație tarball deja implementată, serverul solicită ca `version` din `package.json` să fie **strict mai mare** (conform ordonării [semver](https://semver.org)) decât versiunea implementată în prezent. Redeployarea aceleiași versiuni sau trimiterea uneia inferioare este respinsă înainte ca tarball-ul să fie stocat — vei vedea o eroare `VERSION_ALREADY_EXISTS` de la CLI. + +Pentru a lansa o actualizare: + +1. Incrementează câmpul `version` din `package.json` (de ex. `1.2.3` → `1.2.4`, `1.3.0` sau `2.0.0`) +2. Rulează `yarn twenty deploy` (sau `yarn twenty deploy --remote production`) +3. Spațiile de lucru care au aplicația instalată vor vedea actualizarea disponibilă în setările lor + + +Etichetele de pre-lansare funcționează conform așteptărilor: incrementarea de la `1.0.0-rc.1` la `1.0.0-rc.2` este permisă, iar o lansare finală precum `1.0.0` este recunoscută corect ca fiind mai mare decât `1.0.0-rc.5`. Versiunea din `package.json` trebuie să fie ea însăși un șir semver valid. + + +{/* TODO: add screenshot of the Upgrade button */} + +### Compatibilitatea versiunii serverului + +Dacă aplicația ta folosește o funcționalitate introdusă într-o anumită versiune de server Twenty (de exemplu, furnizori OAuth adăugați în v2.3.0), ar trebui să declari versiunea minimă de server necesară aplicației folosind câmpul `engines.twenty` din `package.json`: + +```json filename="package.json" +{ + "name": "twenty-my-app", + "version": "1.0.0", + "engines": { + "node": "^24.5.0", + "twenty": ">=2.3.0" + } +} +``` + +Valoarea este un [interval semver](https://github.com/npm/node-semver#ranges) standard. Tipare comune: + +| Interval | Semnificație | +| ---------------------------------- | ------------------------------------------------------ | +| `>=2.3.0` | Orice server de la 2.3.0 încolo | +| `>=2.3.0 \<3.0.0` | 2.3.0 sau ulterior, dar sub următoarea versiune majoră | +| `^2.3.0` | La fel ca `>=2.3.0 \<3.0.0` | + +**Ce se întâmplă în timpul implementării și instalării:** + +* Dacă `engines.twenty` este setat și versiunea serverului țintă nu respectă intervalul, implementarea (încărcarea arhivei tarball) sau instalarea este respinsă cu eroarea `SERVER_VERSION_INCOMPATIBLE` și cu un mesaj care indică atât intervalul necesar, cât și versiunea efectivă a serverului. +* Dacă `engines.twenty` nu este setat, aplicația este acceptată pe orice versiune de server (retrocompatibilă cu aplicațiile existente). +* Dacă serverul nu are nicio `APP_VERSION` configurată, verificarea este omisă. + + +Serverul este verificarea autoritativă — validează `engines.twenty` atât la încărcarea arhivei tarball, cât și la instalarea în spațiul de lucru. Dacă implementezi un tarball în afara fluxului standard sau instalezi din marketplace, serverul impune în continuare compatibilitatea. + + +## CI/CD automatizat (fluxuri de lucru preconfigurate) + +Aplicațiile generate cu `create-twenty-app` vin, gata de utilizare, cu două fluxuri de lucru GitHub Actions, în `.github/workflows/`. Acestea sunt gata să ruleze imediat ce faci push al repozitoriului pe GitHub — nu este necesară nicio configurare suplimentară pentru CI, iar CD necesită doar un singur secret. + +### CI — `ci.yml` + +Rulează testele de integrare la fiecare push pe `main` și la fiecare pull request. + +**Ce face:** + +1. Preia codul sursă al aplicației. +2. Pornește o instanță de test Twenty izolată folosind acțiunea compozită `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (echivalentul din CI al `yarn twenty server start --test`). +3. Activează Corepack, configurează Node.js pe baza fișierului `.nvmrc` și instalează dependențele cu `yarn install --immutable`. +4. Rulează `yarn test`, transmitând `TWENTY_API_URL` și `TWENTY_API_KEY` din instanța pornită, astfel încât testele să poată comunica cu un server real. + +**Opțiuni de configurare:** + +* `TWENTY_VERSION` (variabilă de mediu, implicit `latest`) — fixează versiunea serverului Twenty folosită în CI editând acest parametru în `ci.yml`. +* Concurența este grupată după `github.ref` și anulează execuțiile în desfășurare la noile push-uri. + +Nu sunt necesare secrete — instanța de test este efemeră și există doar pe durata jobului. + +### CD — `cd.yml` + +Implementează aplicația pe un server Twenty configurat la fiecare push pe `main` și, opțional, dintr-un pull request când se aplică eticheta `deploy`. + +**Ce face:** + +1. Preia head-ul PR-ului (pentru PR-urile etichetate) sau commitul împins. +2. Rulează `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — echivalentul din CI al `yarn twenty deploy`. +3. Rulează `twentyhq/twenty/.github/actions/install-twenty-app@main` astfel încât versiunea nou implementată să fie instalată în spațiul de lucru țintă. + +**Configurare necesară:** + +| Setare | Unde | Scop | +| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `TWENTY_DEPLOY_URL` | `env` în `cd.yml` (implicit `http://localhost:3000`) | Serverul Twenty la care se face implementarea. Modifică-l la URL-ul real al serverului înainte de prima utilizare. | +| `TWENTY_DEPLOY_API_KEY` | GitHub repo **Settings → Secrets and variables → Actions** | Cheie API cu permisiune de implementare pe serverul țintă. | + + +Valoarea implicită a `TWENTY_DEPLOY_URL`, `http://localhost:3000`, este un placeholder — nu va putea accesa nimic dintr-un runner găzduit de GitHub. Actualizează-l la URL-ul public al serverului tău (sau folosește un runner self-hosted cu acces la rețea) înainte de a activa CD. + + +**Declanșarea unei implementări de previzualizare dintr-un PR:** + +Adaugă eticheta `deploy` la un pull request. Condiția `if:` din `cd.yml` va rula jobul pentru acel PR folosind commitul head al PR-ului, permițându-ți să validezi o modificare pe serverul țintă înainte de a face merge. + +### Fixarea acțiunilor reutilizabile + +Ambele fluxuri de lucru fac referire la acțiuni reutilizabile la `@main`, astfel încât actualizările acțiunilor din repo-ul `twentyhq/twenty` sunt preluate automat. Dacă dorești builduri deterministe, înlocuiește `@main` cu un SHA de commit sau cu un tag de release pe fiecare linie `uses:`. + +## Publicarea pe npm + +Publicarea pe npm face ca aplicația ta să poată fi descoperită în marketplace-ul Twenty. Orice spațiu de lucru Twenty poate răsfoi, instala și actualiza aplicațiile din marketplace direct din interfață. + +### Cerințe + +* Un cont [npm](https://www.npmjs.com) +* Cuvântul cheie `twenty-app` din array-ul `keywords` al fișierului `package.json` (adaugă-l manual — nu este inclus în mod implicit în șablonul `create-twenty-app`) + +```json filename="package.json" +{ + "name": "twenty-app-postcard-sender", + "version": "1.0.0", + "keywords": ["twenty-app"] +} +``` + +### Metadate pentru marketplace + +Configurația `defineApplication()` acceptă câmpuri opționale care controlează modul în care aplicația ta apare în marketplace. Folosește `logoUrl` și `screenshots` pentru a face referire la imaginile din folderul `public/`: + +```ts src/application-config.ts +export default defineApplication({ + universalIdentifier: '...', + displayName: 'My App', + description: 'A great app', + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + logoUrl: 'public/logo.png', + screenshots: [ + 'public/screenshot-1.png', + 'public/screenshot-2.png', + ], +}); +``` + +Vezi [acordeonul defineApplication](/l/ro/developers/extend/apps/config/application#marketplace-metadata) din pagina Building Apps pentru lista completă de câmpuri ale marketplace-ului (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.). + +#### Dimensiuni recomandate pentru capturi de ecran + +Marketplace-ul redă `screenshots` într-un container fix cu raport `8:5` (de exemplu, `1600×1000 px`). + + +Capturile de ecran cu orice raport de aspect sunt afișate integral și nu sunt niciodată decupate, însă orice este semnificativ mai înalt sau mai îngust decât `8:5` va afișa benzi goale pe laterale. + + +### Publicare + +```bash filename="Terminal" +yarn twenty publish +``` + +Pentru a publica sub un dist-tag specific (de ex., `beta` sau `next`): + +```bash filename="Terminal" +yarn twenty publish --tag beta +``` + +### Cum funcționează descoperirea în marketplace + +Serverul Twenty sincronizează catalogul marketplace-ului din registrul npm **la fiecare oră**. + +Poți declanșa sincronizarea imediat, în loc să aștepți: + +```bash filename="Terminal" +yarn twenty server catalog-sync +# To target a specific remote: +# yarn twenty server catalog-sync --remote production +``` + +Metadatele afișate în marketplace provin din configurația `defineApplication()` — câmpuri precum `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` și `termsUrl`. + + +Dacă aplicația ta nu definește un `aboutDescription` în `defineApplication()`, piața va folosi automat fișierul `README.md` al pachetului tău de pe npm drept conținut pentru pagina Despre. Acest lucru înseamnă că poți menține un singur README atât pentru npm, cât și pentru piața Twenty. Dacă vrei o descriere diferită în piață, setează explicit `aboutDescription`. + + +### Publicare CI + +Folosește acest workflow GitHub Actions pentru a publica automat la fiecare release (folosește [OIDC](https://docs.npmjs.com/trusted-publishers)): + +```yaml filename=".github/workflows/publish.yml" +name: Publish +on: + release: + types: [published] + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: https://registry.npmjs.org + - run: yarn install --immutable + - run: npx twenty build + - run: npm publish --provenance --access public + working-directory: .twenty/output +``` + +Pentru alte sisteme CI (GitLab CI, CircleCI etc.), se aplică aceleași trei comenzi: `yarn install`, `yarn twenty build`, apoi `npm publish` din `.twenty/output`. + + +**npm provenance** este opțională, dar recomandată. Publicarea cu `--provenance` adaugă un badge de încredere la listarea ta în npm, permițând utilizatorilor să verifice că pachetul a fost construit dintr-un commit specific într-un pipeline CI public. Vezi [documentația npm provenance](https://docs.npmjs.com/generating-provenance-statements) pentru instrucțiuni de configurare. + + +## Instalarea aplicațiilor + +După ce o aplicație este publicată (npm) sau implementată (tarball), spațiile de lucru o pot instala prin interfața utilizatorului (UI). + +Mergi la pagina **Setări > Aplicații** din Twenty, unde pot fi parcurse și instalate atât aplicațiile din marketplace, cât și cele implementate prin tarball. + +{/* TODO: add screenshot of the UI when the app is registered */} + +Poți instala aplicații și din linia de comandă: + +```bash filename="Terminal" +yarn twenty install +``` + + +Serverul impune versionarea semver la instalare, reflectând regulile de la deploy: + +* Instalarea aceleiași versiuni care este deja instalată în spațiul tău de lucru este respinsă cu o eroare `APP_ALREADY_INSTALLED`. +* Instalarea unei versiuni mai mici decât cea instalată în prezent este respinsă cu o eroare `CANNOT_DOWNGRADE_APPLICATION`. + +Pentru a instala o versiune mai nouă, fă mai întâi deploy sau public-o, apoi rulează din nou `yarn twenty install`. + diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/operations/testing.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/operations/testing.mdx new file mode 100644 index 0000000000..e221822524 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/operations/testing.mdx @@ -0,0 +1,301 @@ +--- +title: Testare +description: Vitest setup, integration tests against a real Twenty server, type checking, and CI with GitHub Actions. +icon: flask +--- + +SDK-ul oferă API-uri programatice care vă permit să construiți, să distribuiți, să instalați și să dezinstalați aplicația din codul de test. Combinat cu [Vitest](https://vitest.dev/) și clienții API tipizați, puteți scrie teste de integrare care verifică faptul că aplicația funcționează cap-coadă împotriva unui server Twenty real. + +## Utilizarea pachetelor npm + +Puteți instala și utiliza orice pachet npm în aplicația dvs. Atât funcțiile logice, cât și componentele frontend sunt împachetate cu [esbuild](https://esbuild.github.io/), care integrează toate dependențele în output — nu sunt necesare `node_modules` la rulare. + +### Instalarea unui pachet + +```bash filename="Terminal" +yarn add axios +``` + +Apoi importați-l în codul dvs.: + +```ts src/logic-functions/fetch-data.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import axios from 'axios'; + +const handler = async (): Promise => { + const { data } = await axios.get('https://api.example.com/data'); + + return { data }; +}; + +export default defineLogicFunction({ + universalIdentifier: '...', + name: 'fetch-data', + description: 'Fetches data from an external API', + timeoutSeconds: 10, + handler, +}); +``` + +Același lucru funcționează și pentru componentele frontend: + +```tsx src/front-components/chart.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { format } from 'date-fns'; + +const DateWidget = () => { + return

Today is {format(new Date(), 'MMMM do, yyyy')}

; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### Cum funcționează împachetarea + +Pasul de build folosește esbuild pentru a produce un singur fișier autonom pentru fiecare funcție logică și pentru fiecare componentă frontend. Toate pachetele importate sunt integrate în bundle. + +**Funcțiile logice** rulează într-un mediu Node.js. Modulele built-in Node (`fs`, `path`, `crypto`, `http` etc.) sunt disponibile și nu trebuie instalate. + +**Componentele frontend** rulează într-un Web Worker. Modulele built-in Node nu sunt disponibile — doar API-urile de browser și pachetele npm care funcționează într-un mediu de browser. + +Ambele medii au `twenty-client-sdk/core` și `twenty-client-sdk/metadata` disponibile ca module pre-furnizate — acestea nu sunt incluse în bundle, ci sunt rezolvate la rulare de către server. + +## Configurare + +Aplicația generată (scaffolded) include deja Vitest. Dacă o configurați manual, instalați dependențele: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Creați un `vitest.config.ts` în rădăcina aplicației: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Creați un fișier de configurare care verifică faptul că serverul este accesibil înainte de rularea testelor: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## API-uri SDK programatice + +Subruta `twenty-sdk/cli` exportă funcții pe care le puteți apela direct din codul de test: + +| Funcție | Descriere | +| -------------- | --------------------------------------------------------- | +| `appBuild` | Construiți aplicația și, opțional, împachetați un tarball | +| `appDeploy` | Încărcați un tarball pe server | +| `appInstall` | Instalați aplicația în spațiul de lucru activ | +| `appUninstall` | Dezinstalați aplicația din spațiul de lucru activ | + +Fiecare funcție returnează un obiect rezultat cu `success: boolean` și fie `data`, fie `error`. + +## Scrierea unui test de integrare + +Iată un exemplu complet care construiește, distribuie și instalează aplicația, apoi verifică faptul că aceasta apare în spațiul de lucru: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Rularea testelor + +Asigurați-vă că serverul Twenty local rulează, apoi: + +```bash filename="Terminal" +yarn test +``` + +Sau în modul watch în timpul dezvoltării: + +```bash filename="Terminal" +yarn test:watch +``` + +## Verificarea tipurilor + +Puteți rula și verificarea tipurilor pe aplicație fără a rula testele: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +Aceasta rulează `tsc --noEmit` și raportează orice erori de tip. + +## CI cu GitHub Actions + +Scaffolderul generează un workflow GitHub Actions gata de utilizare în `.github/workflows/ci.yml`. Rulează automat testele de integrare la fiecare push pe `main` și la pull request-uri. + +Workflow-ul: + +1. Preia codul +2. Pornește un server Twenty temporar folosind acțiunea `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` +3. Instalează dependențele cu `yarn install --immutable` +4. Rulează `yarn test` cu `TWENTY_API_URL` și `TWENTY_API_KEY` injectate din rezultatele acțiunii + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +Nu trebuie să configurați niciun secret — acțiunea `spawn-twenty-docker-image` pornește un server Twenty efemer direct în runner și oferă detaliile de conectare. Secretul `GITHUB_TOKEN` este furnizat automat de GitHub. + +Pentru a fixa o versiune Twenty specifică în loc de `latest`, modificați variabila de mediu `TWENTY_VERSION` din partea de sus a workflow-ului. diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/config/application.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..46af2e8c9d --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/config/application.mdx @@ -0,0 +1,65 @@ +--- +title: Application Config +description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. +icon: rocket +--- + +Every app must have exactly one `defineApplication` call. It declares: + +* **Identity** — universal identifier, display name, description. +* **Permissions** — which role its logic functions and front components run under. +* **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. +* **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/l/ru/developers/extend/apps/logic/logic-functions). + +```ts src/application-config.ts +import { defineApplication } from 'twenty-sdk/define'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; + +export default defineApplication({ + universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', + displayName: 'My Twenty App', + description: 'My first Twenty app', + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', + description: 'Default recipient name for postcards', + value: 'Jane Doe', + isSecret: false, + }, + }, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). +* `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/l/ru/developers/extend/apps/config/roles). +* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. + +## Default function role + +The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: + +* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. +* The typed API client is restricted to the permissions granted to that role. +* Follow least-privilege: declare only the permissions your functions need. + +When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/l/ru/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/l/ru/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `author` | Author or company name | +| `category` | App category for marketplace filtering | +| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) | +| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) | +| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | +| `websiteUrl` | Link to your website | +| `termsUrl` | Link to terms of service | +| `emailSupport` | Support email address | +| `issueReportUrl` | Link to issue tracker | diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..38efdbe1e9 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/config/install-hooks.mdx @@ -0,0 +1,206 @@ +--- +title: Install Hooks +description: Run logic before or after the install — seed data, back up records, validate the upgrade. +icon: wrench +--- + +Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/l/ru/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload`, but they're declared with their own define functions — `definePostInstallLogicFunction()` and `definePreInstallLogicFunction()` — and live outside the normal trigger model (HTTP, cron, database events). + +Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build will error if more than one of either is detected. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ install flow │ +│ │ +│ upload package → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +│ │ +│ old schema visible new schema visible │ +└─────────────────────────────────────────────────────────────┘ +``` + + + + +A post-install function runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services. + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Post install logic function executed successfully!', payload.previousVersion); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Runs after installation to set up the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + shouldRunSynchronously: false, + handler, +}); +``` + +You can also manually execute the post-install function at any time using the CLI: + +```bash filename="Terminal" +yarn twenty exec --postInstall +``` + +Key points: +* Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`). +* The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic. +* **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook. +* **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed. + * `shouldRunSynchronously: false` *(default)* — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window. + * `shouldRunSynchronously: true` — the hook is executed **inline during the install flow** (same executor as pre-install). The install request blocks until the handler finishes, and if it throws, the install caller receives a `POST_INSTALL_ERROR`. No automatic retries. **Use this for fast, must-complete-before-response work** — for example, emitting a validation error to the user, or quick setup that the client will rely on immediately after the install call returns. Keep in mind the metadata migration has already been applied by the time post-install runs, so a sync-mode failure does **not** roll back the schema changes — it only surfaces the error. +* Make sure your handler is idempotent. In async mode the queue may retry up to three times; in either mode the hook may run again on upgrades when `shouldRunOnVersionUpgrade: true`. +* The environment variables `APPLICATION_ID`, `APP_ACCESS_TOKEN`, and `API_URL` are available inside the handler (same as any other logic function), so you can call the Twenty API with an application access token scoped to your app. +* Only one post-install function is allowed per application. The manifest build will error if more than one is detected. +* The function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in [`defineApplication()`](/l/ru/developers/extend/apps/config/application). +* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding. +* **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` to trigger it manually against a running workspace. + + + + +A pre-install function runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped. + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Pre install logic function executed successfully!', payload.previousVersion); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Runs before installation to prepare the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +You can also manually execute the pre-install function at any time using the CLI: + +```bash filename="Terminal" +yarn twenty exec --preInstall +``` + +Key points: +* Pre-install functions use `definePreInstallLogicFunction()` — same specialized config as post-install, just attached to a different lifecycle slot. +* Both pre- and post-install handlers receive the same `InstallPayload` type: `{ previousVersion?: string; newVersion: string }`. Import it once and reuse it for both hooks. +* **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state. +* **Execution model**: pre-install is executed **synchronously** and **blocks the install**. If the handler throws, the install is aborted before any schema changes are applied — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade. +* As with post-install, only one pre-install function is allowed per application. It is attached to the application manifest under `preInstallLogicFunction` automatically during the build. +* **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty exec --preInstall` to trigger it manually. + + + + +Both hooks are part of the same install flow and receive the same `InstallPayload`. The difference is **when** they run relative to the workspace metadata migration, and that changes what data they can safely touch. + +Pre-install is always **synchronous** (it blocks the install and can abort it). Post-install is **asynchronous by default** — enqueued on a worker with automatic retries — but can opt into synchronous execution with `shouldRunSynchronously: true`. See the `definePostInstallLogicFunction` accordion above for when to use each mode. + +**Use `post-install` for anything that needs the new schema to exist.** This is the common case: + +* Seeding default data (creating initial records, default views, demo content) against newly-added objects and fields. +* Registering webhooks with third-party services now that the app has its credentials. +* Calling your own API to finish setup that depends on the synchronized metadata. +* Idempotent "ensure this exists" logic that should reconcile state on every upgrade — combine with `shouldRunOnVersionUpgrade: true`. + +Example — seed a default `PostCard` record after install: + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion }: InstallPayload): Promise => { + if (previousVersion) return; // fresh installs only + + const client = createClient(); + await client.postCard.create({ + data: { title: 'Welcome to Postcard', content: 'Your first card!' }, + }); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Seeds a welcome post card after install.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + handler, +}); +``` + +**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the *previous* schema and its failure rolls back the upgrade, it is the right place for anything risky: + +* **Backing up data that is about to be dropped or restructured** — e.g. you are removing a field in v2 and need to copy its values into another field or export them to storage before the migration runs. +* **Archiving records that a new constraint would invalidate** — e.g. a field is becoming `NOT NULL` and you need to delete or fix rows with null values first. +* **Validating compatibility and refusing the upgrade if the current data cannot be migrated cleanly** — throw from the handler and the install aborts with no changes applied. This is safer than discovering the incompatibility mid-migration. +* **Renaming or rekeying data** ahead of a schema change that would lose the association. + +Example — archive records before a destructive migration: + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise => { + // Only the 1.x → 2.x upgrade drops the legacy `notes` field. + if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) { + return; + } + + const client = createClient(); + const legacyRecords = await client.postCard.findMany({ + where: { notes: { isNotNull: true } }, + }); + + if (legacyRecords.length === 0) return; + + // Copy legacy `notes` into the new `description` field before the migration + // drops the `notes` column. If this fails, the upgrade is aborted and the + // workspace stays on v1 with all data intact. + await Promise.all( + legacyRecords.map((record) => + client.postCard.update({ + where: { id: record.id }, + data: { description: record.notes }, + }), + ), + ); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Backs up legacy notes into description before the v2 migration.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +**Rule of thumb:** + +| You want to... | Use | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Seed default data, configure the workspace, register external resources | `post-install` | +| Run long-running seeding or third-party calls that shouldn't block the install response | `post-install` (default — `shouldRunSynchronously: false`, with worker retries) | +| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` | +| Read or back up data that the upcoming migration would lose | `pre-install` | +| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) | +| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` | +| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) | + + +If in doubt, default to **post-install**. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone. + + + + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/config/overview.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/config/overview.mdx new file mode 100644 index 0000000000..748af11382 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/config/overview.mdx @@ -0,0 +1,51 @@ +--- +title: Overview +description: Configure the app itself — its identity, default permissions, and what runs at install time. +icon: screwdriver-wrench +--- + +A Twenty app's **config layer** is what describes the app *to the platform* — its identity, the permissions it holds, and the code that runs during install or upgrade. These declarations don't add new data shapes or runtime behavior; they tell Twenty *who the app is* and *how to set it up*. + +```text +┌────────────────────────────────────────────────────────┐ +│ Application — identity, default role, variables, │ +│ marketplace metadata │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Role — what the app's logic functions can read │ │ +│ │ and write (referenced by Application) │ │ +│ └──────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────┘ + │ + ▼ (at install / upgrade time) + ┌──────────────────────────────────┐ + │ Pre-install hook │ before metadata migration + └──────────────────────────────────┘ + ┌──────────────────────────────────┐ + │ Post-install hook │ after metadata migration + └──────────────────────────────────┘ +``` + +## In this section + + + + `defineApplication` — identity, default role, variables, marketplace metadata. + + + `defineRole` — declare what your app's logic functions can read and write. + + + `definePreInstallLogicFunction` and `definePostInstallLogicFunction` — back up data, seed defaults, validate upgrades. + + + +## How the pieces relate + +* **Application** is the entry point. Every app has exactly one `defineApplication()` call, and it points at one **Role** as its default. +* The **Role** controls what the app's logic functions and front components can read and write. Follow least-privilege: only grant the permissions your code actually needs. +* **Install Hooks** run during install or upgrade — pre-install before the metadata migration (so it can refuse a risky upgrade), post-install after the migration (so it can seed default data against the new schema). + + +Install hooks share the [logic function](/l/ru/developers/extend/apps/logic/logic-functions) runtime — same handler signature, same environment variables, same typed API client — but they're declared with their own define functions and live outside the regular trigger model (HTTP, cron, database events). + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/config/public-assets.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/config/public-assets.mdx new file mode 100644 index 0000000000..338c0f7f8e --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/config/public-assets.mdx @@ -0,0 +1,59 @@ +--- +title: Public Assets +description: Ship static files — images, icons, fonts — alongside your app via the public/ folder. +icon: folder-open +--- + +The `public/` folder at the root of your app holds static files — images, icons, fonts, or any other assets your app needs at runtime. These files are automatically included in builds, synced during dev mode, and uploaded to the server. + +Files placed in `public/` are: + +* **Publicly accessible** — once synced to the server, assets are served at a public URL. No authentication is needed to access them. +* **Available in front components** — use asset URLs to display images, icons, or any media inside your React components. +* **Available in logic functions** — reference asset URLs in emails, API responses, or any server-side logic. +* **Used for marketplace metadata** — the `logoUrl` and `screenshots` fields in `defineApplication()` reference files from this folder (e.g., `public/logo.png`). These are displayed in the marketplace when your app is published. +* **Auto-synced in dev mode** — when you add, update, or delete a file in `public/`, it is synced to the server automatically. No restart needed. +* **Included in builds** — `yarn twenty build` bundles all public assets into the distribution output. + +## Accessing public assets with `getPublicAssetUrl` + +Use the `getPublicAssetUrl` helper from `twenty-sdk` to get the full URL of a file in your `public/` directory. It works in both **logic functions** and **front components**. + +**In a logic function:** + +```ts src/logic-functions/send-invoice.ts +import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define'; + +const handler = async (): Promise => { + const logoUrl = getPublicAssetUrl('logo.png'); + const invoiceUrl = getPublicAssetUrl('templates/invoice.png'); + + // Fetch the file content (no auth required — public endpoint) + const response = await fetch(invoiceUrl); + const buffer = await response.arrayBuffer(); + + return { logoUrl, size: buffer.byteLength }; +}; + +export default defineLogicFunction({ + universalIdentifier: 'a1b2c3d4-...', + name: 'send-invoice', + description: 'Sends an invoice with the app logo', + timeoutSeconds: 10, + handler, +}); +``` + +**In a front component:** + +```tsx src/front-components/company-card.tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +export default defineFrontComponent(() => { + const logoUrl = getPublicAssetUrl('logo.png'); + + return App logo; +}); +``` + +The `path` argument is relative to your app's `public/` folder. Both `getPublicAssetUrl('logo.png')` and `getPublicAssetUrl('public/logo.png')` resolve to the same URL — the `public/` prefix is stripped automatically if present. diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/config/roles.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/config/roles.mdx new file mode 100644 index 0000000000..db2198ef7b --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/config/roles.mdx @@ -0,0 +1,90 @@ +--- +title: Roles & Permissions +description: Declare what objects and fields your app's logic functions and front components can read and write. +icon: shield-halved +--- + +A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role declared as `defaultRoleUniversalIdentifier` in [`defineApplication`](/l/ru/developers/extend/apps/config/application). + +```ts src/roles/restricted-company-role.ts +import { + defineRole, + PermissionFlag, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; + +export default defineRole({ + universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6', + label: 'My new role', + description: 'A role that can be used in your workspace', + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + fieldPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + fieldUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier, + canReadFieldValue: false, + canUpdateFieldValue: false, + }, + ], + permissionFlags: [PermissionFlag.APPLICATIONS], +}); +``` + +## The default function role + +When you scaffold a new app, the CLI creates a default role file: + +```ts src/roles/default-role.ts +import { defineRole, PermissionFlag } from 'twenty-sdk/define'; + +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = + 'b648f87b-1d26-4961-b974-0908fd991061'; + +export default defineRole({ + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + label: 'Default function role', + description: 'Default role for function Twenty client', + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [], + fieldPermissions: [], + permissionFlags: [], +}); +``` + +This role's `universalIdentifier` is referenced from `application-config.ts` as `defaultRoleUniversalIdentifier`: + +* **`*.role.ts`** declares what the role can do. +* **`application-config.ts`** points to that role so your functions inherit its permissions. + +## Best practices + +* Start from the scaffolded role, then progressively restrict it — the default grants broad read access, which is rarely what you want in production. +* Replace `objectPermissions` and `fieldPermissions` with the exact objects and fields your functions actually need. +* `permissionFlags` control access to platform-level capabilities. Keep them minimal. +* See a working example: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts). diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/data/extending-objects.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/data/extending-objects.mdx new file mode 100644 index 0000000000..4428de4654 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/data/extending-objects.mdx @@ -0,0 +1,48 @@ +--- +title: Extending Objects +description: Add fields to standard Twenty objects (Person, Company, …) or to objects from other apps using defineField. +icon: wand-magic-sparkles +--- + +Use `defineField()` to add a field to an object you don't own — a standard Twenty object like Person or Company, or an object shipped by another installed app. Unlike inline fields declared inside [`defineObject`](/l/ru/developers/extend/apps/data/objects), standalone fields require an `objectUniversalIdentifier` to specify which object they extend. + +```ts src/fields/company-loyalty-tier.field.ts +import { defineField, FieldType } from 'twenty-sdk/define'; + +export default defineField({ + universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890', + objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object + name: 'loyaltyTier', + type: FieldType.SELECT, + label: 'Loyalty Tier', + icon: 'IconStar', + options: [ + { value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' }, + { value: 'SILVER', label: 'Silver', position: 1, color: 'gray' }, + { value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' }, + ], +}); +``` + +## Key points + +* `objectUniversalIdentifier` identifies the target object. For standard Twenty objects, import the constant from `twenty-sdk`: + + ```ts + import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define'; + + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier + // … + ``` + +* When defining fields **inline inside `defineObject()`**, you do **not** need `objectUniversalIdentifier` — it's inherited from the parent object. + +* `defineField()` is the only way to add fields to objects you didn't create with `defineObject()`. + +* File location is up to you. The convention is `src/fields/\.field.ts`, but the SDK detects fields anywhere in `src/`. + +## Adding a relation to an existing object + +To add a relation field (e.g. linking your custom object to a standard `Person`), use `defineField()` with `FieldType.RELATION`. The pattern is the same as for inline relations but with `objectUniversalIdentifier` set explicitly. See [Relations](/l/ru/developers/extend/apps/data/relations) for the bidirectional pattern. diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/data/objects.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/data/objects.mdx new file mode 100644 index 0000000000..799e883e8c --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/data/objects.mdx @@ -0,0 +1,93 @@ +--- +title: Объекты +description: Declare new record types — custom tables with their own fields — using defineObject. +icon: таблица +--- + +Custom **objects** are new record types your app adds to a workspace — Post Card, Invoice, Subscription, anything specific to your domain. Each object declares its schema (fields, relations, default values) and a stable universal identifier that survives across syncs and deploys. + +```ts src/objects/post-card.object.ts +import { defineObject, FieldType } from 'twenty-sdk/define'; + +enum PostCardStatus { + DRAFT = 'DRAFT', + SENT = 'SENT', + DELIVERED = 'DELIVERED', + RETURNED = 'RETURNED', +} + +export default defineObject({ + universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', + nameSingular: 'postCard', + namePlural: 'postCards', + labelSingular: 'Post Card', + labelPlural: 'Post Cards', + description: 'A post card object', + icon: 'IconMail', + fields: [ + { + universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b', + name: 'content', + type: FieldType.TEXT, + label: 'Content', + description: "Postcard's content", + icon: 'IconAbc', + }, + { + universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac', + name: 'recipientName', + type: FieldType.FULL_NAME, + label: 'Recipient name', + icon: 'IconUser', + }, + { + universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266', + name: 'recipientAddress', + type: FieldType.ADDRESS, + label: 'Recipient address', + icon: 'IconHome', + }, + { + universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e', + name: 'status', + type: FieldType.SELECT, + label: 'Status', + icon: 'IconSend', + defaultValue: `'${PostCardStatus.DRAFT}'`, + options: [ + { value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' }, + { value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' }, + { value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' }, + { value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' }, + ], + }, + { + universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433', + name: 'deliveredAt', + type: FieldType.DATE_TIME, + label: 'Delivered at', + icon: 'IconCheck', + isNullable: true, + defaultValue: null, + }, + ], +}); +``` + +## Основные моменты + +* `universalIdentifier` должен быть уникальным и стабильным между развёртываниями. +* Каждому полю требуются `name`, `type`, `label` и собственный стабильный `universalIdentifier`. +* Массив `fields` необязателен — вы можете определять объекты без пользовательских полей. +* Inline fields defined here do **not** need an `objectUniversalIdentifier` — it's inherited from the parent object. Use [`defineField()`](/l/ru/developers/extend/apps/data/extending-objects) to add fields to objects you don't own. +* You can scaffold new objects with `yarn twenty add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/l/ru/developers/extend/apps/getting-started/scaffolding). + + +**Base fields are added automatically.** When you define a custom object, Twenty creates standard fields like `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and `deletedAt` for you. You don't need to declare them in your `fields` array — only your custom fields. You can override a default field by declaring one with the same name, but this is rarely a good idea. + + +## Что дальше + +* **Connect this object to others** — see [Relations](/l/ru/developers/extend/apps/data/relations) for the bidirectional relation pattern. +* **Add fields to objects from other apps** — see [Extending Objects](/l/ru/developers/extend/apps/data/extending-objects) for `defineField()`. +* **Display this object in the UI** — see [Views](/l/ru/developers/extend/apps/layout/views) and [Navigation Menu Items](/l/ru/developers/extend/apps/layout/navigation-menu-items) to put it in the sidebar. diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/data/overview.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/data/overview.mdx new file mode 100644 index 0000000000..ddf15230c0 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/data/overview.mdx @@ -0,0 +1,52 @@ +--- +title: Overview +description: Shape the data your app adds to a workspace — objects, fields, and relations. +icon: database +--- + +A Twenty app's **data layer** is the data your app *adds* to a workspace — the new record types it declares, the columns it adds to existing objects, and how those records connect to each other. + +```text +┌──────────────────────────────────────────────────┐ +│ Object — a record type, e.g. PostCard │ +│ ├─ Field (name, type, label) │ +│ ├─ Field │ +│ └─ Relation (link to another object) │ +└──────────────────────────────────────────────────┘ + │ + ├── lives in your app, OR + │ + ▼ +┌──────────────────────────────────────────────────┐ +│ Standard / other apps' objects │ +│ └─ Field added by your app via defineField │ +└──────────────────────────────────────────────────┘ +``` + +## In this section + + + + `defineObject` — declare new record types with their own fields. + + + `defineField` — add fields to standard or other apps' objects. + + + Bidirectional `MANY_TO_ONE` / `ONE_TO_MANY` connections between objects. + + + +## Entities at a glance + +| Entity | Purpose | Defined with | +| ------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| **Object** | A new custom record type (e.g. PostCard, Invoice) with its own fields | `defineObject()` | +| **Field** | A column on an object. Standalone fields can extend objects you didn't create (e.g. add `loyaltyTier` to Company) | `defineField()` | +| **Relation** | A bidirectional link between two objects — both sides declared as fields | `defineField()` with `FieldType.RELATION` | + +The SDK detects these via AST analysis at build time, so file organization is up to you — the convention is `src/objects/` and `src/fields/`. Stable `universalIdentifier` UUIDs tie everything together across deploys. + + +Looking for **Application Config** or **Roles & Permissions**? Those describe the app itself rather than the data it adds — they live under [Config](/l/ru/developers/extend/apps/config/overview). Looking for **Connections** (Linear, GitHub, Slack OAuth)? Those exist to be called *from* logic functions and live under [Logic](/l/ru/developers/extend/apps/logic/connections). + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/data/relations.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/data/relations.mdx new file mode 100644 index 0000000000..4d33e46127 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/data/relations.mdx @@ -0,0 +1,160 @@ +--- +title: Связи +description: Connect objects together with bidirectional MANY_TO_ONE / ONE_TO_MANY relations. +icon: diagram-project +--- + +Relations connect two objects together. In Twenty, relations are always **bidirectional** — every relation has two sides, and each side is declared as a field that references the other. + +| Тип отношения | Описание | Есть внешний ключ? | +| ------------- | --------------------------------------------------------------------- | --------------------- | +| `MANY_TO_ONE` | Многие записи этого объекта указывают на одну запись целевого объекта | Да (`joinColumnName`) | +| `ONE_TO_MANY` | Одна запись этого объекта имеет много записей целевого объекта | No (the inverse side) | + +## How relations work + +Every relation requires **two fields** that reference each other: + +1. The **MANY_TO_ONE** side — lives on the object that holds the foreign key. +2. The **ONE_TO_MANY** side — lives on the object that owns the collection. + +Both fields use `FieldType.RELATION` and cross-reference each other via `relationTargetFieldMetadataUniversalIdentifier`. + +## Example: Post Card has many Recipients + +A `PostCard` can be sent to many `PostCardRecipient` records. Each recipient belongs to exactly one post card. + +**Step 1: Define the ONE_TO_MANY side on PostCard** (the "one" side): + +```ts src/fields/post-card-recipients-on-post-card.field.ts +import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111'; +// Import from the other side +import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field'; + +export default defineField({ + universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCardRecipients', + label: 'Post Card Recipients', + icon: 'IconUsers', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, +}); +``` + +**Step 2: Define the MANY_TO_ONE side on PostCardRecipient** (the "many" side — holds the foreign key): + +```ts src/fields/post-card-on-post-card-recipient.field.ts +import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222'; +// Import from the other side +import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field'; + +export default defineField({ + universalIdentifier: POST_CARD_FIELD_ID, + objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + icon: 'IconMail', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, +}); +``` + + +**Circular imports:** both relation fields reference each other's `universalIdentifier`. To avoid circular import issues, export your field IDs as named constants from each file and import them in the other. The build system resolves these at compile time. + + +## Relating to standard objects + +To create a relation with a built-in Twenty object (Person, Company, etc.), use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`: + +```ts src/fields/person-on-self-hosting-user.field.ts +import { + defineField, + FieldType, + RelationType, + OnDeleteAction, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; +import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object'; + +export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333'; +export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444'; + +export default defineField({ + universalIdentifier: PERSON_FIELD_ID, + objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'person', + label: 'Person', + description: 'Person matching with the self hosting user', + isNullable: true, + relationTargetObjectMetadataUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, + relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.SET_NULL, + joinColumnName: 'personId', + }, +}); +``` + +## Relation field properties + +| Property | Required | Description | +| ------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------- | +| `type` | Yes | Must be `FieldType.RELATION` | +| `relationTargetObjectMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the target object | +| `relationTargetFieldMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the matching field on the target object | +| `universalSettings.relationType` | Yes | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` | +| `universalSettings.onDelete` | MANY_TO_ONE only | What happens when the referenced record is deleted: `CASCADE`, `SET_NULL`, `RESTRICT`, or `NO_ACTION` | +| `universalSettings.joinColumnName` | MANY_TO_ONE only | Database column name for the foreign key (e.g., `postCardId`) | + +## Inline relation fields + +You can also declare a relation directly inside [`defineObject`](/l/ru/developers/extend/apps/data/objects). When inline, omit `objectUniversalIdentifier` — it's inherited from the parent object: + +```ts +export default defineObject({ + universalIdentifier: '...', + nameSingular: 'postCardRecipient', + // ... + fields: [ + { + universalIdentifier: POST_CARD_FIELD_ID, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, + }, + // … other fields + ], +}); +``` diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/concepts.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/concepts.mdx new file mode 100644 index 0000000000..34977d57ab --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/concepts.mdx @@ -0,0 +1,101 @@ +--- +title: Concepts +description: How Twenty apps work — entity model, sandboxing, and the install lifecycle. +icon: sitemap +--- + +Twenty apps are TypeScript packages that extend your workspace with custom objects, logic, UI components, and AI capabilities. They run on the Twenty platform with full sandboxing and permission controls. + +## How apps work + +An app is a collection of **entities** declared using `defineEntity()` functions from the `twenty-sdk` package. The SDK detects these declarations via AST analysis at build time and produces a **manifest** — a complete description of what your app adds to a workspace. These functions validate your configuration at build time and provide IDE autocompletion and type safety. + +``` +your-app/ +├── src/ +│ ├── application-config.ts ← defineApplication (required, one per app) +│ ├── roles/ ← defineRole +│ ├── objects/ ← defineObject +│ ├── fields/ ← defineField +│ ├── logic-functions/ ← defineLogicFunction +│ ├── front-components/ ← defineFrontComponent +│ ├── skills/ ← defineSkill +│ ├── agents/ ← defineAgent +│ ├── views/ ← defineView +│ ├── navigation-menu-items/ ← defineNavigationMenuItem +│ └── page-layouts/ ← definePageLayout +├── public/ ← Static assets (images, icons) +└── package.json +``` + + + **File organization is up to you.** Entity detection is AST-based — the SDK finds `export default defineEntity(...)` calls regardless of where the file lives. The folder structure above is a convention, not a requirement. + + +## Entity types + +| Entity | Purpose | Docs | +| ------------------------ | ------------------------------------------ | ----------------------------------------------------------------------------- | +| **Application** | App identity, default role, variables | [Application Config](/l/ru/developers/extend/apps/config/application) | +| **Role** | Permission sets on objects and fields | [Roles & Permissions](/l/ru/developers/extend/apps/config/roles) | +| **Object** | Custom record types with fields | [Objects](/l/ru/developers/extend/apps/data/objects) | +| **Field** | Add fields to objects from other apps | [Extending Objects](/l/ru/developers/extend/apps/data/extending-objects) | +| **Relation** | Bidirectional links between objects | [Relations](/l/ru/developers/extend/apps/data/relations) | +| **Logic Function** | Server-side TypeScript with triggers | [Logic Functions](/l/ru/developers/extend/apps/logic/logic-functions) | +| **Skill** | Reusable AI agent instructions | [Skills & Agents](/l/ru/developers/extend/apps/logic/skills-and-agents) | +| **Agent** | AI assistants with custom prompts | [Skills & Agents](/l/ru/developers/extend/apps/logic/skills-and-agents) | +| **Connection Provider** | OAuth credentials for third-party APIs | [Connections](/l/ru/developers/extend/apps/logic/connections) | +| **View** | Pre-configured record list views | [Views](/l/ru/developers/extend/apps/layout/views) | +| **Navigation Menu Item** | Custom sidebar entries | [Navigation Menu Items](/l/ru/developers/extend/apps/layout/navigation-menu-items) | +| **Page Layout** | Tabs and widgets on a record's detail page | [Page Layouts](/l/ru/developers/extend/apps/layout/page-layouts) | +| **Front Component** | Sandboxed React UI inside Twenty | [Front Components](/l/ru/developers/extend/apps/layout/front-components) | +| **Command Menu Item** | Quick actions and Cmd+K entries | [Command Menu Items](/l/ru/developers/extend/apps/layout/command-menu-items) | + +## Sandboxing + +* **Logic functions** run in isolated Node.js processes on the server. They only access data through the typed API client, scoped to the app's role permissions. +* **Front components** run in Web Workers using Remote DOM — sandboxed from the main page but rendering native DOM elements (not iframes). They communicate with Twenty via a message-passing host API. +* **Permissions** are enforced at the API level. The runtime token (`TWENTY_APP_ACCESS_TOKEN`) is derived from the role defined in `defineApplication()`. + +## App lifecycle + +``` +┌─────────────────────────────────────────────────────────┐ +│ Development │ +│ npx create-twenty-app → yarn twenty dev (live sync) │ +├─────────────────────────────────────────────────────────┤ +│ Build & Deploy │ +│ yarn twenty build → yarn twenty deploy │ +├─────────────────────────────────────────────────────────┤ +│ Install flow │ +│ upload → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +├─────────────────────────────────────────────────────────┤ +│ Publish │ +│ npm publish → appears in Twenty marketplace │ +└─────────────────────────────────────────────────────────┘ +``` + +* **`yarn twenty dev`** — watches your source files and live-syncs changes to a connected Twenty server. The typed API client is regenerated automatically when the schema changes. +* **`yarn twenty build`** — compiles TypeScript, bundles logic functions and front components with esbuild, and produces a manifest. +* **Pre/post-install hooks** — optional functions that run during installation. See [Install Hooks](/l/ru/developers/extend/apps/config/install-hooks) for details. + +## Next steps + + + + Application identity, default role, and install hooks. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/local-server.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/local-server.mdx new file mode 100644 index 0000000000..a424e5203a --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/local-server.mdx @@ -0,0 +1,72 @@ +--- +title: Local Server +description: Manage the local Twenty Docker server — start, stop, upgrade, parallel test instance, and manual SDK setup. +icon: server +--- + +## Managing the local server + +Use `yarn twenty server` to control the local Twenty container: + +| Command | What it does | +| -------------------------------------- | -------------------------------------------- | +| `yarn twenty server start` | Start the server (pulls the image if needed) | +| `yarn twenty server start --port 3030` | Start on a custom port | +| `yarn twenty server stop` | Stop the server (preserves data) | +| `yarn twenty server status` | Show URL, version, and login credentials | +| `yarn twenty server logs` | Stream server logs | +| `yarn twenty server reset` | Wipe data and start fresh | +| `yarn twenty server upgrade` | Pull the latest `twenty-app-dev` image | +| `yarn twenty server upgrade 2.2.0` | Upgrade to a specific version | + +Data persists across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything. + +## Upgrading the server image + +`yarn twenty server upgrade` pulls the latest image, compares digests, and only recreates the container if anything actually changed. Volumes are preserved — only the container is replaced. If a new image was pulled and the container was running, the upgrade automatically starts a new container; run `yarn twenty server start` afterward to wait for it to become healthy. + +```bash filename="Terminal" +yarn twenty server upgrade # Latest +yarn twenty server upgrade 2.2.0 # Specific version +``` + +Verify the running version with `yarn twenty server status` (it shows the `APP_VERSION` baked into the container). + +## Running a parallel test instance + +Pass `--test` to any `server` command to manage a second, fully isolated instance — useful for integration tests or experiments without touching your main dev data: + +| Command | What it does | +| ----------------------------------- | ----------------------------------------------- | +| `yarn twenty server start --test` | Start the test instance (defaults to port 2021) | +| `yarn twenty server stop --test` | Stop it | +| `yarn twenty server status --test` | Show its status | +| `yarn twenty server logs --test` | Stream its logs | +| `yarn twenty server reset --test` | Wipe its data | +| `yarn twenty server upgrade --test` | Upgrade its image | + +The test instance has its own container (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`), and config — it runs alongside your main instance without conflicts. Combine `--test` with `--port` to override 2021. + +## Manual setup (without the scaffolder) + +Skip the scaffolder if you're adding the SDK to an existing project: + +```bash filename="Terminal" +yarn add twenty-sdk twenty-client-sdk +``` + +Add the script to `package.json`: + +```json filename="package.json" +{ + "scripts": { + "twenty": "twenty" + } +} +``` + +You can now run `yarn twenty dev`, `yarn twenty server start`, and the rest. + + +Don't install `twenty-sdk` globally — pin it per project so each app uses its own version. + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/project-structure.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/project-structure.mdx new file mode 100644 index 0000000000..ad85a70452 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/project-structure.mdx @@ -0,0 +1,40 @@ +--- +title: Project Structure +description: What's inside a scaffolded Twenty app — files, folders, and what each one does. +icon: folder-tree +--- + +A new app generated by `npx create-twenty-app` looks like this: + +```text filename="my-twenty-app/" +my-twenty-app/ + package.json + src/ + application-config.ts # Required — your app's entry point + default-role.ts # Permissions for logic functions + constants/ + universal-identifiers.ts # Auto-generated UUIDs and metadata + __tests__/ + setup-test.ts + app-install.integration-test.ts + .github/workflows/ci.yml # GitHub Actions + public/ # Static assets + vitest.config.ts # Test runner config + tsconfig.json, tsconfig.spec.json + .nvmrc, .yarnrc.yml, .oxlintrc.json + README.md, LLMS.md +``` + +## Key files + +| File / Folder | Purpose | +| ---------------------------------------- | -------------------------------------------------------------- | +| `src/application-config.ts` | **Required.** The main configuration file for your app. | +| `src/default-role.ts` | Default role controlling what your logic functions can access. | +| `src/constants/universal-identifiers.ts` | Auto-generated UUIDs and metadata (display name, description). | +| `src/__tests__/` | Integration tests (setup + example test). | +| `public/` | Static assets (images, fonts) served with your app. | + + +**File organization is up to you.** The folders above are conventions — the SDK detects entities via AST analysis on `export default defineEntity(...)` calls regardless of where the file lives. + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/quick-start.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/quick-start.mdx new file mode 100644 index 0000000000..437f5187f4 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/quick-start.mdx @@ -0,0 +1,184 @@ +--- +title: Quick Start +icon: rocket +description: Create your first Twenty app in minutes. +--- + +## Prerequisites + +* **Node.js 24+** — [Download](https://nodejs.org/) +* **Yarn 4** — bundled with Node via Corepack. Enable it: `corepack enable` +* **Docker** — [Download](https://www.docker.com/products/docker-desktop/). Needed to run a local Twenty server. Skip if you already have Twenty running elsewhere. + +Building a Twenty app has three phases. The scaffolder collapses them into one happy-path command, but each phase is a separate concept — when something fails, knowing which phase you're in tells you what to fix. + +| Phase | What you do | Tool | Result | +| ------------------- | ---------------------------------- | ----------------------------- | ----------------------------- | +| **1. Scaffold** | Generate the app's source code | `npx create-twenty-app` | A TypeScript project on disk | +| **2. Run a server** | Start a Twenty server to sync into | Docker + `yarn twenty server` | A running Twenty instance | +| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` | Your changes appear in the UI | + +--- + +## Phase 1 — Scaffold your project + +Create a new app from the template: + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app +``` + +You'll be prompted for a name and description — press **Enter** for the defaults. This generates a TypeScript project in `my-twenty-app/` with a starter `application-config.ts`, a default role, a CI workflow, and an integration test. + +**After this phase:** you have an app's source code on your machine. It isn't running yet — that's Phase 2. + +--- + +## Phase 2 — Run a local Twenty server + +Your app needs a Twenty server to sync into. The server is a full Twenty instance — UI, GraphQL API, PostgreSQL — running locally in Docker. Your local code uploads its definitions to that server, which makes them appear in the UI. + +The scaffolder offers to start one for you: + +> **Would you like to set up a local Twenty instance?** + +* **Yes (recommended)** — pulls the `twentycrm/twenty-app-dev` Docker image and starts it on port `2020`. Make sure Docker is running first. +* **No** — choose this if you already have a Twenty server you want to connect to. You can wire it up later with `yarn twenty remote add`. + +
+ Should start local instance? +
+ +Once the server is up, a browser opens for sign-in. Use the pre-seeded demo account: + +* **Email:** `tim@apple.dev` +* **Password:** `tim@apple.dev` + +
+ Twenty login screen +
+ +Click **Authorize** on the next screen — this gives the CLI access to your workspace. + +
+ Twenty CLI authorization screen +
+ +Your terminal will confirm everything is set up. + +
+ App scaffolded successfully +
+ +**After this phase:** you have a running Twenty server at [http://localhost:2020](http://localhost:2020) with your CLI authorized to sync to it. + + +If Docker isn't installed or running, the scaffolder will tell you the right start command for your OS. Once Docker is up, you can resume with `yarn twenty server start` — no need to re-scaffold. + + +--- + +## Phase 3 — Sync your changes + +This is the inner loop you'll spend most of your time in. + +```bash filename="Terminal" +cd my-twenty-app +yarn twenty dev +``` + +This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a second the server reflects the change. You'll see a live status panel in your terminal. + +For more detailed output (build logs, sync requests, error traces), add `--verbose`. + +
+ Dev mode terminal output +
+ +Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). You should see your app under **Your Apps**. + +
+ Your Apps list showing My twenty app +
+ +Click **My twenty app** to see its **application registration** — a server-level record describing your app (name, identifier, OAuth credentials, source). One registration can be installed across multiple workspaces on the same server. + +
+ Application registration details +
+ +Click **View installed app** to see the workspace install. The **About** tab shows version and management options. + +
+ Installed app +
+ +**After this phase:** you have a live development loop. Edit any file in `src/` and it appears in the UI. + +### One-shot sync for CI and scripts + +Pass `--once` to run a single build + sync and exit — same pipeline, no watcher: + +```bash filename="Terminal" +yarn twenty dev --once +``` + +| Command | Behavior | When to use | +| ------------------------ | ------------------------------------------------------------- | ---------------------------------------------------- | +| `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. | +| `yarn twenty dev --once` | Single build + sync, exits `0` on success, `1` on failure. | CI, pre-commit hooks, AI agents, scripted workflows. | + +Both modes need a server in development mode and an authenticated remote. + + +Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests — use `yarn twenty deploy` to deploy to production servers. See [Publishing](/l/ru/developers/extend/apps/operations/publishing). + + +--- + +## Starting from an example + +Use `--example` to start with a more complete project (custom objects, fields, logic functions, front components): + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app --example postcard +``` + +Examples live in [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). You can also scaffold individual entities into an existing project with `yarn twenty add` — see [Scaffolding](/l/ru/developers/extend/apps/getting-started/scaffolding). + +--- + +## What you can build + +Apps are composed of **entities** — each defined as a TypeScript file with a single `export default`: + +| Entity | What it does | +| ---------------------- | ----------------------------------------------------------------------------------- | +| **Objects & Fields** | Custom data models (Post Card, Invoice, etc.) with typed fields | +| **Logic functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events | +| **Front components** | React components that render inside Twenty's UI (side panel, widgets, command menu) | +| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants | +| **Views & Navigation** | Pre-configured list views and sidebar menu items | +| **Page layouts** | Custom record detail pages with tabs and widgets | + +Full reference: [Concepts](/l/ru/developers/extend/apps/getting-started/concepts). + +## Next steps + + + + Application identity, default role, install hooks, public assets. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/scaffolding.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/scaffolding.mdx new file mode 100644 index 0000000000..b2853cadf2 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/scaffolding.mdx @@ -0,0 +1,58 @@ +--- +title: Scaffolding +description: Generate entity files interactively with yarn twenty add — objects, fields, views, logic functions, and more. +icon: wand-magic-sparkles +--- + +Instead of creating entity files by hand, use the interactive scaffolder: + +```bash filename="Terminal" +yarn twenty add +``` + +It prompts you to pick an entity type and walks you through the required fields, then writes a ready-to-use file with a stable `universalIdentifier` and the correct `defineEntity()` call. + +You can also pass the entity type directly to skip the first prompt: + +```bash filename="Terminal" +yarn twenty add object +yarn twenty add logicFunction +yarn twenty add frontComponent +``` + +## Available entity types + +| Entity type | Command | Generated file | +| -------------------- | ------------------------------------ | ------------------------------------------------------- | +| Object | `yarn twenty add object` | `src/objects/\.ts` | +| Field | `yarn twenty add field` | `src/fields/\.ts` | +| Logic function | `yarn twenty add logicFunction` | `src/logic-functions/\.ts` | +| Front component | `yarn twenty add frontComponent` | `src/front-components/\.tsx` | +| Role | `yarn twenty add role` | `src/roles/\.ts` | +| Skill | `yarn twenty add skill` | `src/skills/\.ts` | +| Agent | `yarn twenty add agent` | `src/agents/\.ts` | +| View | `yarn twenty add view` | `src/views/\.ts` | +| Navigation menu item | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\.ts` | +| Page layout | `yarn twenty add pageLayout` | `src/page-layouts/\.ts` | + +## What the scaffolder generates + +Each entity type has its own template. For example, `yarn twenty add object` asks for: + +1. **Name (singular)** — e.g., `invoice` +2. **Name (plural)** — e.g., `invoices` +3. **Label (singular)** — auto-populated from the name (e.g., `Invoice`) +4. **Label (plural)** — auto-populated (e.g., `Invoices`) +5. **Create a view and navigation item?** — if you answer yes, the scaffolder also generates a matching view and sidebar link for the new object. + +Other entity types have simpler prompts — most only ask for a name. + +The `field` entity type is more detailed: it asks for the field name, label, type (from a list of all available field types like `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.), and the target object's `universalIdentifier`. + +## Custom output path + +Use the `--path` flag to place the generated file in a custom location: + +```bash filename="Terminal" +yarn twenty add logicFunction --path src/custom-folder +``` diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/troubleshooting.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/troubleshooting.mdx new file mode 100644 index 0000000000..6a85a65061 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/getting-started/troubleshooting.mdx @@ -0,0 +1,12 @@ +--- +title: Устранение неполадок +description: Распространенные проблемы при первом запуске — Docker, версия Node, Yarn, зависимости. +icon: wrench +--- + +* **Ошибки Docker** — убедитесь, что Docker Desktop (или демон) запущен перед выполнением `yarn twenty server start`. Сообщение об ошибке укажет правильную команду запуска для вашей ОС. +* **Неподходящая версия Node** — нужна 24+. Проверьте с помощью `node -v`. +* **Отсутствует Yarn 4** — выполните `corepack enable`. +* **Зависимости повреждены** — `rm -rf node_modules && yarn install`. + +Застряли? Попросите помощи на [Discord-сервере Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322). diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/layout/command-menu-items.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/layout/command-menu-items.mdx new file mode 100644 index 0000000000..e5078badfb --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/layout/command-menu-items.mdx @@ -0,0 +1,144 @@ +--- +title: Command Menu Items +description: Surface front components as quick actions and command menu (Cmd+K) entries with defineCommandMenuItem. +icon: terminal +--- + +A **command menu item** is the bridge between the user and a [front component](/l/ru/developers/extend/apps/layout/front-components). It registers the component in Twenty's command menu (Cmd+K) and, optionally, as a pinned quick-action button in the top-right corner of the page. + +```ts src/command-menu-items/open-dashboard.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + label: 'Open Dashboard', + shortLabel: 'Dashboard', + icon: 'IconLayoutDashboard', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +## Configuration fields + +| Field | Required | Description | +| --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `universalIdentifier` | Yes | Stable unique ID for the command | +| `label` | Yes | Full label shown in the command menu (Cmd+K) | +| `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens | +| `shortLabel` | No | Shorter label displayed on the pinned quick-action button | +| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) | +| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page | +| `availabilityType` | No | Controls where the command appears: `'GLOBAL'` (always available), `'RECORD_SELECTION'` (only when records are selected), or `'FALLBACK'` (shown when no other commands match) | +| `availabilityObjectUniversalIdentifier` | No | Restrict the command to pages of a specific object type (e.g. only on Company records) | +| `conditionalAvailabilityExpression` | No | A boolean expression that dynamically controls visibility (see below) | + +## Headless commands + +A command menu item paired with a [headless front component](/l/ru/developers/extend/apps/layout/front-components#headless-vs-non-headless) is the idiomatic way to ship a one-click action — run code, navigate, or confirm and execute. The Front Components page covers the [SDK Command components](/l/ru/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) that handle the action-and-unmount pattern. + +A typical flow: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +## Conditional availability expressions + +The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions: + +```ts src/command-menu-items/bulk-update.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; +import { + objectPermissions, + everyEquals, +} from 'twenty-sdk/front-component'; + +export default defineCommandMenuItem({ + universalIdentifier: '...', + label: 'Bulk Update', + availabilityType: 'RECORD_SELECTION', + frontComponentUniversalIdentifier: '...', + conditionalAvailabilityExpression: everyEquals( + objectPermissions, + 'canUpdateObjectRecords', + true, + ), +}); +``` + +### Context variables + +These represent the current state of the page: + +| Variable | Type | Description | +| ------------------------------ | --------- | ---------------------------------------------------------------- | +| `pageType` | `string` | Current page type (e.g. `'RecordIndexPage'`, `'RecordShowPage'`) | +| `isInSidePanel` | `boolean` | Whether the component is rendered in a side panel | +| `numberOfSelectedRecords` | `number` | Number of currently selected records | +| `isSelectAll` | `boolean` | Whether "select all" is active | +| `selectedRecords` | `array` | The selected record objects | +| `favoriteRecordIds` | `array` | IDs of favorited records | +| `objectPermissions` | `object` | Permissions for the current object type | +| `targetObjectReadPermissions` | `object` | Read permissions for the target object | +| `targetObjectWritePermissions` | `object` | Write permissions for the target object | +| `featureFlags` | `object` | Active feature flags | +| `objectMetadataItem` | `object` | Metadata of the current object type | +| `hasAnySoftDeleteFilterOnView` | `boolean` | Whether the current view has a soft-delete filter | + +### Operators + +Combine variables into boolean expressions: + +| Operator | Description | +| ----------------------------------- | ----------------------------------------------------------------- | +| `isDefined(value)` | `true` if the value is not null/undefined | +| `isNonEmptyString(value)` | `true` if the value is a non-empty string | +| `includes(array, value)` | `true` if the array contains the value | +| `includesEvery(array, prop, value)` | `true` if every item's property includes the value | +| `every(array, prop)` | `true` if the property is truthy on every item | +| `everyDefined(array, prop)` | `true` if the property is defined on every item | +| `everyEquals(array, prop, value)` | `true` if the property equals the value on every item | +| `some(array, prop)` | `true` if the property is truthy on at least one item | +| `someDefined(array, prop)` | `true` if the property is defined on at least one item | +| `someEquals(array, prop, value)` | `true` if the property equals the value on at least one item | +| `someNonEmptyString(array, prop)` | `true` if the property is a non-empty string on at least one item | +| `none(array, prop)` | `true` if the property is falsy on every item | +| `noneDefined(array, prop)` | `true` if the property is undefined on every item | +| `noneEquals(array, prop, value)` | `true` if the property does not equal the value on any item | diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/layout/front-components.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/layout/front-components.mdx new file mode 100644 index 0000000000..14c7de8db7 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/layout/front-components.mdx @@ -0,0 +1,404 @@ +--- +title: Front Components +description: Build React components that render inside Twenty's UI with sandboxed isolation. +icon: window-maximize +--- + +Front components are React components that render directly inside Twenty's UI. They run in an **isolated Web Worker** using Remote DOM — your code is sandboxed but renders natively in the page, not in an iframe. + +## Where front components can be used + +Front components can render in two locations within Twenty: + +* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu. +* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside [page layouts](/l/ru/developers/extend/apps/layout/page-layouts). When configuring a dashboard or a record page layout, users can add a front component widget. + +A front component on its own isn't reachable from the UI — you need to *surface* it. The two ways to do that are: + +* **Pair it with a [command menu item](/l/ru/developers/extend/apps/layout/command-menu-items)** — registers it in the command menu (Cmd+K) and, optionally, as a pinned quick-action. +* **Embed it as a widget in a [page layout](/l/ru/developers/extend/apps/layout/page-layouts)** — places it on a record's detail page or dashboard. + +## Basic example + +The quickest way to see a front component in action is to pair it with a [`defineCommandMenuItem`](/l/ru/developers/extend/apps/layout/command-menu-items), so it appears as a quick-action button in the top-right corner of the page: + +```tsx src/front-components/hello-world.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; + +const HelloWorld = () => { + return ( +
+

Hello from my app!

+

This component renders inside Twenty.

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', + name: 'hello-world', + description: 'A simple front component', + component: HelloWorld, +}); +``` + +```ts src/command-menu-items/hello-world.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345', + shortLabel: 'Hello', + label: 'Hello World', + icon: 'IconBolt', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +After syncing with `yarn twenty dev` (or running a one-shot `yarn twenty dev --once`), the quick action appears in the top-right corner of the page: + +
+ Quick action button in the top-right corner +
+ +Click it to render the component inline. + +## Configuration fields + +| Field | Required | Description | +| --------------------- | -------- | ------------------------------------------------------------ | +| `universalIdentifier` | Yes | Stable unique ID for this component | +| `component` | Yes | A React component function | +| `name` | No | Display name | +| `description` | No | Description of what the component does | +| `isHeadless` | No | Set to `true` if the component has no visible UI (see below) | + +## Placing a front component on a page + +Beyond commands, you can embed a front component directly into a record page by adding it as a widget in a **page layout**. See [Page Layouts](/l/ru/developers/extend/apps/layout/page-layouts) for details. + +## Headless vs non-headless + +Front components come in two rendering modes controlled by the `isHeadless` option: + +**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted. + +**Headless (`isHeadless: true`)** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below. + +```tsx src/front-components/sync-tracker.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component'; +import { useEffect } from 'react'; + +const SyncTracker = () => { + const recordId = useRecordId(); + + useEffect(() => { + enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' }); + }, [recordId]); + + return null; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'sync-tracker', + description: 'Tracks record views silently', + isHeadless: true, + component: SyncTracker, +}); +``` + +Because the component returns `null`, Twenty skips rendering a container for it — no empty space appears in the layout. The component still has access to all hooks and the host communication API. + +## SDK Command components + +The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done. + +Import them from `twenty-sdk/command`: + +* **`Command`** — Runs an async callback via the `execute` prop. +* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`. +* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`. +* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`. + +Here is a full example of a headless front component using `Command` to run an action from the command menu: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +And an example using `CommandModal` to ask for confirmation before executing: + +```tsx src/front-components/delete-draft.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { CommandModal } from 'twenty-sdk/command'; + +const DeleteDraft = () => { + const execute = async () => { + // perform the deletion + }; + + return ( + + ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456', + name: 'delete-draft', + description: 'Deletes a draft with confirmation', + component: DeleteDraft, + isHeadless: true, +}); +``` + +## Accessing runtime context + +Inside your component, use SDK hooks to access the current user, record, and component instance: + +```tsx src/front-components/record-info.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { + useUserId, + useRecordId, + useFrontComponentId, +} from 'twenty-sdk/front-component'; + +const RecordInfo = () => { + const userId = useUserId(); + const recordId = useRecordId(); + const componentId = useFrontComponentId(); + + return ( +
+

User: {userId}

+

Record: {recordId ?? 'No record context'}

+

Component: {componentId}

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012', + name: 'record-info', + component: RecordInfo, +}); +``` + +Available hooks: + +| Hook | Returns | Description | +| --------------------------------------------- | ------------------ | ---------------------------------------------------------- | +| `useUserId()` | `string` or `null` | The current user's ID | +| `useSelectedRecordIds()` | `string[]` | All selected record IDs (empty array if none selected) | +| `useRecordId()` | `string` or `null` | **Deprecated.** Use `useSelectedRecordIds()` instead | +| `useFrontComponentId()` | `string` | This component instance's ID | +| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function | + +## Host communication API + +Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`: + +| Function | Description | +| ----------------------------------------------- | ----------------------------- | +| `navigate(to, params?, queryParams?, options?)` | Navigate to a page in the app | +| `openSidePanelPage(params)` | Open a side panel | +| `closeSidePanel()` | Close the side panel | +| `openCommandConfirmationModal(params)` | Show a confirmation dialog | +| `enqueueSnackbar(params)` | Show a toast notification | +| `unmountFrontComponent()` | Unmount the component | +| `updateProgress(progress)` | Update a progress indicator | + +Here is an example that uses the host API to show a snackbar and close the side panel after an action completes: + +```tsx src/front-components/archive-record.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const ArchiveRecord = () => { + const recordId = useRecordId(); + + const handleArchive = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { status: 'ARCHIVED' } }, + id: true, + }, + }); + + await enqueueSnackbar({ + message: 'Record archived', + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Archive this record?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678', + name: 'archive-record', + description: 'Archives the current record', + component: ArchiveRecord, +}); +``` + +### Working with multiple records + +Use `useSelectedRecordIds()` to handle multiple selected records. This is useful for bulk operations: + +```tsx src/front-components/bulk-export.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const BulkExport = () => { + const selectedRecordIds = useSelectedRecordIds(); + + const handleExport = async () => { + const client = new CoreApiClient(); + + for (const recordId of selectedRecordIds) { + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { exported: true } }, + id: true, + }, + }); + } + + await enqueueSnackbar({ + message: `Exported ${selectedRecordIds.length} records`, + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Export {selectedRecordIds.length} selected record(s)?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901', + name: 'bulk-export', + description: 'Export selected records', + component: BulkExport, + command: { + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902', + label: 'Bulk Export', + availabilityType: 'RECORD_SELECTION', + conditionalAvailabilityExpression: numberOfSelectedRecords > 0, + }, +}); +``` + +## Public assets + +Front components can access files from the app's `public/` directory using `getPublicAssetUrl`: + +```tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +const Logo = () => Logo; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'logo', + component: Logo, +}); +``` + +See the [public assets section](/l/ru/developers/extend/apps/config/public-assets) for details. + +## Styling + +Front components support multiple styling approaches. You can use: + +* **Inline styles** — `style={{ color: 'red' }}` +* **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more) +* **Emotion** — CSS-in-JS with `@emotion/react` +* **Styled-components** — `styled.div` patterns +* **Tailwind CSS** — utility classes +* **Any CSS-in-JS library** compatible with React + +```tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Button, Tag, Status } from 'twenty-sdk/ui'; + +const StyledWidget = () => { + return ( +
+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456', + name: 'styled-widget', + component: StyledWidget, +}); +``` diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/layout/navigation-menu-items.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/layout/navigation-menu-items.mdx new file mode 100644 index 0000000000..1b7f893831 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/layout/navigation-menu-items.mdx @@ -0,0 +1,44 @@ +--- +title: Navigation Menu Items +description: Add custom entries to the workspace sidebar — links to saved views or external URLs. +icon: меню +--- + +A **navigation menu item** is an entry in the left sidebar. Use `defineNavigationMenuItem()` to ship custom sidebar links — typically one per [view](/l/ru/developers/extend/apps/layout/views) you ship — or to point at external URLs. + +```ts src/navigation-menu-items/example-navigation-menu-item.ts +import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define'; +import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view'; + +export default defineNavigationMenuItem({ + universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c', + name: 'example-navigation-menu-item', + icon: 'IconList', + color: 'blue', + position: 0, + type: NavigationMenuItemType.VIEW, + viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER, +}); +``` + +## Основные моменты + +* `type` determines what the menu item links to. Each type pairs with a specific identifier field: + + | Тип | Что делает | Required field | + | ------------------------------------ | ------------------------------------ | ----------------------------------------------------------------------------- | + | `NavigationMenuItemType.VIEW` | Opens a saved view | `viewUniversalIdentifier` | + | `NavigationMenuItemType.LINK` | Opens an external URL | `link` | + | `NavigationMenuItemType.FOLDER` | Groups nested items under a label | `name` (and child items reference the folder via `folderUniversalIdentifier`) | + | `NavigationMenuItemType.OBJECT` | Opens an object's default index page | `targetObjectUniversalIdentifier` | + | `NavigationMenuItemType.PAGE_LAYOUT` | Opens a standalone page layout | `pageLayoutUniversalIdentifier` | + +* `position` controls ordering in the sidebar. + +* `icon` and `color` are optional and customize how the entry looks. + +* `folderUniversalIdentifier` is also available on any item to nest it inside a `FOLDER`-type parent. + + +**Common pitfall:** creating an object without an associated view + navigation menu item makes that object invisible to users. Unless it's a technical/internal object, every custom object should have a default view *and* a sidebar entry pointing at it. + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/layout/overview.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/layout/overview.mdx new file mode 100644 index 0000000000..7934797f2a --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/layout/overview.mdx @@ -0,0 +1,56 @@ +--- +title: Overview +description: Place your app inside Twenty's UI — sidebar entries, saved views, record page tabs, and sandboxed React components. +icon: table-columns +--- + +A Twenty app's **layout layer** is everything the user sees: where the app surfaces in the sidebar, which list views it ships, how its record detail pages are arranged, and which custom React components render inside those pages. + +```text + Sidebar Record list Record detail page + ─────── ─────────── ────────────────── + [📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐ + [📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │ + [📋 Inbox ] │ ──────── │ │ [Notes ] │ + ▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab + │ │ Acme │ │ │ adds a tab... + └ defineNavi- │ … │ │ ┌────────────────┐ │ + gationMenu- └────▲─────┘ │ │ │ │ + Item points │ │ │ React UI │◀── …with a + to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent + └ defineView │ │ a Worker) │ │ widget inside + picks columns │ └────────────────┘ │ + and filters └─────────────────────┘ +``` + +## In this section + + + + `defineView` — saved list configurations: visible columns, filters, groups. + + + `defineNavigationMenuItem` — sidebar entries pointing at views or external URLs. + + + `definePageLayout` and `definePageLayoutTab` — tabs and widgets on a record's detail page. + + + `defineFrontComponent` — sandboxed React components that render inside Twenty. + + + `defineCommandMenuItem` — register front components as Cmd+K entries and quick actions. + + + +## Where the app surfaces + +| Surface | What it controls | Entity | +| --------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------- | +| **Sidebar** | A custom entry linking to a saved view or external URL | `defineNavigationMenuItem` | +| **Record list** | A saved configuration for an object — visible columns, order, filters, groups | `defineView` | +| **Record detail page** | The tabs and widgets on a record page (your own object's, or a standard one) | `definePageLayout`, `definePageLayoutTab` | +| **Inside any of the above** | A custom React widget — buttons, forms, dashboards, integrations | `defineFrontComponent` | +| **Command menu (Cmd+K)** | A pinned quick action or hidden command | `defineCommandMenuItem` | + +Front components run inside an isolated Web Worker using Remote DOM — they render *natively* in the page (not inside an iframe), but cannot reach the host page or DOM directly. Communication with Twenty happens through a message-passing host API. diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/layout/page-layouts.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/layout/page-layouts.mdx new file mode 100644 index 0000000000..011576324f --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/layout/page-layouts.mdx @@ -0,0 +1,102 @@ +--- +title: Page Layouts +description: Customize record detail pages — tabs, widgets, and where front components render — using definePageLayout and definePageLayoutTab. +icon: table-columns +--- + +A **page layout** controls how a record's detail page is arranged: which tabs appear and what widgets they contain. Use `definePageLayout()` to declare a layout for an object you own, or `definePageLayoutTab()` to add a single tab to a layout that already exists (yours or a standard Twenty one). + +| Use case | Entity | +| ---------------------------------------------------------------------- | --------------------- | +| Define the entire layout for a record page on an object you own | `definePageLayout` | +| Add one tab to an existing layout (your own object, or a standard one) | `definePageLayoutTab` | + +## definePageLayout + +Use this when you own the entire detail page — typically for a custom object you defined yourself. + +```ts src/page-layouts/example-record-page-layout.ts +import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +export default definePageLayout({ + universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134', + name: 'Example Record Page', + type: 'RECORD_PAGE', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + tabs: [ + { + universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5', + title: 'Hello World', + position: 50, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], + }, + ], +}); +``` + +### Key points + +* `type` is typically `'RECORD_PAGE'` to customize the detail view of a specific object. +* `objectUniversalIdentifier` specifies which object this layout applies to. +* Each `tab` defines a section of the page with a `title`, `position`, and `layoutMode` (`CANVAS` for free-form layout). +* Each `widget` inside a tab can render a [front component](/l/ru/developers/extend/apps/layout/front-components), a relation list, or other built-in widget types. +* `position` on tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones. + +## definePageLayoutTab + +Use this when you only want to **add** a tab to an existing layout — for example, an analytics tab on the standard Company page, or an AI summary tab attached to your own object's layout. + +```ts src/page-layouts/example-extra-tab.ts +import { + definePageLayoutTab, + PageLayoutTabLayoutMode, +} from 'twenty-sdk/define'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = + '20202020-ab01-4001-8001-c0aba11c0100'; + +export default definePageLayoutTab({ + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001', + pageLayoutUniversalIdentifier: + COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER, + title: 'Hello World', + position: 1000, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], +}); +``` + +### Key points + +* `pageLayoutUniversalIdentifier` is **required** and must point to a page layout that already exists at install time — either a standard Twenty layout or one defined by your own app. Cross-app references to layouts owned by another installed app are not supported today. When the parent layout is missing, installation fails with a clear validation error. +* `widgets` are scoped to this tab only — they reference [front components](/l/ru/developers/extend/apps/layout/front-components), views, etc. exactly like widgets defined inline in `definePageLayout`. +* `position` controls ordering against existing tabs on the targeted layout. Pick a value that places your tab where you want it relative to built-in tabs. +* Use this instead of `definePageLayout` when you only want to add to an existing layout. Use `definePageLayout` when you own the entire layout. diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/layout/views.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/layout/views.mdx new file mode 100644 index 0000000000..be42010c35 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/layout/views.mdx @@ -0,0 +1,43 @@ +--- +title: Views +description: Ship pre-configured saved views — column order, filters, groups — for objects in your app. +icon: list +--- + +A **view** is a saved configuration for how records of an object are displayed: which fields appear, their order, whether they're visible, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app — typically a default index view for each custom object you create. + +```ts src/views/example-view.ts +import { defineView, ViewKey } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; + +export default defineView({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'All example items', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + icon: 'IconList', + key: ViewKey.INDEX, + position: 0, + fields: [ + { + universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0', + fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER, + position: 0, + isVisible: true, + size: 200, + }, + ], +}); +``` + +## Key points + +* `objectUniversalIdentifier` specifies which object this view applies to. It can be a custom object you defined or a standard Twenty object. +* `key` determines the view type — `ViewKey.INDEX` is the main list view for the object. +* `fields` controls which columns appear and in what order. Each field references a `fieldMetadataUniversalIdentifier`. +* You can also declare `filters`, `filterGroups`, `groups`, and `fieldGroups` for advanced configurations. +* `position` controls ordering when multiple views exist for the same object. + +## How views show up in the UI + +A view by itself isn't reachable from the sidebar. To make it appear there, pair it with a [navigation menu item](/l/ru/developers/extend/apps/layout/navigation-menu-items) of type `VIEW` that points at the view's `universalIdentifier`. That's the canonical pattern: every custom object typically ships a default view + a sidebar entry that opens it. diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/logic/connections.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/logic/connections.mdx new file mode 100644 index 0000000000..1a99c59742 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/logic/connections.mdx @@ -0,0 +1,193 @@ +--- +title: Connections +description: Let your app act on a user's behalf in third-party services via OAuth. +icon: plug +--- + +Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API. + +Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate. + + + + + +A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace. + +A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials. + +```ts src/connection-providers/linear-connection.ts +import { defineConnectionProvider } from 'twenty-sdk/define'; + +export default defineConnectionProvider({ + universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f', + name: 'linear', + displayName: 'Linear', + icon: 'IconBrandLinear', + type: 'oauth', + oauth: { + authorizationEndpoint: 'https://linear.app/oauth/authorize', + tokenEndpoint: 'https://api.linear.app/oauth/token', + scopes: ['read', 'write'], + // These must match keys in `defineApplication.serverVariables` below. + clientIdVariable: 'LINEAR_CLIENT_ID', + clientSecretVariable: 'LINEAR_CLIENT_SECRET', + // Optional: defaults to 'json'. Some providers (Linear, Slack) want + // 'form-urlencoded' for the token request. + tokenRequestContentType: 'form-urlencoded', + // Optional: defaults to true. Disable only if the provider rejects PKCE. + usePkce: false, + // Optional: extra query params on the authorize URL. + // authorizationParams: { prompt: 'consent' }, + // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect. + // revokeEndpoint: 'https://example.com/oauth/revoke', + }, +}); +``` + +```ts src/application.config.ts +import { defineApplication } from 'twenty-sdk/define'; + +export default defineApplication({ + universalIdentifier: '...', + displayName: 'Linear', + description: 'Connect Linear to Twenty.', + defaultRoleUniversalIdentifier: '...', + // OAuth client credentials live on the app registration (one OAuth app per + // Twenty server, configured by the admin) — not per-workspace. Declare them + // as serverVariables so the admin can fill them in once for all installs. + serverVariables: { + LINEAR_CLIENT_ID: { + description: 'OAuth client ID from your Linear OAuth application.', + isSecret: false, + isRequired: true, + }, + LINEAR_CLIENT_SECRET: { + description: 'OAuth client secret from your Linear OAuth application.', + isSecret: true, + isRequired: true, + }, + }, +}); +``` + +Key points: + +* `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`). +* `displayName` shows in the per-app settings tab and in the AI tool list. +* `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo. +* Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server. +* Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled. +* `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`. + +The OAuth callback URL your provider needs to whitelist is: + +``` +https:///apps/oauth/callback +``` + + + + + +Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens. + +```ts src/logic-functions/handlers/create-linear-issue-handler.ts +import { listConnections } from 'twenty-sdk/logic-function'; + +export const createLinearIssueHandler = async (input: { + teamId?: string; + title?: string; +}) => { + if (!input.teamId || !input.title) { + return { success: false, error: 'teamId and title are required' }; + } + + const connections = await listConnections({ providerName: 'linear' }); + + // Workspace-shared credentials win when present; fall back to the first + // user-visibility one. For HTTP-route triggers you typically pick the + // request user's connection via event.userWorkspaceId instead. + const connection = + connections.find((c) => c.visibility === 'workspace') ?? connections[0]; + + if (!connection) { + return { + success: false, + error: + 'Linear is not connected. Open the app settings and click "Add connection".', + }; + } + + // Use connection.accessToken to call the third-party API. + const response = await fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + Authorization: `Bearer ${connection.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`, + }), + }); + + return { success: response.ok }; +}; +``` + +Each connection has: + +| Field | Description | +| ----------------- | -------------------------------------------------------------------------------------------------------- | +| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one | +| `visibility` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) | +| `scopes` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) | +| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers | +| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) | +| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) | +| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect | + +Key points: + +* Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers. +* The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set). +* `getConnection(id)` is the single-row equivalent. + + + + + +When a user clicks "Add connection," they're prompted to pick a visibility: + +* **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not. +* **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user. + +Use the right one for each handler: + +```ts +// HTTP-route trigger — prefer the request user's own connection. +const conn = + connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ?? + connections.find((c) => c.visibility === 'workspace'); + +// Cron trigger — no request user; only shared credentials are sensible. +const conn = connections.find((c) => c.visibility === 'workspace'); +``` + +Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side. + + + + + +For each connection provider, the server admin needs to register an OAuth app at the third party first. + +1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new). +2. Set the **Redirect URI** to `\/apps/oauth/callback`. +3. Copy the generated **Client ID** and **Client Secret**. +4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`. +5. Workspace members can then add connections from the per-app **Connections** section. + + + + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/logic/logic-functions.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/logic/logic-functions.mdx new file mode 100644 index 0000000000..cb55e40abf --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/logic/logic-functions.mdx @@ -0,0 +1,375 @@ +--- +title: Logic Functions +description: Define server-side TypeScript functions with HTTP, cron, and database event triggers. +icon: bolt +--- + +Logic functions are server-side TypeScript functions that run on the Twenty platform. They can be triggered by HTTP requests, cron schedules, or database events — and can also be exposed as tools for AI agents. + + + + +Each function file uses `defineLogicFunction()` to export a configuration with a handler and optional triggers. + +```ts src/logic-functions/createPostCard.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define'; +import { CoreApiClient, type Person } from 'twenty-client-sdk/core'; + +const handler = async (params: RoutePayload) => { + const client = new CoreApiClient(); + const body = (params.body ?? {}) as { name?: string }; + const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'; + + const result = await client.mutation({ + createPostCard: { + __args: { data: { name } }, + id: true, + name: true, + }, + }); + return result; +}; + +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'create-new-post-card', + timeoutSeconds: 2, + handler, + httpRouteTriggerSettings: { + path: '/post-card/create', + httpMethod: 'POST', + isAuthRequired: true, + }, + /*databaseEventTriggerSettings: { + eventName: 'people.created', + },*/ + /*cronTriggerSettings: { + pattern: '0 0 1 1 *', + },*/ +}); +``` + +Available trigger types: +* **httpRoute**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**: +> e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create` +* **cron**: Runs your function on a schedule using a CRON expression. +* **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function. +> e.g. `person.updated`, `*.created`, `company.*` + + +You can also manually execute a function using the CLI: + +```bash filename="Terminal" +yarn twenty exec -n create-new-post-card -p '{"key": "value"}' +``` + +```bash filename="Terminal" +yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + +You can watch logs with: + +```bash filename="Terminal" +yarn twenty logs +``` + + +#### Route trigger payload + +When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the +[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). +Import the `RoutePayload` type from `twenty-sdk`: + +```ts +import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define'; + +const handler = async (event: RoutePayload) => { + const { headers, queryStringParameters, pathParameters, body } = event; + const { method, path } = event.requestContext.http; + + return { message: 'Success' }; +}; +``` + +The `RoutePayload` type has the following structure: + + | Property | Type | Description | Example | + | ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | + | `headers` | `Record\` | HTTP headers (only those listed in `forwardedRequestHeaders`) | see section below | + | `queryStringParameters` | `Record\` | Query string parameters (multiple values joined with commas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` | + | `pathParameters` | `Record\` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` | + | `body` | `object \| null` | Parsed request body (JSON) | `{ id: 1 }` -> `{ id: 1 }` | + | `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | | + | `isBase64Encoded` | `boolean` | Whether the body is base64 encoded | | + | `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) | | + | `requestContext.http.path` | `string` | Raw request path | | + + +#### forwardedRequestHeaders + +By default, HTTP headers from incoming requests are **not** passed to your logic function for security reasons. +To access specific headers, list them in the `forwardedRequestHeaders` array: + +```ts +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'webhook-handler', + handler, + httpRouteTriggerSettings: { + path: '/webhook', + httpMethod: 'POST', + isAuthRequired: false, + forwardedRequestHeaders: ['x-webhook-signature', 'content-type'], + }, +}); +``` + +In your handler, access the forwarded headers like this: + +```ts +const handler = async (event: RoutePayload) => { + const signature = event.headers['x-webhook-signature']; + const contentType = event.headers['content-type']; + + // Validate webhook signature... + return { received: true }; +}; +``` + + +Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`). + + +#### Exposing a function as an AI tool or workflow action + +Logic functions can be exposed on two surfaces, each with its own trigger: + +* **`toolTriggerSettings`** — makes the function discoverable by Twenty's AI features (chat, MCP, function calling). Uses standard JSON Schema, the format LLMs natively understand. +* **`workflowActionTriggerSettings`** — makes the function appear as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper field editors, variable pickers, and labels. + +A function can opt into one, the other, or both. They sit alongside `cronTriggerSettings`, `databaseEventTriggerSettings`, and `httpRouteTriggerSettings` — same pattern, same shape. + +```ts src/logic-functions/enrich-company.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const handler = async (params: { companyName: string; domain?: string }) => { + const client = new CoreApiClient(); + + 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, + toolTriggerSettings: {}, +}); +``` + +Key points: + +* A function can mix surfaces — declare both `toolTriggerSettings` and `workflowActionTriggerSettings` to expose it in chat AND in the workflow builder. +* `toolTriggerSettings.inputSchema` and `workflowActionTriggerSettings.inputSchema` are both optional. When omitted, the manifest builder infers them from the handler source code (JSON Schema for the AI tool, Twenty's `InputSchema` for the workflow action). Provide one explicitly when you want richer typing — for example, with `FieldMetadataType`-aware fields like `CURRENCY` or `RELATION` for the workflow builder, or with `description` fields the AI agent can read: + +```ts +export default defineLogicFunction({ + ..., + toolTriggerSettings: { + inputSchema: { + 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'], + }, + }, +}); +``` + + +**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. + + + + + + +**Install hooks** — pre-install and post-install handlers — share this runtime but are declared with their own define functions and don't take trigger settings. See [Install Hooks](/l/ru/developers/extend/apps/config/install-hooks) for `definePreInstallLogicFunction` and `definePostInstallLogicFunction`. + + +## Typed API clients (twenty-client-sdk) + +The `twenty-client-sdk` package provides two typed GraphQL clients for interacting with the Twenty API from your logic functions and front components. + +| Client | Import | Endpoint | Generated? | +| ------------------- | ---------------------------- | ---------------------------------------------- | ---------------------- | +| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — workspace data (records, objects) | Yes, at dev/build time | +| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — workspace config, file uploads | No, ships pre-built | + + + + +`CoreApiClient` is the main client for querying and mutating workspace data. It is **generated from your workspace schema** during `yarn twenty dev` or `yarn twenty build`, so it is fully typed to match your objects and fields. + +```ts +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const client = new CoreApiClient(); + +// Query records +const { companies } = await client.query({ + companies: { + edges: { + node: { + id: true, + name: true, + domainName: { + primaryLinkLabel: true, + primaryLinkUrl: true, + }, + }, + }, + }, +}); + +// Create a record +const { createCompany } = await client.mutation({ + createCompany: { + __args: { + data: { + name: 'Acme Corp', + }, + }, + id: true, + name: true, + }, +}); +``` + +The client uses a selection-set syntax: pass `true` to include a field, use `__args` for arguments, and nest objects for relations. You get full autocompletion and type checking based on your workspace schema. + + +**CoreApiClient is generated at dev/build time.** If you use it without running `yarn twenty dev` or `yarn twenty build` first, it throws an error. The generation happens automatically — the CLI introspects your workspace's GraphQL schema and generates a typed client using `@genql/cli`. + + +#### Using CoreSchema for type annotations + +`CoreSchema` provides TypeScript types matching your workspace objects — useful for typing component state or function parameters: + +```ts +import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core'; +import { useState } from 'react'; + +const [company, setCompany] = useState< + Pick | undefined +>(undefined); + +const client = new CoreApiClient(); +const result = await client.query({ + company: { + __args: { filter: { position: { eq: 1 } } }, + id: true, + name: true, + }, +}); +setCompany(result.company); +``` + + + + +`MetadataApiClient` ships pre-built with the SDK (no generation required). It queries the `/metadata` endpoint for workspace configuration, applications, and file uploads. + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; + +const metadataClient = new MetadataApiClient(); + +// List first 10 objects in the workspace +const { objects } = await metadataClient.query({ + objects: { + edges: { + node: { + id: true, + nameSingular: true, + namePlural: true, + labelSingular: true, + isCustom: true, + }, + }, + __args: { + filter: {}, + paging: { first: 10 }, + }, + }, +}); +``` + +#### Uploading files + +`MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields: + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import * as fs from 'fs'; + +const metadataClient = new MetadataApiClient(); + +const fileBuffer = fs.readFileSync('./invoice.pdf'); + +const uploadedFile = await metadataClient.uploadFile( + fileBuffer, // file contents as a Buffer + 'invoice.pdf', // filename + 'application/pdf', // MIME type + '58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier +); + +console.log(uploadedFile); +// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' } +``` + +| Parameter | Type | Description | +| ---------------------------------- | -------- | --------------------------------------------------------------- | +| `fileBuffer` | `Buffer` | The raw file contents | +| `filename` | `string` | The name of the file (used for storage and display) | +| `contentType` | `string` | MIME type (defaults to `application/octet-stream` if omitted) | +| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object | + +Key points: +* Uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed. +* The returned `url` is a signed URL you can use to access the uploaded file. + + + + + + When your code runs on Twenty (logic functions or front components), the platform injects credentials as environment variables: + + * `TWENTY_API_URL` — Base URL of the Twenty API + * `TWENTY_APP_ACCESS_TOKEN` — Short-lived key scoped to your application's default function role + + You do **not** need to pass these to the clients — they read from `process.env` automatically. The API key's permissions are determined by the role referenced in `defaultRoleUniversalIdentifier` in your `application-config.ts`. + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/logic/overview.mdx new file mode 100644 index 0000000000..902195d43e --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/logic/overview.mdx @@ -0,0 +1,55 @@ +--- +title: Overview +description: Server-side TypeScript that runs inside Twenty — triggered by HTTP routes, cron schedules, database events, AI tools, or workflow actions. +icon: bolt +--- + +A Twenty app's **logic layer** is the code that *runs* — server-side TypeScript handlers reacting to HTTP requests, cron schedules, and record changes; AI skills and agents that live inside the workspace; and OAuth connections that let your functions act on a user's behalf in third-party services. + +```text + ┌─ HTTP route ──┐ + │ Cron schedule │ + │ Database event │ ┌────────────────────┐ + triggers ─┤ AI tool call ├─────▶│ Logic function │ + │ Workflow action │ │ (your handler) │ + │ Manual exec │ └────────────────────┘ + └────────────────────┘ │ + ▼ + ┌────────────────────────────┐ + │ Twenty API (records) │ + │ Third-party API │ + │ (via Connection token) │ + └────────────────────────────┘ +``` + +## In this section + + + + The core building block — trigger types, payloads, and the typed API client. + + + Reusable AI agent instructions and assistants with custom system prompts. + + + OAuth credentials your app holds for third-party services — Linear, GitHub, Slack, and more. + + + +## Trigger types at a glance + +A logic function picks one or more triggers — every entry below is a separate field on `defineLogicFunction()`: + +| Trigger | When it runs | Setting | +| ------------------- | ---------------------------------------------------------- | ------------------------------- | +| **HTTP route** | A request hits your `/s/\` endpoint | `httpRouteTriggerSettings` | +| **Cron** | A CRON expression matches | `cronTriggerSettings` | +| **Database event** | A workspace record is created, updated, or deleted | `databaseEventTriggerSettings` | +| **AI tool** | A Twenty AI feature decides to call your function | `toolTriggerSettings` | +| **Workflow action** | A workflow step invokes your function | `workflowActionTriggerSettings` | + +Functions run sandboxed in isolated Node.js processes and access the workspace through a typed API client scoped to the role declared on [`defineApplication()`](/l/ru/developers/extend/apps/config/application). + + +**Install-time hooks** — code that runs before or after the install — share this runtime but use their own define functions and live under [Config → Install Hooks](/l/ru/developers/extend/apps/config/install-hooks). + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/logic/skills-and-agents.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/logic/skills-and-agents.mdx new file mode 100644 index 0000000000..82a66ef20f --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/logic/skills-and-agents.mdx @@ -0,0 +1,69 @@ +--- +title: Skills & Agents +description: Define AI skills and agents for your app. +icon: robot +--- + + + Skills and agents are currently in alpha. The feature works but is still evolving. + + +Apps can define AI capabilities that live inside the workspace — reusable skill instructions and agents with custom system prompts. + + + + +Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation: + +```ts src/skills/example-skill.ts +import { defineSkill } from 'twenty-sdk/define'; + +export default defineSkill({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'sales-outreach', + label: 'Sales Outreach', + description: 'Guides the AI agent through a structured sales outreach process', + icon: 'IconBrain', + content: `You are a sales outreach assistant. When reaching out to a prospect: +1. Research the company and recent news +2. Identify the prospect's role and likely pain points +3. Draft a personalized message referencing specific details +4. Keep the tone professional but conversational`, +}); +``` + +Key points: +* `name` is a unique identifier string for the skill (kebab-case recommended). +* `label` is the human-readable display name shown in the UI. +* `content` contains the skill instructions — this is the text the AI agent uses. +* `icon` (optional) sets the icon displayed in the UI. +* `description` (optional) provides additional context about the skill's purpose. + + + + +Agents are AI assistants that live inside your workspace. Use `defineAgent()` to create agents with a custom system prompt: + +```ts src/agents/example-agent.ts +import { defineAgent } from 'twenty-sdk/define'; + +export default defineAgent({ + universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123', + name: 'sales-assistant', + label: 'Sales Assistant', + description: 'Helps the sales team draft outreach emails and research prospects', + icon: 'IconRobot', + prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.', +}); +``` + +Key points: +* `name` is the unique identifier string for the agent (kebab-case recommended). +* `label` is the display name shown in the UI. +* `prompt` is the system prompt that defines the agent's behavior. +* `description` (optional) provides context about what the agent does. +* `icon` (optional) sets the icon displayed in the UI. +* `modelId` (optional) overrides the default AI model used by the agent. + + + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/operations/cli.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/operations/cli.mdx new file mode 100644 index 0000000000..0cccebdbb5 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/operations/cli.mdx @@ -0,0 +1,78 @@ +--- +title: CLI +description: yarn twenty commands for executing functions, streaming logs, managing app installations, and switching remotes. +icon: terminal +--- + +Beyond `dev`, `build`, `add`, and `typecheck`, the `yarn twenty` CLI provides commands for executing functions, viewing logs, and managing app installations. + +## Executing functions (`yarn twenty exec`) + +Run a logic function manually without triggering it via HTTP, cron, or database event: + +```bash filename="Terminal" +# Execute by function name +yarn twenty exec -n create-new-post-card + +# Execute by universalIdentifier +yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf + +# Pass a JSON payload +yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}' + +# Execute the post-install function +yarn twenty exec --postInstall +``` + +## Viewing function logs (`yarn twenty logs`) + +Stream execution logs for your app's logic functions: + +```bash filename="Terminal" +# Stream all function logs +yarn twenty logs + +# Filter by function name +yarn twenty logs -n create-new-post-card + +# Filter by universalIdentifier +yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + + +This is different from `yarn twenty server logs`, which shows the Docker container logs. `yarn twenty logs` shows your app's function execution logs from the Twenty server. + + +## Uninstalling an app (`yarn twenty uninstall`) + +Remove your app from the active workspace: + +```bash filename="Terminal" +yarn twenty uninstall + +# Skip the confirmation prompt +yarn twenty uninstall --yes +``` + +## Managing remotes + +A **remote** is a Twenty server that your app connects to. During setup, the scaffolder creates one for you automatically. You can add more remotes or switch between them at any time. + +```bash filename="Terminal" +# Add a new remote (opens a browser for OAuth login) +yarn twenty remote add + +# Connect to a local Twenty server (auto-detects port 2020 or 3000) +yarn twenty remote add --local + +# Add a remote non-interactively (useful for CI) +yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote + +# List all configured remotes +yarn twenty remote list + +# Switch the active remote +yarn twenty remote switch +``` + +Your credentials are stored in `~/.twenty/config.json`. diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/operations/overview.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/operations/overview.mdx new file mode 100644 index 0000000000..1e2cfe83a4 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/operations/overview.mdx @@ -0,0 +1,29 @@ +--- +title: Overview +description: Build, test, and ship your app — CLI commands, integration tests, CI, and publishing to a server or to npm. +icon: rocket +--- + +The **operations layer** is everything you do *to* your app rather than *with* it: invoking CLI commands, running integration tests against a real Twenty server, configuring CI, and shipping releases — either as a tarball deployed to a single server or as an npm package listed in the marketplace. + +```text + develop ─▶ test ─▶ build ─▶ deploy / publish + ─────── ──── ───── ───────────────── + yarn yarn yarn yarn twenty deploy (tarball → one server) + twenty test twenty + dev build yarn twenty publish (npm → marketplace) +``` + +## In this section + + + + `yarn twenty` reference — exec, logs, uninstall, remotes. + + + Vitest setup, integration tests, type checking, CI workflow. + + + Build, deploy a tarball, publish to npm, install. + + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/operations/publishing.mdx new file mode 100644 index 0000000000..340eb88d56 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/operations/publishing.mdx @@ -0,0 +1,295 @@ +--- +title: Publishing +icon: upload +description: Distribute your Twenty app to the marketplace or deploy it internally. +--- + +## Overview + +Once your app is [built and tested locally](/l/ru/developers/extend/apps/getting-started/concepts), you have two paths for distributing it: + +* **Deploy a tarball** — upload your app directly to a specific Twenty server for internal or private use. +* **Publish to npm** — list your app in the Twenty marketplace for any workspace to discover and install. + +Both paths start from the same **build** step. + +## Building your app + +Run the build command to compile your app and generate a distribution-ready `manifest.json`: + +```bash filename="Terminal" +yarn twenty build +``` + +This compiles TypeScript sources, transpiles logic functions and front components, and writes everything to `.twenty/output/`. Add `--tarball` to also produce a `.tgz` package for manual distribution or the deploy command. + +## Deploying to a server (tarball) + +For apps you don't want publicly available — proprietary tools, enterprise-only integrations, or experimental builds — you can deploy a tarball directly to a Twenty server. + +### Prerequisites + +Before deploying, you need a configured remote pointing to the target server. Remotes store the server URL and authentication credentials locally in `~/.twenty/config.json`. + +Add a remote: + +```bash filename="Terminal" +yarn twenty remote add --api-url https://your-twenty-server.com --as production +``` + +### Deploying + +Build and upload your app to the server in one step: + +```bash filename="Terminal" +yarn twenty deploy +# To deploy to a specific remote: +# yarn twenty deploy --remote production +``` + +### Sharing a deployed app + + +Sharing private (tarball) apps across workspaces is an **Enterprise** feature. The **Distribution** tab will show an upgrade prompt instead of the share controls until your workspace has a valid Enterprise key. See [Settings > Admin Panel > Enterprise](/settings/admin-panel#enterprise) to activate it. + + +Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. Once your workspace is on the Enterprise plan, you can share a deployed app like this: + +1. Go to **Settings > Applications > Registrations** and open your app +2. In the **Distribution** tab, click **Copy share link** +3. Share this link with users on other workspaces — it takes them directly to the app's install page + +The share link uses the server's base URL (without any workspace subdomain) so it works for any workspace on the server. + +### Version management + +When updating an already deployed tarball app, the server requires the `version` in `package.json` to be **strictly higher** (per [semver](https://semver.org) ordering) than the currently deployed version. Re-deploying the same version, or pushing a lower one, is rejected before the tarball is stored — you'll see a `VERSION_ALREADY_EXISTS` error from the CLI. + +To release an update: + +1. Bump the `version` field in your `package.json` (e.g. `1.2.3` → `1.2.4`, `1.3.0`, or `2.0.0`) +2. Run `yarn twenty deploy` (or `yarn twenty deploy --remote production`) +3. Workspaces that have the app installed will see the upgrade available in their settings + + +Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allowed, and a final release like `1.0.0` is correctly recognized as higher than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver string. + + +{/* TODO: add screenshot of the Upgrade button */} + +### Server version compatibility + +If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`: + +```json filename="package.json" +{ + "name": "twenty-my-app", + "version": "1.0.0", + "engines": { + "node": "^24.5.0", + "twenty": ">=2.3.0" + } +} +``` + +The value is a standard [semver range](https://github.com/npm/node-semver#ranges). Common patterns: + +| Range | Meaning | +| ---------------------------------- | ------------------------------------------ | +| `>=2.3.0` | Any server from 2.3.0 onward | +| `>=2.3.0 \<3.0.0` | 2.3.0 or later, but below the next major | +| `^2.3.0` | Same as `>=2.3.0 \<3.0.0` | + +**What happens at deploy and install time:** + +* If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version. +* If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps). +* If the server has no `APP_VERSION` configured, the check is skipped. + + +The server is the authoritative check — it validates `engines.twenty` on both tarball upload and workspace install. If you deploy a tarball out-of-band or install from the marketplace, the server still enforces compatibility. + + +## Automated CI/CD (scaffolded workflows) + +Apps generated with `create-twenty-app` ship with two GitHub Actions workflows out of the box, under `.github/workflows/`. They are ready to run as soon as you push the repo to GitHub — no extra setup is needed for CI, and CD only requires a single secret. + +### CI — `ci.yml` + +Runs integration tests on every push to `main` and every pull request. + +**What it does:** + +1. Checks out your app's source. +2. Spawns an isolated Twenty test instance using the `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` composite action (the CI equivalent of `yarn twenty server start --test`). +3. Enables Corepack, sets up Node.js from your `.nvmrc`, and installs dependencies with `yarn install --immutable`. +4. Runs `yarn test`, passing `TWENTY_API_URL` and `TWENTY_API_KEY` from the spawned instance so your tests can talk to a real server. + +**Config knobs:** + +* `TWENTY_VERSION` (env, defaults to `latest`) — pin the Twenty server version used in CI by editing this in `ci.yml`. +* Concurrency is grouped by `github.ref` and cancels in-progress runs on new pushes. + +No secrets are required — the test instance is ephemeral and lives only for the duration of the job. + +### CD — `cd.yml` + +Deploys your app to a configured Twenty server on every push to `main`, and optionally from a pull request when the `deploy` label is applied. + +**What it does:** + +1. Checks out the PR head (for labeled PRs) or the pushed commit. +2. Runs `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — the CI equivalent of `yarn twenty deploy`. +3. Runs `twentyhq/twenty/.github/actions/install-twenty-app@main` so the newly deployed version is installed into the target workspace. + +**Required configuration:** + +| Setting | Where | Purpose | +| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `TWENTY_DEPLOY_URL` | `env` in `cd.yml` (defaults to `http://localhost:3000`) | The Twenty server to deploy to. Change this to your real server URL before first use. | +| `TWENTY_DEPLOY_API_KEY` | GitHub repo **Settings → Secrets and variables → Actions** | API key with deploy permission on the target server. | + + +The default `TWENTY_DEPLOY_URL` of `http://localhost:3000` is a placeholder — it will not reach anything from a GitHub-hosted runner. Update it to your server's public URL (or use a self-hosted runner with network access) before enabling CD. + + +**Triggering a preview deploy from a PR:** + +Add the `deploy` label to a pull request. The `if:` guard in `cd.yml` will run the job for that PR using the PR's head commit, letting you validate a change on the target server before merging. + +### Pinning the reusable actions + +Both workflows reference reusable actions at `@main`, so action updates in the `twentyhq/twenty` repo are picked up automatically. If you want deterministic builds, replace `@main` with a commit SHA or release tag on each `uses:` line. + +## Publishing to npm + +Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twenty workspace can browse, install, and upgrade marketplace apps directly from the UI. + +### Requirements + +* An [npm](https://www.npmjs.com) account +* The `twenty-app` keyword in your `package.json` `keywords` array (add it manually — it is not included by default in the `create-twenty-app` template) + +```json filename="package.json" +{ + "name": "twenty-app-postcard-sender", + "version": "1.0.0", + "keywords": ["twenty-app"] +} +``` + +### Marketplace metadata + +The `defineApplication()` config supports optional fields that control how your app appears in the marketplace. Use `logoUrl` and `screenshots` to reference images from the `public/` folder: + +```ts src/application-config.ts +export default defineApplication({ + universalIdentifier: '...', + displayName: 'My App', + description: 'A great app', + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + logoUrl: 'public/logo.png', + screenshots: [ + 'public/screenshot-1.png', + 'public/screenshot-2.png', + ], +}); +``` + +See the [defineApplication accordion](/l/ru/developers/extend/apps/config/application#marketplace-metadata) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.). + +#### Recommended screenshot dimensions + +The marketplace renders `screenshots` in a fixed `8:5` container (for example, `1600×1000 px`). + + +Screenshots of any aspect ratio are displayed in full and are never cropped, but anything significantly taller or narrower than `8:5` will show empty bands on the sides. + + +### Publish + +```bash filename="Terminal" +yarn twenty publish +``` + +To publish under a specific dist-tag (e.g., `beta` or `next`): + +```bash filename="Terminal" +yarn twenty publish --tag beta +``` + +### How marketplace discovery works + +The Twenty server syncs its marketplace catalog from the npm registry **every hour**. + +You can trigger the sync immediately instead of waiting: + +```bash filename="Terminal" +yarn twenty server catalog-sync +# To target a specific remote: +# yarn twenty server catalog-sync --remote production +``` + +The metadata shown in the marketplace comes from your `defineApplication()` config — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`. + + +If your app does not define an `aboutDescription` in `defineApplication()`, the marketplace will automatically use your package's `README.md` from npm as the about page content. This means you can maintain a single README for both npm and the Twenty marketplace. If you want a different description in the marketplace, explicitly set `aboutDescription`. + + +### CI publishing + +Use this GitHub Actions workflow to publish automatically on every release (uses [OIDC](https://docs.npmjs.com/trusted-publishers)): + +```yaml filename=".github/workflows/publish.yml" +name: Publish +on: + release: + types: [published] + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: https://registry.npmjs.org + - run: yarn install --immutable + - run: npx twenty build + - run: npm publish --provenance --access public + working-directory: .twenty/output +``` + +For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `yarn twenty build`, then `npm publish` from `.twenty/output`. + + +**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions. + + +## Installing apps + +Once an app is published (npm) or deployed (tarball), workspaces can install it through the UI. + +Go to the **Settings > Applications** page in Twenty, where both marketplace and tarball-deployed apps can be browsed and installed. + +{/* TODO: add screenshot of the UI when the app is registered */} + +You can also install apps from the command line: + +```bash filename="Terminal" +yarn twenty install +``` + + +The server enforces semver versioning on install, mirroring the rules on deploy: + +* Installing the same version that is already installed in your workspace is rejected with an `APP_ALREADY_INSTALLED` error. +* Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error. + +To install a newer version, deploy or publish it first, then re-run `yarn twenty install`. + diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/operations/testing.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/operations/testing.mdx new file mode 100644 index 0000000000..b1959be0ed --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/operations/testing.mdx @@ -0,0 +1,301 @@ +--- +title: Testing +description: Vitest setup, integration tests against a real Twenty server, type checking, and CI with GitHub Actions. +icon: flask +--- + +The SDK provides programmatic APIs that let you build, deploy, install, and uninstall your app from test code. Combined with [Vitest](https://vitest.dev/) and the typed API clients, you can write integration tests that verify your app works end-to-end against a real Twenty server. + +## Using npm packages + +You can install and use any npm package in your app. Both logic functions and front components are bundled with [esbuild](https://esbuild.github.io/), which inlines all dependencies into the output — no `node_modules` are needed at runtime. + +### Installing a package + +```bash filename="Terminal" +yarn add axios +``` + +Then import it in your code: + +```ts src/logic-functions/fetch-data.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import axios from 'axios'; + +const handler = async (): Promise => { + const { data } = await axios.get('https://api.example.com/data'); + + return { data }; +}; + +export default defineLogicFunction({ + universalIdentifier: '...', + name: 'fetch-data', + description: 'Fetches data from an external API', + timeoutSeconds: 10, + handler, +}); +``` + +The same works for front components: + +```tsx src/front-components/chart.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { format } from 'date-fns'; + +const DateWidget = () => { + return

Today is {format(new Date(), 'MMMM do, yyyy')}

; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### How bundling works + +The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle. + +**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed. + +**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment. + +Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server. + +## Setup + +The scaffolded app already includes Vitest. If you set it up manually, install the dependencies: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Create a `vitest.config.ts` at the root of your app: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Create a setup file that verifies the server is reachable before tests run: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## Programmatic SDK APIs + +The `twenty-sdk/cli` subpath exports functions you can call directly from test code: + +| Function | Description | +| -------------- | ------------------------------------------- | +| `appBuild` | Build the app and optionally pack a tarball | +| `appDeploy` | Upload a tarball to the server | +| `appInstall` | Install the app on the active workspace | +| `appUninstall` | Uninstall the app from the active workspace | + +Each function returns a result object with `success: boolean` and either `data` or `error`. + +## Writing an integration test + +Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Running tests + +Make sure your local Twenty server is running, then: + +```bash filename="Terminal" +yarn test +``` + +Or in watch mode during development: + +```bash filename="Terminal" +yarn test:watch +``` + +## Type checking + +You can also run type checking on your app without running tests: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +This runs `tsc --noEmit` and reports any type errors. + +## CI with GitHub Actions + +The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests. + +The workflow: + +1. Checks out your code +2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action +3. Installs dependencies with `yarn install --immutable` +4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub. + +To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/config/application.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..633d12dc0f --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/config/application.mdx @@ -0,0 +1,65 @@ +--- +title: Application Config +description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. +icon: rocket +--- + +Every app must have exactly one `defineApplication` call. It declares: + +* **Identity** — universal identifier, display name, description. +* **Permissions** — which role its logic functions and front components run under. +* **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. +* **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/l/tr/developers/extend/apps/logic/logic-functions). + +```ts src/application-config.ts +import { defineApplication } from 'twenty-sdk/define'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; + +export default defineApplication({ + universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', + displayName: 'My Twenty App', + description: 'My first Twenty app', + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', + description: 'Default recipient name for postcards', + value: 'Jane Doe', + isSecret: false, + }, + }, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). +* `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/l/tr/developers/extend/apps/config/roles). +* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. + +## Default function role + +The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: + +* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. +* The typed API client is restricted to the permissions granted to that role. +* Follow least-privilege: declare only the permissions your functions need. + +When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/l/tr/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/l/tr/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `author` | Author or company name | +| `category` | App category for marketplace filtering | +| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) | +| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) | +| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | +| `websiteUrl` | Link to your website | +| `termsUrl` | Link to terms of service | +| `emailSupport` | Support email address | +| `issueReportUrl` | Link to issue tracker | diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..cb66b87a8e --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/config/install-hooks.mdx @@ -0,0 +1,206 @@ +--- +title: Install Hooks +description: Run logic before or after the install — seed data, back up records, validate the upgrade. +icon: wrench +--- + +Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/l/tr/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload`, but they're declared with their own define functions — `definePostInstallLogicFunction()` and `definePreInstallLogicFunction()` — and live outside the normal trigger model (HTTP, cron, database events). + +Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build will error if more than one of either is detected. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ install flow │ +│ │ +│ upload package → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +│ │ +│ old schema visible new schema visible │ +└─────────────────────────────────────────────────────────────┘ +``` + + + + +A post-install function runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services. + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Post install logic function executed successfully!', payload.previousVersion); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Runs after installation to set up the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + shouldRunSynchronously: false, + handler, +}); +``` + +You can also manually execute the post-install function at any time using the CLI: + +```bash filename="Terminal" +yarn twenty exec --postInstall +``` + +Key points: +* Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`). +* The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic. +* **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook. +* **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed. + * `shouldRunSynchronously: false` *(default)* — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window. + * `shouldRunSynchronously: true` — the hook is executed **inline during the install flow** (same executor as pre-install). The install request blocks until the handler finishes, and if it throws, the install caller receives a `POST_INSTALL_ERROR`. No automatic retries. **Use this for fast, must-complete-before-response work** — for example, emitting a validation error to the user, or quick setup that the client will rely on immediately after the install call returns. Keep in mind the metadata migration has already been applied by the time post-install runs, so a sync-mode failure does **not** roll back the schema changes — it only surfaces the error. +* Make sure your handler is idempotent. In async mode the queue may retry up to three times; in either mode the hook may run again on upgrades when `shouldRunOnVersionUpgrade: true`. +* The environment variables `APPLICATION_ID`, `APP_ACCESS_TOKEN`, and `API_URL` are available inside the handler (same as any other logic function), so you can call the Twenty API with an application access token scoped to your app. +* Only one post-install function is allowed per application. The manifest build will error if more than one is detected. +* The function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in [`defineApplication()`](/l/tr/developers/extend/apps/config/application). +* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding. +* **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` to trigger it manually against a running workspace. + + + + +A pre-install function runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped. + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Pre install logic function executed successfully!', payload.previousVersion); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Runs before installation to prepare the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +You can also manually execute the pre-install function at any time using the CLI: + +```bash filename="Terminal" +yarn twenty exec --preInstall +``` + +Key points: +* Pre-install functions use `definePreInstallLogicFunction()` — same specialized config as post-install, just attached to a different lifecycle slot. +* Both pre- and post-install handlers receive the same `InstallPayload` type: `{ previousVersion?: string; newVersion: string }`. Import it once and reuse it for both hooks. +* **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state. +* **Execution model**: pre-install is executed **synchronously** and **blocks the install**. If the handler throws, the install is aborted before any schema changes are applied — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade. +* As with post-install, only one pre-install function is allowed per application. It is attached to the application manifest under `preInstallLogicFunction` automatically during the build. +* **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty exec --preInstall` to trigger it manually. + + + + +Both hooks are part of the same install flow and receive the same `InstallPayload`. The difference is **when** they run relative to the workspace metadata migration, and that changes what data they can safely touch. + +Pre-install is always **synchronous** (it blocks the install and can abort it). Post-install is **asynchronous by default** — enqueued on a worker with automatic retries — but can opt into synchronous execution with `shouldRunSynchronously: true`. See the `definePostInstallLogicFunction` accordion above for when to use each mode. + +**Use `post-install` for anything that needs the new schema to exist.** This is the common case: + +* Seeding default data (creating initial records, default views, demo content) against newly-added objects and fields. +* Registering webhooks with third-party services now that the app has its credentials. +* Calling your own API to finish setup that depends on the synchronized metadata. +* Idempotent "ensure this exists" logic that should reconcile state on every upgrade — combine with `shouldRunOnVersionUpgrade: true`. + +Example — seed a default `PostCard` record after install: + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion }: InstallPayload): Promise => { + if (previousVersion) return; // fresh installs only + + const client = createClient(); + await client.postCard.create({ + data: { title: 'Welcome to Postcard', content: 'Your first card!' }, + }); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Seeds a welcome post card after install.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + handler, +}); +``` + +**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the *previous* schema and its failure rolls back the upgrade, it is the right place for anything risky: + +* **Backing up data that is about to be dropped or restructured** — e.g. you are removing a field in v2 and need to copy its values into another field or export them to storage before the migration runs. +* **Archiving records that a new constraint would invalidate** — e.g. a field is becoming `NOT NULL` and you need to delete or fix rows with null values first. +* **Validating compatibility and refusing the upgrade if the current data cannot be migrated cleanly** — throw from the handler and the install aborts with no changes applied. This is safer than discovering the incompatibility mid-migration. +* **Renaming or rekeying data** ahead of a schema change that would lose the association. + +Example — archive records before a destructive migration: + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise => { + // Only the 1.x → 2.x upgrade drops the legacy `notes` field. + if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) { + return; + } + + const client = createClient(); + const legacyRecords = await client.postCard.findMany({ + where: { notes: { isNotNull: true } }, + }); + + if (legacyRecords.length === 0) return; + + // Copy legacy `notes` into the new `description` field before the migration + // drops the `notes` column. If this fails, the upgrade is aborted and the + // workspace stays on v1 with all data intact. + await Promise.all( + legacyRecords.map((record) => + client.postCard.update({ + where: { id: record.id }, + data: { description: record.notes }, + }), + ), + ); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Backs up legacy notes into description before the v2 migration.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +**Rule of thumb:** + +| You want to... | Use | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Seed default data, configure the workspace, register external resources | `post-install` | +| Run long-running seeding or third-party calls that shouldn't block the install response | `post-install` (default — `shouldRunSynchronously: false`, with worker retries) | +| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` | +| Read or back up data that the upcoming migration would lose | `pre-install` | +| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) | +| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` | +| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) | + + +If in doubt, default to **post-install**. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone. + + + + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/config/overview.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/config/overview.mdx new file mode 100644 index 0000000000..2e9688e102 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/config/overview.mdx @@ -0,0 +1,51 @@ +--- +title: Overview +description: Configure the app itself — its identity, default permissions, and what runs at install time. +icon: screwdriver-wrench +--- + +A Twenty app's **config layer** is what describes the app *to the platform* — its identity, the permissions it holds, and the code that runs during install or upgrade. These declarations don't add new data shapes or runtime behavior; they tell Twenty *who the app is* and *how to set it up*. + +```text +┌────────────────────────────────────────────────────────┐ +│ Application — identity, default role, variables, │ +│ marketplace metadata │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Role — what the app's logic functions can read │ │ +│ │ and write (referenced by Application) │ │ +│ └──────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────┘ + │ + ▼ (at install / upgrade time) + ┌──────────────────────────────────┐ + │ Pre-install hook │ before metadata migration + └──────────────────────────────────┘ + ┌──────────────────────────────────┐ + │ Post-install hook │ after metadata migration + └──────────────────────────────────┘ +``` + +## In this section + + + + `defineApplication` — identity, default role, variables, marketplace metadata. + + + `defineRole` — declare what your app's logic functions can read and write. + + + `definePreInstallLogicFunction` and `definePostInstallLogicFunction` — back up data, seed defaults, validate upgrades. + + + +## How the pieces relate + +* **Application** is the entry point. Every app has exactly one `defineApplication()` call, and it points at one **Role** as its default. +* The **Role** controls what the app's logic functions and front components can read and write. Follow least-privilege: only grant the permissions your code actually needs. +* **Install Hooks** run during install or upgrade — pre-install before the metadata migration (so it can refuse a risky upgrade), post-install after the migration (so it can seed default data against the new schema). + + +Install hooks share the [logic function](/l/tr/developers/extend/apps/logic/logic-functions) runtime — same handler signature, same environment variables, same typed API client — but they're declared with their own define functions and live outside the regular trigger model (HTTP, cron, database events). + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/config/public-assets.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/config/public-assets.mdx new file mode 100644 index 0000000000..338c0f7f8e --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/config/public-assets.mdx @@ -0,0 +1,59 @@ +--- +title: Public Assets +description: Ship static files — images, icons, fonts — alongside your app via the public/ folder. +icon: folder-open +--- + +The `public/` folder at the root of your app holds static files — images, icons, fonts, or any other assets your app needs at runtime. These files are automatically included in builds, synced during dev mode, and uploaded to the server. + +Files placed in `public/` are: + +* **Publicly accessible** — once synced to the server, assets are served at a public URL. No authentication is needed to access them. +* **Available in front components** — use asset URLs to display images, icons, or any media inside your React components. +* **Available in logic functions** — reference asset URLs in emails, API responses, or any server-side logic. +* **Used for marketplace metadata** — the `logoUrl` and `screenshots` fields in `defineApplication()` reference files from this folder (e.g., `public/logo.png`). These are displayed in the marketplace when your app is published. +* **Auto-synced in dev mode** — when you add, update, or delete a file in `public/`, it is synced to the server automatically. No restart needed. +* **Included in builds** — `yarn twenty build` bundles all public assets into the distribution output. + +## Accessing public assets with `getPublicAssetUrl` + +Use the `getPublicAssetUrl` helper from `twenty-sdk` to get the full URL of a file in your `public/` directory. It works in both **logic functions** and **front components**. + +**In a logic function:** + +```ts src/logic-functions/send-invoice.ts +import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define'; + +const handler = async (): Promise => { + const logoUrl = getPublicAssetUrl('logo.png'); + const invoiceUrl = getPublicAssetUrl('templates/invoice.png'); + + // Fetch the file content (no auth required — public endpoint) + const response = await fetch(invoiceUrl); + const buffer = await response.arrayBuffer(); + + return { logoUrl, size: buffer.byteLength }; +}; + +export default defineLogicFunction({ + universalIdentifier: 'a1b2c3d4-...', + name: 'send-invoice', + description: 'Sends an invoice with the app logo', + timeoutSeconds: 10, + handler, +}); +``` + +**In a front component:** + +```tsx src/front-components/company-card.tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +export default defineFrontComponent(() => { + const logoUrl = getPublicAssetUrl('logo.png'); + + return App logo; +}); +``` + +The `path` argument is relative to your app's `public/` folder. Both `getPublicAssetUrl('logo.png')` and `getPublicAssetUrl('public/logo.png')` resolve to the same URL — the `public/` prefix is stripped automatically if present. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/config/roles.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/config/roles.mdx new file mode 100644 index 0000000000..18813be9dc --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/config/roles.mdx @@ -0,0 +1,90 @@ +--- +title: Roles & Permissions +description: Declare what objects and fields your app's logic functions and front components can read and write. +icon: shield-halved +--- + +A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role declared as `defaultRoleUniversalIdentifier` in [`defineApplication`](/l/tr/developers/extend/apps/config/application). + +```ts src/roles/restricted-company-role.ts +import { + defineRole, + PermissionFlag, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; + +export default defineRole({ + universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6', + label: 'My new role', + description: 'A role that can be used in your workspace', + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + fieldPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + fieldUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier, + canReadFieldValue: false, + canUpdateFieldValue: false, + }, + ], + permissionFlags: [PermissionFlag.APPLICATIONS], +}); +``` + +## The default function role + +When you scaffold a new app, the CLI creates a default role file: + +```ts src/roles/default-role.ts +import { defineRole, PermissionFlag } from 'twenty-sdk/define'; + +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = + 'b648f87b-1d26-4961-b974-0908fd991061'; + +export default defineRole({ + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + label: 'Default function role', + description: 'Default role for function Twenty client', + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [], + fieldPermissions: [], + permissionFlags: [], +}); +``` + +This role's `universalIdentifier` is referenced from `application-config.ts` as `defaultRoleUniversalIdentifier`: + +* **`*.role.ts`** declares what the role can do. +* **`application-config.ts`** points to that role so your functions inherit its permissions. + +## Best practices + +* Start from the scaffolded role, then progressively restrict it — the default grants broad read access, which is rarely what you want in production. +* Replace `objectPermissions` and `fieldPermissions` with the exact objects and fields your functions actually need. +* `permissionFlags` control access to platform-level capabilities. Keep them minimal. +* See a working example: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts). diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/data/extending-objects.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/data/extending-objects.mdx new file mode 100644 index 0000000000..488804e5b4 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/data/extending-objects.mdx @@ -0,0 +1,48 @@ +--- +title: Extending Objects +description: Add fields to standard Twenty objects (Person, Company, …) or to objects from other apps using defineField. +icon: wand-magic-sparkles +--- + +Use `defineField()` to add a field to an object you don't own — a standard Twenty object like Person or Company, or an object shipped by another installed app. Unlike inline fields declared inside [`defineObject`](/l/tr/developers/extend/apps/data/objects), standalone fields require an `objectUniversalIdentifier` to specify which object they extend. + +```ts src/fields/company-loyalty-tier.field.ts +import { defineField, FieldType } from 'twenty-sdk/define'; + +export default defineField({ + universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890', + objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object + name: 'loyaltyTier', + type: FieldType.SELECT, + label: 'Loyalty Tier', + icon: 'IconStar', + options: [ + { value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' }, + { value: 'SILVER', label: 'Silver', position: 1, color: 'gray' }, + { value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' }, + ], +}); +``` + +## Key points + +* `objectUniversalIdentifier` identifies the target object. For standard Twenty objects, import the constant from `twenty-sdk`: + + ```ts + import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define'; + + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier + // … + ``` + +* When defining fields **inline inside `defineObject()`**, you do **not** need `objectUniversalIdentifier` — it's inherited from the parent object. + +* `defineField()` is the only way to add fields to objects you didn't create with `defineObject()`. + +* File location is up to you. The convention is `src/fields/\.field.ts`, but the SDK detects fields anywhere in `src/`. + +## Adding a relation to an existing object + +To add a relation field (e.g. linking your custom object to a standard `Person`), use `defineField()` with `FieldType.RELATION`. The pattern is the same as for inline relations but with `objectUniversalIdentifier` set explicitly. See [Relations](/l/tr/developers/extend/apps/data/relations) for the bidirectional pattern. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/data/objects.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/data/objects.mdx new file mode 100644 index 0000000000..693e2f848f --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/data/objects.mdx @@ -0,0 +1,93 @@ +--- +title: Nesneler +description: Declare new record types — custom tables with their own fields — using defineObject. +icon: tablo +--- + +Custom **objects** are new record types your app adds to a workspace — Post Card, Invoice, Subscription, anything specific to your domain. Each object declares its schema (fields, relations, default values) and a stable universal identifier that survives across syncs and deploys. + +```ts src/objects/post-card.object.ts +import { defineObject, FieldType } from 'twenty-sdk/define'; + +enum PostCardStatus { + DRAFT = 'DRAFT', + SENT = 'SENT', + DELIVERED = 'DELIVERED', + RETURNED = 'RETURNED', +} + +export default defineObject({ + universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', + nameSingular: 'postCard', + namePlural: 'postCards', + labelSingular: 'Post Card', + labelPlural: 'Post Cards', + description: 'A post card object', + icon: 'IconMail', + fields: [ + { + universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b', + name: 'content', + type: FieldType.TEXT, + label: 'Content', + description: "Postcard's content", + icon: 'IconAbc', + }, + { + universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac', + name: 'recipientName', + type: FieldType.FULL_NAME, + label: 'Recipient name', + icon: 'IconUser', + }, + { + universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266', + name: 'recipientAddress', + type: FieldType.ADDRESS, + label: 'Recipient address', + icon: 'IconHome', + }, + { + universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e', + name: 'status', + type: FieldType.SELECT, + label: 'Status', + icon: 'IconSend', + defaultValue: `'${PostCardStatus.DRAFT}'`, + options: [ + { value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' }, + { value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' }, + { value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' }, + { value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' }, + ], + }, + { + universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433', + name: 'deliveredAt', + type: FieldType.DATE_TIME, + label: 'Delivered at', + icon: 'IconCheck', + isNullable: true, + defaultValue: null, + }, + ], +}); +``` + +## Önemli noktalar + +* `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. +* Inline fields defined here do **not** need an `objectUniversalIdentifier` — it's inherited from the parent object. Use [`defineField()`](/l/tr/developers/extend/apps/data/extending-objects) to add fields to objects you don't own. +* You can scaffold new objects with `yarn twenty add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/l/tr/developers/extend/apps/getting-started/scaffolding). + + +**Base fields are added automatically.** When you define a custom object, Twenty creates standard fields like `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and `deletedAt` for you. You don't need to declare them in your `fields` array — only your custom fields. You can override a default field by declaring one with the same name, but this is rarely a good idea. + + +## Sırada ne var + +* **Connect this object to others** — see [Relations](/l/tr/developers/extend/apps/data/relations) for the bidirectional relation pattern. +* **Add fields to objects from other apps** — see [Extending Objects](/l/tr/developers/extend/apps/data/extending-objects) for `defineField()`. +* **Display this object in the UI** — see [Views](/l/tr/developers/extend/apps/layout/views) and [Navigation Menu Items](/l/tr/developers/extend/apps/layout/navigation-menu-items) to put it in the sidebar. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/data/overview.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/data/overview.mdx new file mode 100644 index 0000000000..6a70594074 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/data/overview.mdx @@ -0,0 +1,52 @@ +--- +title: Overview +description: Shape the data your app adds to a workspace — objects, fields, and relations. +icon: database +--- + +A Twenty app's **data layer** is the data your app *adds* to a workspace — the new record types it declares, the columns it adds to existing objects, and how those records connect to each other. + +```text +┌──────────────────────────────────────────────────┐ +│ Object — a record type, e.g. PostCard │ +│ ├─ Field (name, type, label) │ +│ ├─ Field │ +│ └─ Relation (link to another object) │ +└──────────────────────────────────────────────────┘ + │ + ├── lives in your app, OR + │ + ▼ +┌──────────────────────────────────────────────────┐ +│ Standard / other apps' objects │ +│ └─ Field added by your app via defineField │ +└──────────────────────────────────────────────────┘ +``` + +## In this section + + + + `defineObject` — declare new record types with their own fields. + + + `defineField` — add fields to standard or other apps' objects. + + + Bidirectional `MANY_TO_ONE` / `ONE_TO_MANY` connections between objects. + + + +## Entities at a glance + +| Entity | Purpose | Defined with | +| ------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| **Object** | A new custom record type (e.g. PostCard, Invoice) with its own fields | `defineObject()` | +| **Field** | A column on an object. Standalone fields can extend objects you didn't create (e.g. add `loyaltyTier` to Company) | `defineField()` | +| **Relation** | A bidirectional link between two objects — both sides declared as fields | `defineField()` with `FieldType.RELATION` | + +The SDK detects these via AST analysis at build time, so file organization is up to you — the convention is `src/objects/` and `src/fields/`. Stable `universalIdentifier` UUIDs tie everything together across deploys. + + +Looking for **Application Config** or **Roles & Permissions**? Those describe the app itself rather than the data it adds — they live under [Config](/l/tr/developers/extend/apps/config/overview). Looking for **Connections** (Linear, GitHub, Slack OAuth)? Those exist to be called *from* logic functions and live under [Logic](/l/tr/developers/extend/apps/logic/connections). + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/data/relations.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/data/relations.mdx new file mode 100644 index 0000000000..3bd7ea11a9 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/data/relations.mdx @@ -0,0 +1,160 @@ +--- +title: İlişkiler +description: Connect objects together with bidirectional MANY_TO_ONE / ONE_TO_MANY relations. +icon: diagram-project +--- + +Relations connect two objects together. In Twenty, relations are always **bidirectional** — every relation has two sides, and each side is declared as a field that references the other. + +| İlişki türü | Açıklama | Yabancı anahtar var mı? | +| ------------- | --------------------------------------------------------- | ----------------------- | +| `MANY_TO_ONE` | Bu nesnenin birçok kaydı, hedefin bir kaydını işaret eder | Evet (`joinColumnName`) | +| `ONE_TO_MANY` | Bu nesnenin bir kaydı, hedefin birçok kaydına sahiptir | No (the inverse side) | + +## How relations work + +Every relation requires **two fields** that reference each other: + +1. The **MANY_TO_ONE** side — lives on the object that holds the foreign key. +2. The **ONE_TO_MANY** side — lives on the object that owns the collection. + +Both fields use `FieldType.RELATION` and cross-reference each other via `relationTargetFieldMetadataUniversalIdentifier`. + +## Example: Post Card has many Recipients + +A `PostCard` can be sent to many `PostCardRecipient` records. Each recipient belongs to exactly one post card. + +**Step 1: Define the ONE_TO_MANY side on PostCard** (the "one" side): + +```ts src/fields/post-card-recipients-on-post-card.field.ts +import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111'; +// Import from the other side +import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field'; + +export default defineField({ + universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCardRecipients', + label: 'Post Card Recipients', + icon: 'IconUsers', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, +}); +``` + +**Step 2: Define the MANY_TO_ONE side on PostCardRecipient** (the "many" side — holds the foreign key): + +```ts src/fields/post-card-on-post-card-recipient.field.ts +import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222'; +// Import from the other side +import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field'; + +export default defineField({ + universalIdentifier: POST_CARD_FIELD_ID, + objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + icon: 'IconMail', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, +}); +``` + + +**Circular imports:** both relation fields reference each other's `universalIdentifier`. To avoid circular import issues, export your field IDs as named constants from each file and import them in the other. The build system resolves these at compile time. + + +## Relating to standard objects + +To create a relation with a built-in Twenty object (Person, Company, etc.), use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`: + +```ts src/fields/person-on-self-hosting-user.field.ts +import { + defineField, + FieldType, + RelationType, + OnDeleteAction, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; +import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object'; + +export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333'; +export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444'; + +export default defineField({ + universalIdentifier: PERSON_FIELD_ID, + objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'person', + label: 'Person', + description: 'Person matching with the self hosting user', + isNullable: true, + relationTargetObjectMetadataUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, + relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.SET_NULL, + joinColumnName: 'personId', + }, +}); +``` + +## Relation field properties + +| Property | Required | Description | +| ------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------- | +| `type` | Yes | Must be `FieldType.RELATION` | +| `relationTargetObjectMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the target object | +| `relationTargetFieldMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the matching field on the target object | +| `universalSettings.relationType` | Yes | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` | +| `universalSettings.onDelete` | MANY_TO_ONE only | What happens when the referenced record is deleted: `CASCADE`, `SET_NULL`, `RESTRICT`, or `NO_ACTION` | +| `universalSettings.joinColumnName` | MANY_TO_ONE only | Database column name for the foreign key (e.g., `postCardId`) | + +## Inline relation fields + +You can also declare a relation directly inside [`defineObject`](/l/tr/developers/extend/apps/data/objects). When inline, omit `objectUniversalIdentifier` — it's inherited from the parent object: + +```ts +export default defineObject({ + universalIdentifier: '...', + nameSingular: 'postCardRecipient', + // ... + fields: [ + { + universalIdentifier: POST_CARD_FIELD_ID, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, + }, + // … other fields + ], +}); +``` diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/concepts.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/concepts.mdx new file mode 100644 index 0000000000..68259ab5b2 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/concepts.mdx @@ -0,0 +1,101 @@ +--- +title: Concepts +description: How Twenty apps work — entity model, sandboxing, and the install lifecycle. +icon: sitemap +--- + +Twenty apps are TypeScript packages that extend your workspace with custom objects, logic, UI components, and AI capabilities. They run on the Twenty platform with full sandboxing and permission controls. + +## How apps work + +An app is a collection of **entities** declared using `defineEntity()` functions from the `twenty-sdk` package. The SDK detects these declarations via AST analysis at build time and produces a **manifest** — a complete description of what your app adds to a workspace. These functions validate your configuration at build time and provide IDE autocompletion and type safety. + +``` +your-app/ +├── src/ +│ ├── application-config.ts ← defineApplication (required, one per app) +│ ├── roles/ ← defineRole +│ ├── objects/ ← defineObject +│ ├── fields/ ← defineField +│ ├── logic-functions/ ← defineLogicFunction +│ ├── front-components/ ← defineFrontComponent +│ ├── skills/ ← defineSkill +│ ├── agents/ ← defineAgent +│ ├── views/ ← defineView +│ ├── navigation-menu-items/ ← defineNavigationMenuItem +│ └── page-layouts/ ← definePageLayout +├── public/ ← Static assets (images, icons) +└── package.json +``` + + + **File organization is up to you.** Entity detection is AST-based — the SDK finds `export default defineEntity(...)` calls regardless of where the file lives. The folder structure above is a convention, not a requirement. + + +## Entity types + +| Entity | Purpose | Docs | +| ------------------------ | ------------------------------------------ | ----------------------------------------------------------------------------- | +| **Application** | App identity, default role, variables | [Application Config](/l/tr/developers/extend/apps/config/application) | +| **Role** | Permission sets on objects and fields | [Roles & Permissions](/l/tr/developers/extend/apps/config/roles) | +| **Object** | Custom record types with fields | [Objects](/l/tr/developers/extend/apps/data/objects) | +| **Field** | Add fields to objects from other apps | [Extending Objects](/l/tr/developers/extend/apps/data/extending-objects) | +| **Relation** | Bidirectional links between objects | [Relations](/l/tr/developers/extend/apps/data/relations) | +| **Logic Function** | Server-side TypeScript with triggers | [Logic Functions](/l/tr/developers/extend/apps/logic/logic-functions) | +| **Skill** | Reusable AI agent instructions | [Skills & Agents](/l/tr/developers/extend/apps/logic/skills-and-agents) | +| **Agent** | AI assistants with custom prompts | [Skills & Agents](/l/tr/developers/extend/apps/logic/skills-and-agents) | +| **Connection Provider** | OAuth credentials for third-party APIs | [Connections](/l/tr/developers/extend/apps/logic/connections) | +| **View** | Pre-configured record list views | [Views](/l/tr/developers/extend/apps/layout/views) | +| **Navigation Menu Item** | Custom sidebar entries | [Navigation Menu Items](/l/tr/developers/extend/apps/layout/navigation-menu-items) | +| **Page Layout** | Tabs and widgets on a record's detail page | [Page Layouts](/l/tr/developers/extend/apps/layout/page-layouts) | +| **Front Component** | Sandboxed React UI inside Twenty | [Front Components](/l/tr/developers/extend/apps/layout/front-components) | +| **Command Menu Item** | Quick actions and Cmd+K entries | [Command Menu Items](/l/tr/developers/extend/apps/layout/command-menu-items) | + +## Sandboxing + +* **Logic functions** run in isolated Node.js processes on the server. They only access data through the typed API client, scoped to the app's role permissions. +* **Front components** run in Web Workers using Remote DOM — sandboxed from the main page but rendering native DOM elements (not iframes). They communicate with Twenty via a message-passing host API. +* **Permissions** are enforced at the API level. The runtime token (`TWENTY_APP_ACCESS_TOKEN`) is derived from the role defined in `defineApplication()`. + +## App lifecycle + +``` +┌─────────────────────────────────────────────────────────┐ +│ Development │ +│ npx create-twenty-app → yarn twenty dev (live sync) │ +├─────────────────────────────────────────────────────────┤ +│ Build & Deploy │ +│ yarn twenty build → yarn twenty deploy │ +├─────────────────────────────────────────────────────────┤ +│ Install flow │ +│ upload → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +├─────────────────────────────────────────────────────────┤ +│ Publish │ +│ npm publish → appears in Twenty marketplace │ +└─────────────────────────────────────────────────────────┘ +``` + +* **`yarn twenty dev`** — watches your source files and live-syncs changes to a connected Twenty server. The typed API client is regenerated automatically when the schema changes. +* **`yarn twenty build`** — compiles TypeScript, bundles logic functions and front components with esbuild, and produces a manifest. +* **Pre/post-install hooks** — optional functions that run during installation. See [Install Hooks](/l/tr/developers/extend/apps/config/install-hooks) for details. + +## Next steps + + + + Application identity, default role, and install hooks. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/local-server.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/local-server.mdx new file mode 100644 index 0000000000..a424e5203a --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/local-server.mdx @@ -0,0 +1,72 @@ +--- +title: Local Server +description: Manage the local Twenty Docker server — start, stop, upgrade, parallel test instance, and manual SDK setup. +icon: server +--- + +## Managing the local server + +Use `yarn twenty server` to control the local Twenty container: + +| Command | What it does | +| -------------------------------------- | -------------------------------------------- | +| `yarn twenty server start` | Start the server (pulls the image if needed) | +| `yarn twenty server start --port 3030` | Start on a custom port | +| `yarn twenty server stop` | Stop the server (preserves data) | +| `yarn twenty server status` | Show URL, version, and login credentials | +| `yarn twenty server logs` | Stream server logs | +| `yarn twenty server reset` | Wipe data and start fresh | +| `yarn twenty server upgrade` | Pull the latest `twenty-app-dev` image | +| `yarn twenty server upgrade 2.2.0` | Upgrade to a specific version | + +Data persists across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything. + +## Upgrading the server image + +`yarn twenty server upgrade` pulls the latest image, compares digests, and only recreates the container if anything actually changed. Volumes are preserved — only the container is replaced. If a new image was pulled and the container was running, the upgrade automatically starts a new container; run `yarn twenty server start` afterward to wait for it to become healthy. + +```bash filename="Terminal" +yarn twenty server upgrade # Latest +yarn twenty server upgrade 2.2.0 # Specific version +``` + +Verify the running version with `yarn twenty server status` (it shows the `APP_VERSION` baked into the container). + +## Running a parallel test instance + +Pass `--test` to any `server` command to manage a second, fully isolated instance — useful for integration tests or experiments without touching your main dev data: + +| Command | What it does | +| ----------------------------------- | ----------------------------------------------- | +| `yarn twenty server start --test` | Start the test instance (defaults to port 2021) | +| `yarn twenty server stop --test` | Stop it | +| `yarn twenty server status --test` | Show its status | +| `yarn twenty server logs --test` | Stream its logs | +| `yarn twenty server reset --test` | Wipe its data | +| `yarn twenty server upgrade --test` | Upgrade its image | + +The test instance has its own container (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`), and config — it runs alongside your main instance without conflicts. Combine `--test` with `--port` to override 2021. + +## Manual setup (without the scaffolder) + +Skip the scaffolder if you're adding the SDK to an existing project: + +```bash filename="Terminal" +yarn add twenty-sdk twenty-client-sdk +``` + +Add the script to `package.json`: + +```json filename="package.json" +{ + "scripts": { + "twenty": "twenty" + } +} +``` + +You can now run `yarn twenty dev`, `yarn twenty server start`, and the rest. + + +Don't install `twenty-sdk` globally — pin it per project so each app uses its own version. + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/project-structure.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/project-structure.mdx new file mode 100644 index 0000000000..ad85a70452 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/project-structure.mdx @@ -0,0 +1,40 @@ +--- +title: Project Structure +description: What's inside a scaffolded Twenty app — files, folders, and what each one does. +icon: folder-tree +--- + +A new app generated by `npx create-twenty-app` looks like this: + +```text filename="my-twenty-app/" +my-twenty-app/ + package.json + src/ + application-config.ts # Required — your app's entry point + default-role.ts # Permissions for logic functions + constants/ + universal-identifiers.ts # Auto-generated UUIDs and metadata + __tests__/ + setup-test.ts + app-install.integration-test.ts + .github/workflows/ci.yml # GitHub Actions + public/ # Static assets + vitest.config.ts # Test runner config + tsconfig.json, tsconfig.spec.json + .nvmrc, .yarnrc.yml, .oxlintrc.json + README.md, LLMS.md +``` + +## Key files + +| File / Folder | Purpose | +| ---------------------------------------- | -------------------------------------------------------------- | +| `src/application-config.ts` | **Required.** The main configuration file for your app. | +| `src/default-role.ts` | Default role controlling what your logic functions can access. | +| `src/constants/universal-identifiers.ts` | Auto-generated UUIDs and metadata (display name, description). | +| `src/__tests__/` | Integration tests (setup + example test). | +| `public/` | Static assets (images, fonts) served with your app. | + + +**File organization is up to you.** The folders above are conventions — the SDK detects entities via AST analysis on `export default defineEntity(...)` calls regardless of where the file lives. + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/quick-start.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/quick-start.mdx new file mode 100644 index 0000000000..85fae895e1 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/quick-start.mdx @@ -0,0 +1,184 @@ +--- +title: Quick Start +icon: rocket +description: Create your first Twenty app in minutes. +--- + +## Prerequisites + +* **Node.js 24+** — [Download](https://nodejs.org/) +* **Yarn 4** — bundled with Node via Corepack. Enable it: `corepack enable` +* **Docker** — [Download](https://www.docker.com/products/docker-desktop/). Needed to run a local Twenty server. Skip if you already have Twenty running elsewhere. + +Building a Twenty app has three phases. The scaffolder collapses them into one happy-path command, but each phase is a separate concept — when something fails, knowing which phase you're in tells you what to fix. + +| Phase | What you do | Tool | Result | +| ------------------- | ---------------------------------- | ----------------------------- | ----------------------------- | +| **1. Scaffold** | Generate the app's source code | `npx create-twenty-app` | A TypeScript project on disk | +| **2. Run a server** | Start a Twenty server to sync into | Docker + `yarn twenty server` | A running Twenty instance | +| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` | Your changes appear in the UI | + +--- + +## Phase 1 — Scaffold your project + +Create a new app from the template: + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app +``` + +You'll be prompted for a name and description — press **Enter** for the defaults. This generates a TypeScript project in `my-twenty-app/` with a starter `application-config.ts`, a default role, a CI workflow, and an integration test. + +**After this phase:** you have an app's source code on your machine. It isn't running yet — that's Phase 2. + +--- + +## Phase 2 — Run a local Twenty server + +Your app needs a Twenty server to sync into. The server is a full Twenty instance — UI, GraphQL API, PostgreSQL — running locally in Docker. Your local code uploads its definitions to that server, which makes them appear in the UI. + +The scaffolder offers to start one for you: + +> **Would you like to set up a local Twenty instance?** + +* **Yes (recommended)** — pulls the `twentycrm/twenty-app-dev` Docker image and starts it on port `2020`. Make sure Docker is running first. +* **No** — choose this if you already have a Twenty server you want to connect to. You can wire it up later with `yarn twenty remote add`. + +
+ Should start local instance? +
+ +Once the server is up, a browser opens for sign-in. Use the pre-seeded demo account: + +* **Email:** `tim@apple.dev` +* **Password:** `tim@apple.dev` + +
+ Twenty login screen +
+ +Click **Authorize** on the next screen — this gives the CLI access to your workspace. + +
+ Twenty CLI authorization screen +
+ +Your terminal will confirm everything is set up. + +
+ App scaffolded successfully +
+ +**After this phase:** you have a running Twenty server at [http://localhost:2020](http://localhost:2020) with your CLI authorized to sync to it. + + +If Docker isn't installed or running, the scaffolder will tell you the right start command for your OS. Once Docker is up, you can resume with `yarn twenty server start` — no need to re-scaffold. + + +--- + +## Phase 3 — Sync your changes + +This is the inner loop you'll spend most of your time in. + +```bash filename="Terminal" +cd my-twenty-app +yarn twenty dev +``` + +This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a second the server reflects the change. You'll see a live status panel in your terminal. + +For more detailed output (build logs, sync requests, error traces), add `--verbose`. + +
+ Dev mode terminal output +
+ +Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). You should see your app under **Your Apps**. + +
+ Your Apps list showing My twenty app +
+ +Click **My twenty app** to see its **application registration** — a server-level record describing your app (name, identifier, OAuth credentials, source). One registration can be installed across multiple workspaces on the same server. + +
+ Application registration details +
+ +Click **View installed app** to see the workspace install. The **About** tab shows version and management options. + +
+ Installed app +
+ +**After this phase:** you have a live development loop. Edit any file in `src/` and it appears in the UI. + +### One-shot sync for CI and scripts + +Pass `--once` to run a single build + sync and exit — same pipeline, no watcher: + +```bash filename="Terminal" +yarn twenty dev --once +``` + +| Command | Behavior | When to use | +| ------------------------ | ------------------------------------------------------------- | ---------------------------------------------------- | +| `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. | +| `yarn twenty dev --once` | Single build + sync, exits `0` on success, `1` on failure. | CI, pre-commit hooks, AI agents, scripted workflows. | + +Both modes need a server in development mode and an authenticated remote. + + +Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests — use `yarn twenty deploy` to deploy to production servers. See [Publishing](/l/tr/developers/extend/apps/operations/publishing). + + +--- + +## Starting from an example + +Use `--example` to start with a more complete project (custom objects, fields, logic functions, front components): + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app --example postcard +``` + +Examples live in [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). You can also scaffold individual entities into an existing project with `yarn twenty add` — see [Scaffolding](/l/tr/developers/extend/apps/getting-started/scaffolding). + +--- + +## What you can build + +Apps are composed of **entities** — each defined as a TypeScript file with a single `export default`: + +| Entity | What it does | +| ---------------------- | ----------------------------------------------------------------------------------- | +| **Objects & Fields** | Custom data models (Post Card, Invoice, etc.) with typed fields | +| **Logic functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events | +| **Front components** | React components that render inside Twenty's UI (side panel, widgets, command menu) | +| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants | +| **Views & Navigation** | Pre-configured list views and sidebar menu items | +| **Page layouts** | Custom record detail pages with tabs and widgets | + +Full reference: [Concepts](/l/tr/developers/extend/apps/getting-started/concepts). + +## Next steps + + + + Application identity, default role, install hooks, public assets. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/scaffolding.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/scaffolding.mdx new file mode 100644 index 0000000000..b2853cadf2 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/scaffolding.mdx @@ -0,0 +1,58 @@ +--- +title: Scaffolding +description: Generate entity files interactively with yarn twenty add — objects, fields, views, logic functions, and more. +icon: wand-magic-sparkles +--- + +Instead of creating entity files by hand, use the interactive scaffolder: + +```bash filename="Terminal" +yarn twenty add +``` + +It prompts you to pick an entity type and walks you through the required fields, then writes a ready-to-use file with a stable `universalIdentifier` and the correct `defineEntity()` call. + +You can also pass the entity type directly to skip the first prompt: + +```bash filename="Terminal" +yarn twenty add object +yarn twenty add logicFunction +yarn twenty add frontComponent +``` + +## Available entity types + +| Entity type | Command | Generated file | +| -------------------- | ------------------------------------ | ------------------------------------------------------- | +| Object | `yarn twenty add object` | `src/objects/\.ts` | +| Field | `yarn twenty add field` | `src/fields/\.ts` | +| Logic function | `yarn twenty add logicFunction` | `src/logic-functions/\.ts` | +| Front component | `yarn twenty add frontComponent` | `src/front-components/\.tsx` | +| Role | `yarn twenty add role` | `src/roles/\.ts` | +| Skill | `yarn twenty add skill` | `src/skills/\.ts` | +| Agent | `yarn twenty add agent` | `src/agents/\.ts` | +| View | `yarn twenty add view` | `src/views/\.ts` | +| Navigation menu item | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\.ts` | +| Page layout | `yarn twenty add pageLayout` | `src/page-layouts/\.ts` | + +## What the scaffolder generates + +Each entity type has its own template. For example, `yarn twenty add object` asks for: + +1. **Name (singular)** — e.g., `invoice` +2. **Name (plural)** — e.g., `invoices` +3. **Label (singular)** — auto-populated from the name (e.g., `Invoice`) +4. **Label (plural)** — auto-populated (e.g., `Invoices`) +5. **Create a view and navigation item?** — if you answer yes, the scaffolder also generates a matching view and sidebar link for the new object. + +Other entity types have simpler prompts — most only ask for a name. + +The `field` entity type is more detailed: it asks for the field name, label, type (from a list of all available field types like `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.), and the target object's `universalIdentifier`. + +## Custom output path + +Use the `--path` flag to place the generated file in a custom location: + +```bash filename="Terminal" +yarn twenty add logicFunction --path src/custom-folder +``` diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/troubleshooting.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/troubleshooting.mdx new file mode 100644 index 0000000000..7b2058eed8 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started/troubleshooting.mdx @@ -0,0 +1,12 @@ +--- +title: Sorun Giderme +description: Yaygın ilk çalıştırma sorunları — Docker, Node sürümü, Yarn, bağımlılıklar. +icon: anahtar +--- + +* **Docker hataları** — `yarn twenty server start` öncesinde Docker Desktop'ın (veya daemon'ın) çalıştığından emin olun. Hata iletisi, işletim sisteminiz için doğru başlatma komutunu gösterecektir. +* **Yanlış Node sürümü** — 24+ gerekir. `node -v` ile kontrol edin. +* **Yarn 4 eksik** — `corepack enable` çalıştırın. +* **Bağımlılıklar bozuk** — `rm -rf node_modules && yarn install`. + +Takıldınız mı? [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322) üzerinde yardım isteyin. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/layout/command-menu-items.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/layout/command-menu-items.mdx new file mode 100644 index 0000000000..f5ff947212 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/layout/command-menu-items.mdx @@ -0,0 +1,144 @@ +--- +title: Command Menu Items +description: Surface front components as quick actions and command menu (Cmd+K) entries with defineCommandMenuItem. +icon: terminal +--- + +A **command menu item** is the bridge between the user and a [front component](/l/tr/developers/extend/apps/layout/front-components). It registers the component in Twenty's command menu (Cmd+K) and, optionally, as a pinned quick-action button in the top-right corner of the page. + +```ts src/command-menu-items/open-dashboard.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + label: 'Open Dashboard', + shortLabel: 'Dashboard', + icon: 'IconLayoutDashboard', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +## Configuration fields + +| Field | Required | Description | +| --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `universalIdentifier` | Yes | Stable unique ID for the command | +| `label` | Yes | Full label shown in the command menu (Cmd+K) | +| `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens | +| `shortLabel` | No | Shorter label displayed on the pinned quick-action button | +| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) | +| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page | +| `availabilityType` | No | Controls where the command appears: `'GLOBAL'` (always available), `'RECORD_SELECTION'` (only when records are selected), or `'FALLBACK'` (shown when no other commands match) | +| `availabilityObjectUniversalIdentifier` | No | Restrict the command to pages of a specific object type (e.g. only on Company records) | +| `conditionalAvailabilityExpression` | No | A boolean expression that dynamically controls visibility (see below) | + +## Headless commands + +A command menu item paired with a [headless front component](/l/tr/developers/extend/apps/layout/front-components#headless-vs-non-headless) is the idiomatic way to ship a one-click action — run code, navigate, or confirm and execute. The Front Components page covers the [SDK Command components](/l/tr/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) that handle the action-and-unmount pattern. + +A typical flow: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +## Conditional availability expressions + +The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions: + +```ts src/command-menu-items/bulk-update.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; +import { + objectPermissions, + everyEquals, +} from 'twenty-sdk/front-component'; + +export default defineCommandMenuItem({ + universalIdentifier: '...', + label: 'Bulk Update', + availabilityType: 'RECORD_SELECTION', + frontComponentUniversalIdentifier: '...', + conditionalAvailabilityExpression: everyEquals( + objectPermissions, + 'canUpdateObjectRecords', + true, + ), +}); +``` + +### Context variables + +These represent the current state of the page: + +| Variable | Type | Description | +| ------------------------------ | --------- | ---------------------------------------------------------------- | +| `pageType` | `string` | Current page type (e.g. `'RecordIndexPage'`, `'RecordShowPage'`) | +| `isInSidePanel` | `boolean` | Whether the component is rendered in a side panel | +| `numberOfSelectedRecords` | `number` | Number of currently selected records | +| `isSelectAll` | `boolean` | Whether "select all" is active | +| `selectedRecords` | `array` | The selected record objects | +| `favoriteRecordIds` | `array` | IDs of favorited records | +| `objectPermissions` | `object` | Permissions for the current object type | +| `targetObjectReadPermissions` | `object` | Read permissions for the target object | +| `targetObjectWritePermissions` | `object` | Write permissions for the target object | +| `featureFlags` | `object` | Active feature flags | +| `objectMetadataItem` | `object` | Metadata of the current object type | +| `hasAnySoftDeleteFilterOnView` | `boolean` | Whether the current view has a soft-delete filter | + +### Operators + +Combine variables into boolean expressions: + +| Operator | Description | +| ----------------------------------- | ----------------------------------------------------------------- | +| `isDefined(value)` | `true` if the value is not null/undefined | +| `isNonEmptyString(value)` | `true` if the value is a non-empty string | +| `includes(array, value)` | `true` if the array contains the value | +| `includesEvery(array, prop, value)` | `true` if every item's property includes the value | +| `every(array, prop)` | `true` if the property is truthy on every item | +| `everyDefined(array, prop)` | `true` if the property is defined on every item | +| `everyEquals(array, prop, value)` | `true` if the property equals the value on every item | +| `some(array, prop)` | `true` if the property is truthy on at least one item | +| `someDefined(array, prop)` | `true` if the property is defined on at least one item | +| `someEquals(array, prop, value)` | `true` if the property equals the value on at least one item | +| `someNonEmptyString(array, prop)` | `true` if the property is a non-empty string on at least one item | +| `none(array, prop)` | `true` if the property is falsy on every item | +| `noneDefined(array, prop)` | `true` if the property is undefined on every item | +| `noneEquals(array, prop, value)` | `true` if the property does not equal the value on any item | diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/layout/front-components.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/layout/front-components.mdx new file mode 100644 index 0000000000..82f712d73f --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/layout/front-components.mdx @@ -0,0 +1,404 @@ +--- +title: Front Components +description: Build React components that render inside Twenty's UI with sandboxed isolation. +icon: window-maximize +--- + +Front components are React components that render directly inside Twenty's UI. They run in an **isolated Web Worker** using Remote DOM — your code is sandboxed but renders natively in the page, not in an iframe. + +## Where front components can be used + +Front components can render in two locations within Twenty: + +* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu. +* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside [page layouts](/l/tr/developers/extend/apps/layout/page-layouts). When configuring a dashboard or a record page layout, users can add a front component widget. + +A front component on its own isn't reachable from the UI — you need to *surface* it. The two ways to do that are: + +* **Pair it with a [command menu item](/l/tr/developers/extend/apps/layout/command-menu-items)** — registers it in the command menu (Cmd+K) and, optionally, as a pinned quick-action. +* **Embed it as a widget in a [page layout](/l/tr/developers/extend/apps/layout/page-layouts)** — places it on a record's detail page or dashboard. + +## Basic example + +The quickest way to see a front component in action is to pair it with a [`defineCommandMenuItem`](/l/tr/developers/extend/apps/layout/command-menu-items), so it appears as a quick-action button in the top-right corner of the page: + +```tsx src/front-components/hello-world.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; + +const HelloWorld = () => { + return ( +
+

Hello from my app!

+

This component renders inside Twenty.

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', + name: 'hello-world', + description: 'A simple front component', + component: HelloWorld, +}); +``` + +```ts src/command-menu-items/hello-world.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345', + shortLabel: 'Hello', + label: 'Hello World', + icon: 'IconBolt', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +After syncing with `yarn twenty dev` (or running a one-shot `yarn twenty dev --once`), the quick action appears in the top-right corner of the page: + +
+ Quick action button in the top-right corner +
+ +Click it to render the component inline. + +## Configuration fields + +| Field | Required | Description | +| --------------------- | -------- | ------------------------------------------------------------ | +| `universalIdentifier` | Yes | Stable unique ID for this component | +| `component` | Yes | A React component function | +| `name` | No | Display name | +| `description` | No | Description of what the component does | +| `isHeadless` | No | Set to `true` if the component has no visible UI (see below) | + +## Placing a front component on a page + +Beyond commands, you can embed a front component directly into a record page by adding it as a widget in a **page layout**. See [Page Layouts](/l/tr/developers/extend/apps/layout/page-layouts) for details. + +## Headless vs non-headless + +Front components come in two rendering modes controlled by the `isHeadless` option: + +**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted. + +**Headless (`isHeadless: true`)** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below. + +```tsx src/front-components/sync-tracker.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component'; +import { useEffect } from 'react'; + +const SyncTracker = () => { + const recordId = useRecordId(); + + useEffect(() => { + enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' }); + }, [recordId]); + + return null; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'sync-tracker', + description: 'Tracks record views silently', + isHeadless: true, + component: SyncTracker, +}); +``` + +Because the component returns `null`, Twenty skips rendering a container for it — no empty space appears in the layout. The component still has access to all hooks and the host communication API. + +## SDK Command components + +The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done. + +Import them from `twenty-sdk/command`: + +* **`Command`** — Runs an async callback via the `execute` prop. +* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`. +* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`. +* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`. + +Here is a full example of a headless front component using `Command` to run an action from the command menu: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +And an example using `CommandModal` to ask for confirmation before executing: + +```tsx src/front-components/delete-draft.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { CommandModal } from 'twenty-sdk/command'; + +const DeleteDraft = () => { + const execute = async () => { + // perform the deletion + }; + + return ( + + ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456', + name: 'delete-draft', + description: 'Deletes a draft with confirmation', + component: DeleteDraft, + isHeadless: true, +}); +``` + +## Accessing runtime context + +Inside your component, use SDK hooks to access the current user, record, and component instance: + +```tsx src/front-components/record-info.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { + useUserId, + useRecordId, + useFrontComponentId, +} from 'twenty-sdk/front-component'; + +const RecordInfo = () => { + const userId = useUserId(); + const recordId = useRecordId(); + const componentId = useFrontComponentId(); + + return ( +
+

User: {userId}

+

Record: {recordId ?? 'No record context'}

+

Component: {componentId}

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012', + name: 'record-info', + component: RecordInfo, +}); +``` + +Available hooks: + +| Hook | Returns | Description | +| --------------------------------------------- | ------------------ | ---------------------------------------------------------- | +| `useUserId()` | `string` or `null` | The current user's ID | +| `useSelectedRecordIds()` | `string[]` | All selected record IDs (empty array if none selected) | +| `useRecordId()` | `string` or `null` | **Deprecated.** Use `useSelectedRecordIds()` instead | +| `useFrontComponentId()` | `string` | This component instance's ID | +| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function | + +## Host communication API + +Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`: + +| Function | Description | +| ----------------------------------------------- | ----------------------------- | +| `navigate(to, params?, queryParams?, options?)` | Navigate to a page in the app | +| `openSidePanelPage(params)` | Open a side panel | +| `closeSidePanel()` | Close the side panel | +| `openCommandConfirmationModal(params)` | Show a confirmation dialog | +| `enqueueSnackbar(params)` | Show a toast notification | +| `unmountFrontComponent()` | Unmount the component | +| `updateProgress(progress)` | Update a progress indicator | + +Here is an example that uses the host API to show a snackbar and close the side panel after an action completes: + +```tsx src/front-components/archive-record.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const ArchiveRecord = () => { + const recordId = useRecordId(); + + const handleArchive = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { status: 'ARCHIVED' } }, + id: true, + }, + }); + + await enqueueSnackbar({ + message: 'Record archived', + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Archive this record?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678', + name: 'archive-record', + description: 'Archives the current record', + component: ArchiveRecord, +}); +``` + +### Working with multiple records + +Use `useSelectedRecordIds()` to handle multiple selected records. This is useful for bulk operations: + +```tsx src/front-components/bulk-export.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const BulkExport = () => { + const selectedRecordIds = useSelectedRecordIds(); + + const handleExport = async () => { + const client = new CoreApiClient(); + + for (const recordId of selectedRecordIds) { + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { exported: true } }, + id: true, + }, + }); + } + + await enqueueSnackbar({ + message: `Exported ${selectedRecordIds.length} records`, + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Export {selectedRecordIds.length} selected record(s)?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901', + name: 'bulk-export', + description: 'Export selected records', + component: BulkExport, + command: { + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902', + label: 'Bulk Export', + availabilityType: 'RECORD_SELECTION', + conditionalAvailabilityExpression: numberOfSelectedRecords > 0, + }, +}); +``` + +## Public assets + +Front components can access files from the app's `public/` directory using `getPublicAssetUrl`: + +```tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +const Logo = () => Logo; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'logo', + component: Logo, +}); +``` + +See the [public assets section](/l/tr/developers/extend/apps/config/public-assets) for details. + +## Styling + +Front components support multiple styling approaches. You can use: + +* **Inline styles** — `style={{ color: 'red' }}` +* **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more) +* **Emotion** — CSS-in-JS with `@emotion/react` +* **Styled-components** — `styled.div` patterns +* **Tailwind CSS** — utility classes +* **Any CSS-in-JS library** compatible with React + +```tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Button, Tag, Status } from 'twenty-sdk/ui'; + +const StyledWidget = () => { + return ( +
+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456', + name: 'styled-widget', + component: StyledWidget, +}); +``` diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/layout/navigation-menu-items.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/layout/navigation-menu-items.mdx new file mode 100644 index 0000000000..e97866cae7 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/layout/navigation-menu-items.mdx @@ -0,0 +1,44 @@ +--- +title: Navigation Menu Items +description: Add custom entries to the workspace sidebar — links to saved views or external URLs. +icon: bars +--- + +A **navigation menu item** is an entry in the left sidebar. Use `defineNavigationMenuItem()` to ship custom sidebar links — typically one per [view](/l/tr/developers/extend/apps/layout/views) you ship — or to point at external URLs. + +```ts src/navigation-menu-items/example-navigation-menu-item.ts +import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define'; +import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view'; + +export default defineNavigationMenuItem({ + universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c', + name: 'example-navigation-menu-item', + icon: 'IconList', + color: 'blue', + position: 0, + type: NavigationMenuItemType.VIEW, + viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER, +}); +``` + +## Önemli noktalar + +* `type` determines what the menu item links to. Each type pairs with a specific identifier field: + + | Tür | Ne yapar | Required field | + | ------------------------------------ | ------------------------------------ | ----------------------------------------------------------------------------- | + | `NavigationMenuItemType.VIEW` | Opens a saved view | `viewUniversalIdentifier` | + | `NavigationMenuItemType.LINK` | Opens an external URL | `link` | + | `NavigationMenuItemType.FOLDER` | Groups nested items under a label | `name` (and child items reference the folder via `folderUniversalIdentifier`) | + | `NavigationMenuItemType.OBJECT` | Opens an object's default index page | `targetObjectUniversalIdentifier` | + | `NavigationMenuItemType.PAGE_LAYOUT` | Opens a standalone page layout | `pageLayoutUniversalIdentifier` | + +* `position` controls ordering in the sidebar. + +* `icon` and `color` are optional and customize how the entry looks. + +* `folderUniversalIdentifier` is also available on any item to nest it inside a `FOLDER`-type parent. + + +**Common pitfall:** creating an object without an associated view + navigation menu item makes that object invisible to users. Unless it's a technical/internal object, every custom object should have a default view *and* a sidebar entry pointing at it. + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/layout/overview.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/layout/overview.mdx new file mode 100644 index 0000000000..f3525c1556 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/layout/overview.mdx @@ -0,0 +1,56 @@ +--- +title: Overview +description: Place your app inside Twenty's UI — sidebar entries, saved views, record page tabs, and sandboxed React components. +icon: table-columns +--- + +A Twenty app's **layout layer** is everything the user sees: where the app surfaces in the sidebar, which list views it ships, how its record detail pages are arranged, and which custom React components render inside those pages. + +```text + Sidebar Record list Record detail page + ─────── ─────────── ────────────────── + [📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐ + [📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │ + [📋 Inbox ] │ ──────── │ │ [Notes ] │ + ▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab + │ │ Acme │ │ │ adds a tab... + └ defineNavi- │ … │ │ ┌────────────────┐ │ + gationMenu- └────▲─────┘ │ │ │ │ + Item points │ │ │ React UI │◀── …with a + to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent + └ defineView │ │ a Worker) │ │ widget inside + picks columns │ └────────────────┘ │ + and filters └─────────────────────┘ +``` + +## In this section + + + + `defineView` — saved list configurations: visible columns, filters, groups. + + + `defineNavigationMenuItem` — sidebar entries pointing at views or external URLs. + + + `definePageLayout` and `definePageLayoutTab` — tabs and widgets on a record's detail page. + + + `defineFrontComponent` — sandboxed React components that render inside Twenty. + + + `defineCommandMenuItem` — register front components as Cmd+K entries and quick actions. + + + +## Where the app surfaces + +| Surface | What it controls | Entity | +| --------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------- | +| **Sidebar** | A custom entry linking to a saved view or external URL | `defineNavigationMenuItem` | +| **Record list** | A saved configuration for an object — visible columns, order, filters, groups | `defineView` | +| **Record detail page** | The tabs and widgets on a record page (your own object's, or a standard one) | `definePageLayout`, `definePageLayoutTab` | +| **Inside any of the above** | A custom React widget — buttons, forms, dashboards, integrations | `defineFrontComponent` | +| **Command menu (Cmd+K)** | A pinned quick action or hidden command | `defineCommandMenuItem` | + +Front components run inside an isolated Web Worker using Remote DOM — they render *natively* in the page (not inside an iframe), but cannot reach the host page or DOM directly. Communication with Twenty happens through a message-passing host API. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/layout/page-layouts.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/layout/page-layouts.mdx new file mode 100644 index 0000000000..ba0a1dc3d8 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/layout/page-layouts.mdx @@ -0,0 +1,102 @@ +--- +title: Page Layouts +description: Customize record detail pages — tabs, widgets, and where front components render — using definePageLayout and definePageLayoutTab. +icon: table-columns +--- + +A **page layout** controls how a record's detail page is arranged: which tabs appear and what widgets they contain. Use `definePageLayout()` to declare a layout for an object you own, or `definePageLayoutTab()` to add a single tab to a layout that already exists (yours or a standard Twenty one). + +| Use case | Entity | +| ---------------------------------------------------------------------- | --------------------- | +| Define the entire layout for a record page on an object you own | `definePageLayout` | +| Add one tab to an existing layout (your own object, or a standard one) | `definePageLayoutTab` | + +## definePageLayout + +Use this when you own the entire detail page — typically for a custom object you defined yourself. + +```ts src/page-layouts/example-record-page-layout.ts +import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +export default definePageLayout({ + universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134', + name: 'Example Record Page', + type: 'RECORD_PAGE', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + tabs: [ + { + universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5', + title: 'Hello World', + position: 50, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], + }, + ], +}); +``` + +### Key points + +* `type` is typically `'RECORD_PAGE'` to customize the detail view of a specific object. +* `objectUniversalIdentifier` specifies which object this layout applies to. +* Each `tab` defines a section of the page with a `title`, `position`, and `layoutMode` (`CANVAS` for free-form layout). +* Each `widget` inside a tab can render a [front component](/l/tr/developers/extend/apps/layout/front-components), a relation list, or other built-in widget types. +* `position` on tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones. + +## definePageLayoutTab + +Use this when you only want to **add** a tab to an existing layout — for example, an analytics tab on the standard Company page, or an AI summary tab attached to your own object's layout. + +```ts src/page-layouts/example-extra-tab.ts +import { + definePageLayoutTab, + PageLayoutTabLayoutMode, +} from 'twenty-sdk/define'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = + '20202020-ab01-4001-8001-c0aba11c0100'; + +export default definePageLayoutTab({ + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001', + pageLayoutUniversalIdentifier: + COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER, + title: 'Hello World', + position: 1000, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], +}); +``` + +### Key points + +* `pageLayoutUniversalIdentifier` is **required** and must point to a page layout that already exists at install time — either a standard Twenty layout or one defined by your own app. Cross-app references to layouts owned by another installed app are not supported today. When the parent layout is missing, installation fails with a clear validation error. +* `widgets` are scoped to this tab only — they reference [front components](/l/tr/developers/extend/apps/layout/front-components), views, etc. exactly like widgets defined inline in `definePageLayout`. +* `position` controls ordering against existing tabs on the targeted layout. Pick a value that places your tab where you want it relative to built-in tabs. +* Use this instead of `definePageLayout` when you only want to add to an existing layout. Use `definePageLayout` when you own the entire layout. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/layout/views.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/layout/views.mdx new file mode 100644 index 0000000000..e4b11dcc1e --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/layout/views.mdx @@ -0,0 +1,43 @@ +--- +title: Views +description: Ship pre-configured saved views — column order, filters, groups — for objects in your app. +icon: list +--- + +A **view** is a saved configuration for how records of an object are displayed: which fields appear, their order, whether they're visible, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app — typically a default index view for each custom object you create. + +```ts src/views/example-view.ts +import { defineView, ViewKey } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; + +export default defineView({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'All example items', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + icon: 'IconList', + key: ViewKey.INDEX, + position: 0, + fields: [ + { + universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0', + fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER, + position: 0, + isVisible: true, + size: 200, + }, + ], +}); +``` + +## Key points + +* `objectUniversalIdentifier` specifies which object this view applies to. It can be a custom object you defined or a standard Twenty object. +* `key` determines the view type — `ViewKey.INDEX` is the main list view for the object. +* `fields` controls which columns appear and in what order. Each field references a `fieldMetadataUniversalIdentifier`. +* You can also declare `filters`, `filterGroups`, `groups`, and `fieldGroups` for advanced configurations. +* `position` controls ordering when multiple views exist for the same object. + +## How views show up in the UI + +A view by itself isn't reachable from the sidebar. To make it appear there, pair it with a [navigation menu item](/l/tr/developers/extend/apps/layout/navigation-menu-items) of type `VIEW` that points at the view's `universalIdentifier`. That's the canonical pattern: every custom object typically ships a default view + a sidebar entry that opens it. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/logic/connections.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/logic/connections.mdx new file mode 100644 index 0000000000..1a99c59742 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/logic/connections.mdx @@ -0,0 +1,193 @@ +--- +title: Connections +description: Let your app act on a user's behalf in third-party services via OAuth. +icon: plug +--- + +Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API. + +Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate. + + + + + +A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace. + +A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials. + +```ts src/connection-providers/linear-connection.ts +import { defineConnectionProvider } from 'twenty-sdk/define'; + +export default defineConnectionProvider({ + universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f', + name: 'linear', + displayName: 'Linear', + icon: 'IconBrandLinear', + type: 'oauth', + oauth: { + authorizationEndpoint: 'https://linear.app/oauth/authorize', + tokenEndpoint: 'https://api.linear.app/oauth/token', + scopes: ['read', 'write'], + // These must match keys in `defineApplication.serverVariables` below. + clientIdVariable: 'LINEAR_CLIENT_ID', + clientSecretVariable: 'LINEAR_CLIENT_SECRET', + // Optional: defaults to 'json'. Some providers (Linear, Slack) want + // 'form-urlencoded' for the token request. + tokenRequestContentType: 'form-urlencoded', + // Optional: defaults to true. Disable only if the provider rejects PKCE. + usePkce: false, + // Optional: extra query params on the authorize URL. + // authorizationParams: { prompt: 'consent' }, + // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect. + // revokeEndpoint: 'https://example.com/oauth/revoke', + }, +}); +``` + +```ts src/application.config.ts +import { defineApplication } from 'twenty-sdk/define'; + +export default defineApplication({ + universalIdentifier: '...', + displayName: 'Linear', + description: 'Connect Linear to Twenty.', + defaultRoleUniversalIdentifier: '...', + // OAuth client credentials live on the app registration (one OAuth app per + // Twenty server, configured by the admin) — not per-workspace. Declare them + // as serverVariables so the admin can fill them in once for all installs. + serverVariables: { + LINEAR_CLIENT_ID: { + description: 'OAuth client ID from your Linear OAuth application.', + isSecret: false, + isRequired: true, + }, + LINEAR_CLIENT_SECRET: { + description: 'OAuth client secret from your Linear OAuth application.', + isSecret: true, + isRequired: true, + }, + }, +}); +``` + +Key points: + +* `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`). +* `displayName` shows in the per-app settings tab and in the AI tool list. +* `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo. +* Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server. +* Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled. +* `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`. + +The OAuth callback URL your provider needs to whitelist is: + +``` +https:///apps/oauth/callback +``` + + + + + +Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens. + +```ts src/logic-functions/handlers/create-linear-issue-handler.ts +import { listConnections } from 'twenty-sdk/logic-function'; + +export const createLinearIssueHandler = async (input: { + teamId?: string; + title?: string; +}) => { + if (!input.teamId || !input.title) { + return { success: false, error: 'teamId and title are required' }; + } + + const connections = await listConnections({ providerName: 'linear' }); + + // Workspace-shared credentials win when present; fall back to the first + // user-visibility one. For HTTP-route triggers you typically pick the + // request user's connection via event.userWorkspaceId instead. + const connection = + connections.find((c) => c.visibility === 'workspace') ?? connections[0]; + + if (!connection) { + return { + success: false, + error: + 'Linear is not connected. Open the app settings and click "Add connection".', + }; + } + + // Use connection.accessToken to call the third-party API. + const response = await fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + Authorization: `Bearer ${connection.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`, + }), + }); + + return { success: response.ok }; +}; +``` + +Each connection has: + +| Field | Description | +| ----------------- | -------------------------------------------------------------------------------------------------------- | +| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one | +| `visibility` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) | +| `scopes` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) | +| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers | +| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) | +| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) | +| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect | + +Key points: + +* Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers. +* The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set). +* `getConnection(id)` is the single-row equivalent. + + + + + +When a user clicks "Add connection," they're prompted to pick a visibility: + +* **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not. +* **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user. + +Use the right one for each handler: + +```ts +// HTTP-route trigger — prefer the request user's own connection. +const conn = + connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ?? + connections.find((c) => c.visibility === 'workspace'); + +// Cron trigger — no request user; only shared credentials are sensible. +const conn = connections.find((c) => c.visibility === 'workspace'); +``` + +Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side. + + + + + +For each connection provider, the server admin needs to register an OAuth app at the third party first. + +1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new). +2. Set the **Redirect URI** to `\/apps/oauth/callback`. +3. Copy the generated **Client ID** and **Client Secret**. +4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`. +5. Workspace members can then add connections from the per-app **Connections** section. + + + + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/logic/logic-functions.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/logic/logic-functions.mdx new file mode 100644 index 0000000000..104b9a229e --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/logic/logic-functions.mdx @@ -0,0 +1,375 @@ +--- +title: Logic Functions +description: Define server-side TypeScript functions with HTTP, cron, and database event triggers. +icon: bolt +--- + +Logic functions are server-side TypeScript functions that run on the Twenty platform. They can be triggered by HTTP requests, cron schedules, or database events — and can also be exposed as tools for AI agents. + + + + +Each function file uses `defineLogicFunction()` to export a configuration with a handler and optional triggers. + +```ts src/logic-functions/createPostCard.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define'; +import { CoreApiClient, type Person } from 'twenty-client-sdk/core'; + +const handler = async (params: RoutePayload) => { + const client = new CoreApiClient(); + const body = (params.body ?? {}) as { name?: string }; + const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'; + + const result = await client.mutation({ + createPostCard: { + __args: { data: { name } }, + id: true, + name: true, + }, + }); + return result; +}; + +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'create-new-post-card', + timeoutSeconds: 2, + handler, + httpRouteTriggerSettings: { + path: '/post-card/create', + httpMethod: 'POST', + isAuthRequired: true, + }, + /*databaseEventTriggerSettings: { + eventName: 'people.created', + },*/ + /*cronTriggerSettings: { + pattern: '0 0 1 1 *', + },*/ +}); +``` + +Available trigger types: +* **httpRoute**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**: +> e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create` +* **cron**: Runs your function on a schedule using a CRON expression. +* **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function. +> e.g. `person.updated`, `*.created`, `company.*` + + +You can also manually execute a function using the CLI: + +```bash filename="Terminal" +yarn twenty exec -n create-new-post-card -p '{"key": "value"}' +``` + +```bash filename="Terminal" +yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + +You can watch logs with: + +```bash filename="Terminal" +yarn twenty logs +``` + + +#### Route trigger payload + +When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the +[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). +Import the `RoutePayload` type from `twenty-sdk`: + +```ts +import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define'; + +const handler = async (event: RoutePayload) => { + const { headers, queryStringParameters, pathParameters, body } = event; + const { method, path } = event.requestContext.http; + + return { message: 'Success' }; +}; +``` + +The `RoutePayload` type has the following structure: + + | Property | Type | Description | Example | + | ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | + | `headers` | `Record\` | HTTP headers (only those listed in `forwardedRequestHeaders`) | see section below | + | `queryStringParameters` | `Record\` | Query string parameters (multiple values joined with commas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` | + | `pathParameters` | `Record\` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` | + | `body` | `object \| null` | Parsed request body (JSON) | `{ id: 1 }` -> `{ id: 1 }` | + | `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | | + | `isBase64Encoded` | `boolean` | Whether the body is base64 encoded | | + | `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) | | + | `requestContext.http.path` | `string` | Raw request path | | + + +#### forwardedRequestHeaders + +By default, HTTP headers from incoming requests are **not** passed to your logic function for security reasons. +To access specific headers, list them in the `forwardedRequestHeaders` array: + +```ts +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'webhook-handler', + handler, + httpRouteTriggerSettings: { + path: '/webhook', + httpMethod: 'POST', + isAuthRequired: false, + forwardedRequestHeaders: ['x-webhook-signature', 'content-type'], + }, +}); +``` + +In your handler, access the forwarded headers like this: + +```ts +const handler = async (event: RoutePayload) => { + const signature = event.headers['x-webhook-signature']; + const contentType = event.headers['content-type']; + + // Validate webhook signature... + return { received: true }; +}; +``` + + +Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`). + + +#### Exposing a function as an AI tool or workflow action + +Logic functions can be exposed on two surfaces, each with its own trigger: + +* **`toolTriggerSettings`** — makes the function discoverable by Twenty's AI features (chat, MCP, function calling). Uses standard JSON Schema, the format LLMs natively understand. +* **`workflowActionTriggerSettings`** — makes the function appear as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper field editors, variable pickers, and labels. + +A function can opt into one, the other, or both. They sit alongside `cronTriggerSettings`, `databaseEventTriggerSettings`, and `httpRouteTriggerSettings` — same pattern, same shape. + +```ts src/logic-functions/enrich-company.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const handler = async (params: { companyName: string; domain?: string }) => { + const client = new CoreApiClient(); + + 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, + toolTriggerSettings: {}, +}); +``` + +Key points: + +* A function can mix surfaces — declare both `toolTriggerSettings` and `workflowActionTriggerSettings` to expose it in chat AND in the workflow builder. +* `toolTriggerSettings.inputSchema` and `workflowActionTriggerSettings.inputSchema` are both optional. When omitted, the manifest builder infers them from the handler source code (JSON Schema for the AI tool, Twenty's `InputSchema` for the workflow action). Provide one explicitly when you want richer typing — for example, with `FieldMetadataType`-aware fields like `CURRENCY` or `RELATION` for the workflow builder, or with `description` fields the AI agent can read: + +```ts +export default defineLogicFunction({ + ..., + toolTriggerSettings: { + inputSchema: { + 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'], + }, + }, +}); +``` + + +**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. + + + + + + +**Install hooks** — pre-install and post-install handlers — share this runtime but are declared with their own define functions and don't take trigger settings. See [Install Hooks](/l/tr/developers/extend/apps/config/install-hooks) for `definePreInstallLogicFunction` and `definePostInstallLogicFunction`. + + +## Typed API clients (twenty-client-sdk) + +The `twenty-client-sdk` package provides two typed GraphQL clients for interacting with the Twenty API from your logic functions and front components. + +| Client | Import | Endpoint | Generated? | +| ------------------- | ---------------------------- | ---------------------------------------------- | ---------------------- | +| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — workspace data (records, objects) | Yes, at dev/build time | +| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — workspace config, file uploads | No, ships pre-built | + + + + +`CoreApiClient` is the main client for querying and mutating workspace data. It is **generated from your workspace schema** during `yarn twenty dev` or `yarn twenty build`, so it is fully typed to match your objects and fields. + +```ts +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const client = new CoreApiClient(); + +// Query records +const { companies } = await client.query({ + companies: { + edges: { + node: { + id: true, + name: true, + domainName: { + primaryLinkLabel: true, + primaryLinkUrl: true, + }, + }, + }, + }, +}); + +// Create a record +const { createCompany } = await client.mutation({ + createCompany: { + __args: { + data: { + name: 'Acme Corp', + }, + }, + id: true, + name: true, + }, +}); +``` + +The client uses a selection-set syntax: pass `true` to include a field, use `__args` for arguments, and nest objects for relations. You get full autocompletion and type checking based on your workspace schema. + + +**CoreApiClient is generated at dev/build time.** If you use it without running `yarn twenty dev` or `yarn twenty build` first, it throws an error. The generation happens automatically — the CLI introspects your workspace's GraphQL schema and generates a typed client using `@genql/cli`. + + +#### Using CoreSchema for type annotations + +`CoreSchema` provides TypeScript types matching your workspace objects — useful for typing component state or function parameters: + +```ts +import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core'; +import { useState } from 'react'; + +const [company, setCompany] = useState< + Pick | undefined +>(undefined); + +const client = new CoreApiClient(); +const result = await client.query({ + company: { + __args: { filter: { position: { eq: 1 } } }, + id: true, + name: true, + }, +}); +setCompany(result.company); +``` + + + + +`MetadataApiClient` ships pre-built with the SDK (no generation required). It queries the `/metadata` endpoint for workspace configuration, applications, and file uploads. + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; + +const metadataClient = new MetadataApiClient(); + +// List first 10 objects in the workspace +const { objects } = await metadataClient.query({ + objects: { + edges: { + node: { + id: true, + nameSingular: true, + namePlural: true, + labelSingular: true, + isCustom: true, + }, + }, + __args: { + filter: {}, + paging: { first: 10 }, + }, + }, +}); +``` + +#### Uploading files + +`MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields: + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import * as fs from 'fs'; + +const metadataClient = new MetadataApiClient(); + +const fileBuffer = fs.readFileSync('./invoice.pdf'); + +const uploadedFile = await metadataClient.uploadFile( + fileBuffer, // file contents as a Buffer + 'invoice.pdf', // filename + 'application/pdf', // MIME type + '58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier +); + +console.log(uploadedFile); +// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' } +``` + +| Parameter | Type | Description | +| ---------------------------------- | -------- | --------------------------------------------------------------- | +| `fileBuffer` | `Buffer` | The raw file contents | +| `filename` | `string` | The name of the file (used for storage and display) | +| `contentType` | `string` | MIME type (defaults to `application/octet-stream` if omitted) | +| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object | + +Key points: +* Uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed. +* The returned `url` is a signed URL you can use to access the uploaded file. + + + + + + When your code runs on Twenty (logic functions or front components), the platform injects credentials as environment variables: + + * `TWENTY_API_URL` — Base URL of the Twenty API + * `TWENTY_APP_ACCESS_TOKEN` — Short-lived key scoped to your application's default function role + + You do **not** need to pass these to the clients — they read from `process.env` automatically. The API key's permissions are determined by the role referenced in `defaultRoleUniversalIdentifier` in your `application-config.ts`. + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/logic/overview.mdx new file mode 100644 index 0000000000..eb6f2b6e9d --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/logic/overview.mdx @@ -0,0 +1,55 @@ +--- +title: Overview +description: Server-side TypeScript that runs inside Twenty — triggered by HTTP routes, cron schedules, database events, AI tools, or workflow actions. +icon: bolt +--- + +A Twenty app's **logic layer** is the code that *runs* — server-side TypeScript handlers reacting to HTTP requests, cron schedules, and record changes; AI skills and agents that live inside the workspace; and OAuth connections that let your functions act on a user's behalf in third-party services. + +```text + ┌─ HTTP route ──┐ + │ Cron schedule │ + │ Database event │ ┌────────────────────┐ + triggers ─┤ AI tool call ├─────▶│ Logic function │ + │ Workflow action │ │ (your handler) │ + │ Manual exec │ └────────────────────┘ + └────────────────────┘ │ + ▼ + ┌────────────────────────────┐ + │ Twenty API (records) │ + │ Third-party API │ + │ (via Connection token) │ + └────────────────────────────┘ +``` + +## In this section + + + + The core building block — trigger types, payloads, and the typed API client. + + + Reusable AI agent instructions and assistants with custom system prompts. + + + OAuth credentials your app holds for third-party services — Linear, GitHub, Slack, and more. + + + +## Trigger types at a glance + +A logic function picks one or more triggers — every entry below is a separate field on `defineLogicFunction()`: + +| Trigger | When it runs | Setting | +| ------------------- | ---------------------------------------------------------- | ------------------------------- | +| **HTTP route** | A request hits your `/s/\` endpoint | `httpRouteTriggerSettings` | +| **Cron** | A CRON expression matches | `cronTriggerSettings` | +| **Database event** | A workspace record is created, updated, or deleted | `databaseEventTriggerSettings` | +| **AI tool** | A Twenty AI feature decides to call your function | `toolTriggerSettings` | +| **Workflow action** | A workflow step invokes your function | `workflowActionTriggerSettings` | + +Functions run sandboxed in isolated Node.js processes and access the workspace through a typed API client scoped to the role declared on [`defineApplication()`](/l/tr/developers/extend/apps/config/application). + + +**Install-time hooks** — code that runs before or after the install — share this runtime but use their own define functions and live under [Config → Install Hooks](/l/tr/developers/extend/apps/config/install-hooks). + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/logic/skills-and-agents.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/logic/skills-and-agents.mdx new file mode 100644 index 0000000000..82a66ef20f --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/logic/skills-and-agents.mdx @@ -0,0 +1,69 @@ +--- +title: Skills & Agents +description: Define AI skills and agents for your app. +icon: robot +--- + + + Skills and agents are currently in alpha. The feature works but is still evolving. + + +Apps can define AI capabilities that live inside the workspace — reusable skill instructions and agents with custom system prompts. + + + + +Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation: + +```ts src/skills/example-skill.ts +import { defineSkill } from 'twenty-sdk/define'; + +export default defineSkill({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'sales-outreach', + label: 'Sales Outreach', + description: 'Guides the AI agent through a structured sales outreach process', + icon: 'IconBrain', + content: `You are a sales outreach assistant. When reaching out to a prospect: +1. Research the company and recent news +2. Identify the prospect's role and likely pain points +3. Draft a personalized message referencing specific details +4. Keep the tone professional but conversational`, +}); +``` + +Key points: +* `name` is a unique identifier string for the skill (kebab-case recommended). +* `label` is the human-readable display name shown in the UI. +* `content` contains the skill instructions — this is the text the AI agent uses. +* `icon` (optional) sets the icon displayed in the UI. +* `description` (optional) provides additional context about the skill's purpose. + + + + +Agents are AI assistants that live inside your workspace. Use `defineAgent()` to create agents with a custom system prompt: + +```ts src/agents/example-agent.ts +import { defineAgent } from 'twenty-sdk/define'; + +export default defineAgent({ + universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123', + name: 'sales-assistant', + label: 'Sales Assistant', + description: 'Helps the sales team draft outreach emails and research prospects', + icon: 'IconRobot', + prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.', +}); +``` + +Key points: +* `name` is the unique identifier string for the agent (kebab-case recommended). +* `label` is the display name shown in the UI. +* `prompt` is the system prompt that defines the agent's behavior. +* `description` (optional) provides context about what the agent does. +* `icon` (optional) sets the icon displayed in the UI. +* `modelId` (optional) overrides the default AI model used by the agent. + + + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/operations/cli.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/operations/cli.mdx new file mode 100644 index 0000000000..0cccebdbb5 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/operations/cli.mdx @@ -0,0 +1,78 @@ +--- +title: CLI +description: yarn twenty commands for executing functions, streaming logs, managing app installations, and switching remotes. +icon: terminal +--- + +Beyond `dev`, `build`, `add`, and `typecheck`, the `yarn twenty` CLI provides commands for executing functions, viewing logs, and managing app installations. + +## Executing functions (`yarn twenty exec`) + +Run a logic function manually without triggering it via HTTP, cron, or database event: + +```bash filename="Terminal" +# Execute by function name +yarn twenty exec -n create-new-post-card + +# Execute by universalIdentifier +yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf + +# Pass a JSON payload +yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}' + +# Execute the post-install function +yarn twenty exec --postInstall +``` + +## Viewing function logs (`yarn twenty logs`) + +Stream execution logs for your app's logic functions: + +```bash filename="Terminal" +# Stream all function logs +yarn twenty logs + +# Filter by function name +yarn twenty logs -n create-new-post-card + +# Filter by universalIdentifier +yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + + +This is different from `yarn twenty server logs`, which shows the Docker container logs. `yarn twenty logs` shows your app's function execution logs from the Twenty server. + + +## Uninstalling an app (`yarn twenty uninstall`) + +Remove your app from the active workspace: + +```bash filename="Terminal" +yarn twenty uninstall + +# Skip the confirmation prompt +yarn twenty uninstall --yes +``` + +## Managing remotes + +A **remote** is a Twenty server that your app connects to. During setup, the scaffolder creates one for you automatically. You can add more remotes or switch between them at any time. + +```bash filename="Terminal" +# Add a new remote (opens a browser for OAuth login) +yarn twenty remote add + +# Connect to a local Twenty server (auto-detects port 2020 or 3000) +yarn twenty remote add --local + +# Add a remote non-interactively (useful for CI) +yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote + +# List all configured remotes +yarn twenty remote list + +# Switch the active remote +yarn twenty remote switch +``` + +Your credentials are stored in `~/.twenty/config.json`. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/operations/overview.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/operations/overview.mdx new file mode 100644 index 0000000000..25d5886813 --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/operations/overview.mdx @@ -0,0 +1,29 @@ +--- +title: Overview +description: Build, test, and ship your app — CLI commands, integration tests, CI, and publishing to a server or to npm. +icon: rocket +--- + +The **operations layer** is everything you do *to* your app rather than *with* it: invoking CLI commands, running integration tests against a real Twenty server, configuring CI, and shipping releases — either as a tarball deployed to a single server or as an npm package listed in the marketplace. + +```text + develop ─▶ test ─▶ build ─▶ deploy / publish + ─────── ──── ───── ───────────────── + yarn yarn yarn yarn twenty deploy (tarball → one server) + twenty test twenty + dev build yarn twenty publish (npm → marketplace) +``` + +## In this section + + + + `yarn twenty` reference — exec, logs, uninstall, remotes. + + + Vitest setup, integration tests, type checking, CI workflow. + + + Build, deploy a tarball, publish to npm, install. + + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/operations/publishing.mdx new file mode 100644 index 0000000000..658a5626fa --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/operations/publishing.mdx @@ -0,0 +1,295 @@ +--- +title: Publishing +icon: upload +description: Distribute your Twenty app to the marketplace or deploy it internally. +--- + +## Overview + +Once your app is [built and tested locally](/l/tr/developers/extend/apps/getting-started/concepts), you have two paths for distributing it: + +* **Deploy a tarball** — upload your app directly to a specific Twenty server for internal or private use. +* **Publish to npm** — list your app in the Twenty marketplace for any workspace to discover and install. + +Both paths start from the same **build** step. + +## Building your app + +Run the build command to compile your app and generate a distribution-ready `manifest.json`: + +```bash filename="Terminal" +yarn twenty build +``` + +This compiles TypeScript sources, transpiles logic functions and front components, and writes everything to `.twenty/output/`. Add `--tarball` to also produce a `.tgz` package for manual distribution or the deploy command. + +## Deploying to a server (tarball) + +For apps you don't want publicly available — proprietary tools, enterprise-only integrations, or experimental builds — you can deploy a tarball directly to a Twenty server. + +### Prerequisites + +Before deploying, you need a configured remote pointing to the target server. Remotes store the server URL and authentication credentials locally in `~/.twenty/config.json`. + +Add a remote: + +```bash filename="Terminal" +yarn twenty remote add --api-url https://your-twenty-server.com --as production +``` + +### Deploying + +Build and upload your app to the server in one step: + +```bash filename="Terminal" +yarn twenty deploy +# To deploy to a specific remote: +# yarn twenty deploy --remote production +``` + +### Sharing a deployed app + + +Sharing private (tarball) apps across workspaces is an **Enterprise** feature. The **Distribution** tab will show an upgrade prompt instead of the share controls until your workspace has a valid Enterprise key. See [Settings > Admin Panel > Enterprise](/settings/admin-panel#enterprise) to activate it. + + +Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. Once your workspace is on the Enterprise plan, you can share a deployed app like this: + +1. Go to **Settings > Applications > Registrations** and open your app +2. In the **Distribution** tab, click **Copy share link** +3. Share this link with users on other workspaces — it takes them directly to the app's install page + +The share link uses the server's base URL (without any workspace subdomain) so it works for any workspace on the server. + +### Version management + +When updating an already deployed tarball app, the server requires the `version` in `package.json` to be **strictly higher** (per [semver](https://semver.org) ordering) than the currently deployed version. Re-deploying the same version, or pushing a lower one, is rejected before the tarball is stored — you'll see a `VERSION_ALREADY_EXISTS` error from the CLI. + +To release an update: + +1. Bump the `version` field in your `package.json` (e.g. `1.2.3` → `1.2.4`, `1.3.0`, or `2.0.0`) +2. Run `yarn twenty deploy` (or `yarn twenty deploy --remote production`) +3. Workspaces that have the app installed will see the upgrade available in their settings + + +Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allowed, and a final release like `1.0.0` is correctly recognized as higher than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver string. + + +{/* TODO: add screenshot of the Upgrade button */} + +### Server version compatibility + +If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`: + +```json filename="package.json" +{ + "name": "twenty-my-app", + "version": "1.0.0", + "engines": { + "node": "^24.5.0", + "twenty": ">=2.3.0" + } +} +``` + +The value is a standard [semver range](https://github.com/npm/node-semver#ranges). Common patterns: + +| Range | Meaning | +| ---------------------------------- | ------------------------------------------ | +| `>=2.3.0` | Any server from 2.3.0 onward | +| `>=2.3.0 \<3.0.0` | 2.3.0 or later, but below the next major | +| `^2.3.0` | Same as `>=2.3.0 \<3.0.0` | + +**What happens at deploy and install time:** + +* If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version. +* If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps). +* If the server has no `APP_VERSION` configured, the check is skipped. + + +The server is the authoritative check — it validates `engines.twenty` on both tarball upload and workspace install. If you deploy a tarball out-of-band or install from the marketplace, the server still enforces compatibility. + + +## Automated CI/CD (scaffolded workflows) + +Apps generated with `create-twenty-app` ship with two GitHub Actions workflows out of the box, under `.github/workflows/`. They are ready to run as soon as you push the repo to GitHub — no extra setup is needed for CI, and CD only requires a single secret. + +### CI — `ci.yml` + +Runs integration tests on every push to `main` and every pull request. + +**What it does:** + +1. Checks out your app's source. +2. Spawns an isolated Twenty test instance using the `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` composite action (the CI equivalent of `yarn twenty server start --test`). +3. Enables Corepack, sets up Node.js from your `.nvmrc`, and installs dependencies with `yarn install --immutable`. +4. Runs `yarn test`, passing `TWENTY_API_URL` and `TWENTY_API_KEY` from the spawned instance so your tests can talk to a real server. + +**Config knobs:** + +* `TWENTY_VERSION` (env, defaults to `latest`) — pin the Twenty server version used in CI by editing this in `ci.yml`. +* Concurrency is grouped by `github.ref` and cancels in-progress runs on new pushes. + +No secrets are required — the test instance is ephemeral and lives only for the duration of the job. + +### CD — `cd.yml` + +Deploys your app to a configured Twenty server on every push to `main`, and optionally from a pull request when the `deploy` label is applied. + +**What it does:** + +1. Checks out the PR head (for labeled PRs) or the pushed commit. +2. Runs `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — the CI equivalent of `yarn twenty deploy`. +3. Runs `twentyhq/twenty/.github/actions/install-twenty-app@main` so the newly deployed version is installed into the target workspace. + +**Required configuration:** + +| Setting | Where | Purpose | +| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `TWENTY_DEPLOY_URL` | `env` in `cd.yml` (defaults to `http://localhost:3000`) | The Twenty server to deploy to. Change this to your real server URL before first use. | +| `TWENTY_DEPLOY_API_KEY` | GitHub repo **Settings → Secrets and variables → Actions** | API key with deploy permission on the target server. | + + +The default `TWENTY_DEPLOY_URL` of `http://localhost:3000` is a placeholder — it will not reach anything from a GitHub-hosted runner. Update it to your server's public URL (or use a self-hosted runner with network access) before enabling CD. + + +**Triggering a preview deploy from a PR:** + +Add the `deploy` label to a pull request. The `if:` guard in `cd.yml` will run the job for that PR using the PR's head commit, letting you validate a change on the target server before merging. + +### Pinning the reusable actions + +Both workflows reference reusable actions at `@main`, so action updates in the `twentyhq/twenty` repo are picked up automatically. If you want deterministic builds, replace `@main` with a commit SHA or release tag on each `uses:` line. + +## Publishing to npm + +Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twenty workspace can browse, install, and upgrade marketplace apps directly from the UI. + +### Requirements + +* An [npm](https://www.npmjs.com) account +* The `twenty-app` keyword in your `package.json` `keywords` array (add it manually — it is not included by default in the `create-twenty-app` template) + +```json filename="package.json" +{ + "name": "twenty-app-postcard-sender", + "version": "1.0.0", + "keywords": ["twenty-app"] +} +``` + +### Marketplace metadata + +The `defineApplication()` config supports optional fields that control how your app appears in the marketplace. Use `logoUrl` and `screenshots` to reference images from the `public/` folder: + +```ts src/application-config.ts +export default defineApplication({ + universalIdentifier: '...', + displayName: 'My App', + description: 'A great app', + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + logoUrl: 'public/logo.png', + screenshots: [ + 'public/screenshot-1.png', + 'public/screenshot-2.png', + ], +}); +``` + +See the [defineApplication accordion](/l/tr/developers/extend/apps/config/application#marketplace-metadata) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.). + +#### Recommended screenshot dimensions + +The marketplace renders `screenshots` in a fixed `8:5` container (for example, `1600×1000 px`). + + +Screenshots of any aspect ratio are displayed in full and are never cropped, but anything significantly taller or narrower than `8:5` will show empty bands on the sides. + + +### Publish + +```bash filename="Terminal" +yarn twenty publish +``` + +To publish under a specific dist-tag (e.g., `beta` or `next`): + +```bash filename="Terminal" +yarn twenty publish --tag beta +``` + +### How marketplace discovery works + +The Twenty server syncs its marketplace catalog from the npm registry **every hour**. + +You can trigger the sync immediately instead of waiting: + +```bash filename="Terminal" +yarn twenty server catalog-sync +# To target a specific remote: +# yarn twenty server catalog-sync --remote production +``` + +The metadata shown in the marketplace comes from your `defineApplication()` config — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`. + + +If your app does not define an `aboutDescription` in `defineApplication()`, the marketplace will automatically use your package's `README.md` from npm as the about page content. This means you can maintain a single README for both npm and the Twenty marketplace. If you want a different description in the marketplace, explicitly set `aboutDescription`. + + +### CI publishing + +Use this GitHub Actions workflow to publish automatically on every release (uses [OIDC](https://docs.npmjs.com/trusted-publishers)): + +```yaml filename=".github/workflows/publish.yml" +name: Publish +on: + release: + types: [published] + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: https://registry.npmjs.org + - run: yarn install --immutable + - run: npx twenty build + - run: npm publish --provenance --access public + working-directory: .twenty/output +``` + +For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `yarn twenty build`, then `npm publish` from `.twenty/output`. + + +**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions. + + +## Installing apps + +Once an app is published (npm) or deployed (tarball), workspaces can install it through the UI. + +Go to the **Settings > Applications** page in Twenty, where both marketplace and tarball-deployed apps can be browsed and installed. + +{/* TODO: add screenshot of the UI when the app is registered */} + +You can also install apps from the command line: + +```bash filename="Terminal" +yarn twenty install +``` + + +The server enforces semver versioning on install, mirroring the rules on deploy: + +* Installing the same version that is already installed in your workspace is rejected with an `APP_ALREADY_INSTALLED` error. +* Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error. + +To install a newer version, deploy or publish it first, then re-run `yarn twenty install`. + diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/operations/testing.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/operations/testing.mdx new file mode 100644 index 0000000000..b1959be0ed --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/operations/testing.mdx @@ -0,0 +1,301 @@ +--- +title: Testing +description: Vitest setup, integration tests against a real Twenty server, type checking, and CI with GitHub Actions. +icon: flask +--- + +The SDK provides programmatic APIs that let you build, deploy, install, and uninstall your app from test code. Combined with [Vitest](https://vitest.dev/) and the typed API clients, you can write integration tests that verify your app works end-to-end against a real Twenty server. + +## Using npm packages + +You can install and use any npm package in your app. Both logic functions and front components are bundled with [esbuild](https://esbuild.github.io/), which inlines all dependencies into the output — no `node_modules` are needed at runtime. + +### Installing a package + +```bash filename="Terminal" +yarn add axios +``` + +Then import it in your code: + +```ts src/logic-functions/fetch-data.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import axios from 'axios'; + +const handler = async (): Promise => { + const { data } = await axios.get('https://api.example.com/data'); + + return { data }; +}; + +export default defineLogicFunction({ + universalIdentifier: '...', + name: 'fetch-data', + description: 'Fetches data from an external API', + timeoutSeconds: 10, + handler, +}); +``` + +The same works for front components: + +```tsx src/front-components/chart.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { format } from 'date-fns'; + +const DateWidget = () => { + return

Today is {format(new Date(), 'MMMM do, yyyy')}

; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### How bundling works + +The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle. + +**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed. + +**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment. + +Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server. + +## Setup + +The scaffolded app already includes Vitest. If you set it up manually, install the dependencies: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Create a `vitest.config.ts` at the root of your app: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Create a setup file that verifies the server is reachable before tests run: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## Programmatic SDK APIs + +The `twenty-sdk/cli` subpath exports functions you can call directly from test code: + +| Function | Description | +| -------------- | ------------------------------------------- | +| `appBuild` | Build the app and optionally pack a tarball | +| `appDeploy` | Upload a tarball to the server | +| `appInstall` | Install the app on the active workspace | +| `appUninstall` | Uninstall the app from the active workspace | + +Each function returns a result object with `success: boolean` and either `data` or `error`. + +## Writing an integration test + +Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Running tests + +Make sure your local Twenty server is running, then: + +```bash filename="Terminal" +yarn test +``` + +Or in watch mode during development: + +```bash filename="Terminal" +yarn test:watch +``` + +## Type checking + +You can also run type checking on your app without running tests: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +This runs `tsc --noEmit` and reports any type errors. + +## CI with GitHub Actions + +The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests. + +The workflow: + +1. Checks out your code +2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action +3. Installs dependencies with `yarn install --immutable` +4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub. + +To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow. diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/config/application.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..eba2fab940 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/config/application.mdx @@ -0,0 +1,65 @@ +--- +title: Application Config +description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. +icon: rocket +--- + +Every app must have exactly one `defineApplication` call. It declares: + +* **Identity** — universal identifier, display name, description. +* **Permissions** — which role its logic functions and front components run under. +* **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. +* **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/l/zh/developers/extend/apps/logic/logic-functions). + +```ts src/application-config.ts +import { defineApplication } from 'twenty-sdk/define'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; + +export default defineApplication({ + universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', + displayName: 'My Twenty App', + description: 'My first Twenty app', + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', + description: 'Default recipient name for postcards', + value: 'Jane Doe', + isSecret: false, + }, + }, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). +* `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/l/zh/developers/extend/apps/config/roles). +* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. + +## Default function role + +The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: + +* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. +* The typed API client is restricted to the permissions granted to that role. +* Follow least-privilege: declare only the permissions your functions need. + +When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/l/zh/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/l/zh/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `author` | Author or company name | +| `category` | App category for marketplace filtering | +| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) | +| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) | +| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | +| `websiteUrl` | Link to your website | +| `termsUrl` | Link to terms of service | +| `emailSupport` | Support email address | +| `issueReportUrl` | Link to issue tracker | diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..734ffc2828 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/config/install-hooks.mdx @@ -0,0 +1,206 @@ +--- +title: Install Hooks +description: Run logic before or after the install — seed data, back up records, validate the upgrade. +icon: wrench +--- + +Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/l/zh/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload`, but they're declared with their own define functions — `definePostInstallLogicFunction()` and `definePreInstallLogicFunction()` — and live outside the normal trigger model (HTTP, cron, database events). + +Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build will error if more than one of either is detected. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ install flow │ +│ │ +│ upload package → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +│ │ +│ old schema visible new schema visible │ +└─────────────────────────────────────────────────────────────┘ +``` + + + + +A post-install function runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services. + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Post install logic function executed successfully!', payload.previousVersion); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Runs after installation to set up the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + shouldRunSynchronously: false, + handler, +}); +``` + +You can also manually execute the post-install function at any time using the CLI: + +```bash filename="Terminal" +yarn twenty exec --postInstall +``` + +Key points: +* Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`). +* The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic. +* **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook. +* **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed. + * `shouldRunSynchronously: false` *(default)* — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window. + * `shouldRunSynchronously: true` — the hook is executed **inline during the install flow** (same executor as pre-install). The install request blocks until the handler finishes, and if it throws, the install caller receives a `POST_INSTALL_ERROR`. No automatic retries. **Use this for fast, must-complete-before-response work** — for example, emitting a validation error to the user, or quick setup that the client will rely on immediately after the install call returns. Keep in mind the metadata migration has already been applied by the time post-install runs, so a sync-mode failure does **not** roll back the schema changes — it only surfaces the error. +* Make sure your handler is idempotent. In async mode the queue may retry up to three times; in either mode the hook may run again on upgrades when `shouldRunOnVersionUpgrade: true`. +* The environment variables `APPLICATION_ID`, `APP_ACCESS_TOKEN`, and `API_URL` are available inside the handler (same as any other logic function), so you can call the Twenty API with an application access token scoped to your app. +* Only one post-install function is allowed per application. The manifest build will error if more than one is detected. +* The function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in [`defineApplication()`](/l/zh/developers/extend/apps/config/application). +* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding. +* **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` to trigger it manually against a running workspace. + + + + +A pre-install function runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped. + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; + +const handler = async (payload: InstallPayload): Promise => { + console.log('Pre install logic function executed successfully!', payload.previousVersion); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Runs before installation to prepare the application.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +You can also manually execute the pre-install function at any time using the CLI: + +```bash filename="Terminal" +yarn twenty exec --preInstall +``` + +Key points: +* Pre-install functions use `definePreInstallLogicFunction()` — same specialized config as post-install, just attached to a different lifecycle slot. +* Both pre- and post-install handlers receive the same `InstallPayload` type: `{ previousVersion?: string; newVersion: string }`. Import it once and reuse it for both hooks. +* **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state. +* **Execution model**: pre-install is executed **synchronously** and **blocks the install**. If the handler throws, the install is aborted before any schema changes are applied — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade. +* As with post-install, only one pre-install function is allowed per application. It is attached to the application manifest under `preInstallLogicFunction` automatically during the build. +* **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty exec --preInstall` to trigger it manually. + + + + +Both hooks are part of the same install flow and receive the same `InstallPayload`. The difference is **when** they run relative to the workspace metadata migration, and that changes what data they can safely touch. + +Pre-install is always **synchronous** (it blocks the install and can abort it). Post-install is **asynchronous by default** — enqueued on a worker with automatic retries — but can opt into synchronous execution with `shouldRunSynchronously: true`. See the `definePostInstallLogicFunction` accordion above for when to use each mode. + +**Use `post-install` for anything that needs the new schema to exist.** This is the common case: + +* Seeding default data (creating initial records, default views, demo content) against newly-added objects and fields. +* Registering webhooks with third-party services now that the app has its credentials. +* Calling your own API to finish setup that depends on the synchronized metadata. +* Idempotent "ensure this exists" logic that should reconcile state on every upgrade — combine with `shouldRunOnVersionUpgrade: true`. + +Example — seed a default `PostCard` record after install: + +```ts src/logic-functions/post-install.ts +import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion }: InstallPayload): Promise => { + if (previousVersion) return; // fresh installs only + + const client = createClient(); + await client.postCard.create({ + data: { title: 'Welcome to Postcard', content: 'Your first card!' }, + }); +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', + name: 'post-install', + description: 'Seeds a welcome post card after install.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: false, + handler, +}); +``` + +**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the *previous* schema and its failure rolls back the upgrade, it is the right place for anything risky: + +* **Backing up data that is about to be dropped or restructured** — e.g. you are removing a field in v2 and need to copy its values into another field or export them to storage before the migration runs. +* **Archiving records that a new constraint would invalidate** — e.g. a field is becoming `NOT NULL` and you need to delete or fix rows with null values first. +* **Validating compatibility and refusing the upgrade if the current data cannot be migrated cleanly** — throw from the handler and the install aborts with no changes applied. This is safer than discovering the incompatibility mid-migration. +* **Renaming or rekeying data** ahead of a schema change that would lose the association. + +Example — archive records before a destructive migration: + +```ts src/logic-functions/pre-install.ts +import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; +import { createClient } from './generated/client'; + +const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise => { + // Only the 1.x → 2.x upgrade drops the legacy `notes` field. + if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) { + return; + } + + const client = createClient(); + const legacyRecords = await client.postCard.findMany({ + where: { notes: { isNotNull: true } }, + }); + + if (legacyRecords.length === 0) return; + + // Copy legacy `notes` into the new `description` field before the migration + // drops the `notes` column. If this fails, the upgrade is aborted and the + // workspace stays on v1 with all data intact. + await Promise.all( + legacyRecords.map((record) => + client.postCard.update({ + where: { id: record.id }, + data: { description: record.notes }, + }), + ), + ); +}; + +export default definePreInstallLogicFunction({ + universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', + name: 'pre-install', + description: 'Backs up legacy notes into description before the v2 migration.', + timeoutSeconds: 300, + shouldRunOnVersionUpgrade: true, + handler, +}); +``` + +**Rule of thumb:** + +| You want to... | Use | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Seed default data, configure the workspace, register external resources | `post-install` | +| Run long-running seeding or third-party calls that shouldn't block the install response | `post-install` (default — `shouldRunSynchronously: false`, with worker retries) | +| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` | +| Read or back up data that the upcoming migration would lose | `pre-install` | +| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) | +| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` | +| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) | + + +If in doubt, default to **post-install**. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone. + + + + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/config/overview.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/config/overview.mdx new file mode 100644 index 0000000000..2ed4a4cbb1 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/config/overview.mdx @@ -0,0 +1,51 @@ +--- +title: Overview +description: Configure the app itself — its identity, default permissions, and what runs at install time. +icon: screwdriver-wrench +--- + +A Twenty app's **config layer** is what describes the app *to the platform* — its identity, the permissions it holds, and the code that runs during install or upgrade. These declarations don't add new data shapes or runtime behavior; they tell Twenty *who the app is* and *how to set it up*. + +```text +┌────────────────────────────────────────────────────────┐ +│ Application — identity, default role, variables, │ +│ marketplace metadata │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Role — what the app's logic functions can read │ │ +│ │ and write (referenced by Application) │ │ +│ └──────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────┘ + │ + ▼ (at install / upgrade time) + ┌──────────────────────────────────┐ + │ Pre-install hook │ before metadata migration + └──────────────────────────────────┘ + ┌──────────────────────────────────┐ + │ Post-install hook │ after metadata migration + └──────────────────────────────────┘ +``` + +## In this section + + + + `defineApplication` — identity, default role, variables, marketplace metadata. + + + `defineRole` — declare what your app's logic functions can read and write. + + + `definePreInstallLogicFunction` and `definePostInstallLogicFunction` — back up data, seed defaults, validate upgrades. + + + +## How the pieces relate + +* **Application** is the entry point. Every app has exactly one `defineApplication()` call, and it points at one **Role** as its default. +* The **Role** controls what the app's logic functions and front components can read and write. Follow least-privilege: only grant the permissions your code actually needs. +* **Install Hooks** run during install or upgrade — pre-install before the metadata migration (so it can refuse a risky upgrade), post-install after the migration (so it can seed default data against the new schema). + + +Install hooks share the [logic function](/l/zh/developers/extend/apps/logic/logic-functions) runtime — same handler signature, same environment variables, same typed API client — but they're declared with their own define functions and live outside the regular trigger model (HTTP, cron, database events). + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/config/public-assets.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/config/public-assets.mdx new file mode 100644 index 0000000000..338c0f7f8e --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/config/public-assets.mdx @@ -0,0 +1,59 @@ +--- +title: Public Assets +description: Ship static files — images, icons, fonts — alongside your app via the public/ folder. +icon: folder-open +--- + +The `public/` folder at the root of your app holds static files — images, icons, fonts, or any other assets your app needs at runtime. These files are automatically included in builds, synced during dev mode, and uploaded to the server. + +Files placed in `public/` are: + +* **Publicly accessible** — once synced to the server, assets are served at a public URL. No authentication is needed to access them. +* **Available in front components** — use asset URLs to display images, icons, or any media inside your React components. +* **Available in logic functions** — reference asset URLs in emails, API responses, or any server-side logic. +* **Used for marketplace metadata** — the `logoUrl` and `screenshots` fields in `defineApplication()` reference files from this folder (e.g., `public/logo.png`). These are displayed in the marketplace when your app is published. +* **Auto-synced in dev mode** — when you add, update, or delete a file in `public/`, it is synced to the server automatically. No restart needed. +* **Included in builds** — `yarn twenty build` bundles all public assets into the distribution output. + +## Accessing public assets with `getPublicAssetUrl` + +Use the `getPublicAssetUrl` helper from `twenty-sdk` to get the full URL of a file in your `public/` directory. It works in both **logic functions** and **front components**. + +**In a logic function:** + +```ts src/logic-functions/send-invoice.ts +import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define'; + +const handler = async (): Promise => { + const logoUrl = getPublicAssetUrl('logo.png'); + const invoiceUrl = getPublicAssetUrl('templates/invoice.png'); + + // Fetch the file content (no auth required — public endpoint) + const response = await fetch(invoiceUrl); + const buffer = await response.arrayBuffer(); + + return { logoUrl, size: buffer.byteLength }; +}; + +export default defineLogicFunction({ + universalIdentifier: 'a1b2c3d4-...', + name: 'send-invoice', + description: 'Sends an invoice with the app logo', + timeoutSeconds: 10, + handler, +}); +``` + +**In a front component:** + +```tsx src/front-components/company-card.tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +export default defineFrontComponent(() => { + const logoUrl = getPublicAssetUrl('logo.png'); + + return App logo; +}); +``` + +The `path` argument is relative to your app's `public/` folder. Both `getPublicAssetUrl('logo.png')` and `getPublicAssetUrl('public/logo.png')` resolve to the same URL — the `public/` prefix is stripped automatically if present. diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/config/roles.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/config/roles.mdx new file mode 100644 index 0000000000..3266959ddd --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/config/roles.mdx @@ -0,0 +1,90 @@ +--- +title: Roles & Permissions +description: Declare what objects and fields your app's logic functions and front components can read and write. +icon: shield-halved +--- + +A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role declared as `defaultRoleUniversalIdentifier` in [`defineApplication`](/l/zh/developers/extend/apps/config/application). + +```ts src/roles/restricted-company-role.ts +import { + defineRole, + PermissionFlag, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; + +export default defineRole({ + universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6', + label: 'My new role', + description: 'A role that can be used in your workspace', + canReadAllObjectRecords: false, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }, + ], + fieldPermissions: [ + { + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + fieldUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier, + canReadFieldValue: false, + canUpdateFieldValue: false, + }, + ], + permissionFlags: [PermissionFlag.APPLICATIONS], +}); +``` + +## The default function role + +When you scaffold a new app, the CLI creates a default role file: + +```ts src/roles/default-role.ts +import { defineRole, PermissionFlag } from 'twenty-sdk/define'; + +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = + 'b648f87b-1d26-4961-b974-0908fd991061'; + +export default defineRole({ + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + label: 'Default function role', + description: 'Default role for function Twenty client', + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: false, + canBeAssignedToApiKeys: false, + objectPermissions: [], + fieldPermissions: [], + permissionFlags: [], +}); +``` + +This role's `universalIdentifier` is referenced from `application-config.ts` as `defaultRoleUniversalIdentifier`: + +* **`*.role.ts`** declares what the role can do. +* **`application-config.ts`** points to that role so your functions inherit its permissions. + +## Best practices + +* Start from the scaffolded role, then progressively restrict it — the default grants broad read access, which is rarely what you want in production. +* Replace `objectPermissions` and `fieldPermissions` with the exact objects and fields your functions actually need. +* `permissionFlags` control access to platform-level capabilities. Keep them minimal. +* See a working example: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts). diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/data/extending-objects.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/data/extending-objects.mdx new file mode 100644 index 0000000000..cdb2ddb9b4 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/data/extending-objects.mdx @@ -0,0 +1,48 @@ +--- +title: Extending Objects +description: Add fields to standard Twenty objects (Person, Company, …) or to objects from other apps using defineField. +icon: wand-magic-sparkles +--- + +Use `defineField()` to add a field to an object you don't own — a standard Twenty object like Person or Company, or an object shipped by another installed app. Unlike inline fields declared inside [`defineObject`](/l/zh/developers/extend/apps/data/objects), standalone fields require an `objectUniversalIdentifier` to specify which object they extend. + +```ts src/fields/company-loyalty-tier.field.ts +import { defineField, FieldType } from 'twenty-sdk/define'; + +export default defineField({ + universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890', + objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object + name: 'loyaltyTier', + type: FieldType.SELECT, + label: 'Loyalty Tier', + icon: 'IconStar', + options: [ + { value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' }, + { value: 'SILVER', label: 'Silver', position: 1, color: 'gray' }, + { value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' }, + ], +}); +``` + +## Key points + +* `objectUniversalIdentifier` identifies the target object. For standard Twenty objects, import the constant from `twenty-sdk`: + + ```ts + import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define'; + + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier + // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier + // … + ``` + +* When defining fields **inline inside `defineObject()`**, you do **not** need `objectUniversalIdentifier` — it's inherited from the parent object. + +* `defineField()` is the only way to add fields to objects you didn't create with `defineObject()`. + +* File location is up to you. The convention is `src/fields/\.field.ts`, but the SDK detects fields anywhere in `src/`. + +## Adding a relation to an existing object + +To add a relation field (e.g. linking your custom object to a standard `Person`), use `defineField()` with `FieldType.RELATION`. The pattern is the same as for inline relations but with `objectUniversalIdentifier` set explicitly. See [Relations](/l/zh/developers/extend/apps/data/relations) for the bidirectional pattern. diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/data/objects.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/data/objects.mdx new file mode 100644 index 0000000000..a9c0c5a30b --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/data/objects.mdx @@ -0,0 +1,93 @@ +--- +title: 对象 +description: 使用 defineObject 声明新的记录类型——具有其自身字段的自定义表。 +icon: 表格 +--- + +Custom **objects** are new record types your app adds to a workspace — Post Card, Invoice, Subscription, anything specific to your domain. Each object declares its schema (fields, relations, default values) and a stable universal identifier that survives across syncs and deploys. + +```ts src/objects/post-card.object.ts +import { defineObject, FieldType } from 'twenty-sdk/define'; + +enum PostCardStatus { + DRAFT = 'DRAFT', + SENT = 'SENT', + DELIVERED = 'DELIVERED', + RETURNED = 'RETURNED', +} + +export default defineObject({ + universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', + nameSingular: 'postCard', + namePlural: 'postCards', + labelSingular: 'Post Card', + labelPlural: 'Post Cards', + description: 'A post card object', + icon: 'IconMail', + fields: [ + { + universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b', + name: 'content', + type: FieldType.TEXT, + label: 'Content', + description: "Postcard's content", + icon: 'IconAbc', + }, + { + universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac', + name: 'recipientName', + type: FieldType.FULL_NAME, + label: 'Recipient name', + icon: 'IconUser', + }, + { + universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266', + name: 'recipientAddress', + type: FieldType.ADDRESS, + label: 'Recipient address', + icon: 'IconHome', + }, + { + universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e', + name: 'status', + type: FieldType.SELECT, + label: 'Status', + icon: 'IconSend', + defaultValue: `'${PostCardStatus.DRAFT}'`, + options: [ + { value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' }, + { value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' }, + { value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' }, + { value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' }, + ], + }, + { + universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433', + name: 'deliveredAt', + type: FieldType.DATE_TIME, + label: 'Delivered at', + icon: 'IconCheck', + isNullable: true, + defaultValue: null, + }, + ], +}); +``` + +## Key points + +* `universalIdentifier` 必须在各次部署间保持唯一且稳定。 +* 每个字段都需要 `name`、`type`、`label` 以及其自身稳定的 `universalIdentifier`。 +* `fields` 数组是可选的——你可以定义没有自定义字段的对象。 +* Inline fields defined here do **not** need an `objectUniversalIdentifier` — it's inherited from the parent object. Use [`defineField()`](/l/zh/developers/extend/apps/data/extending-objects) to add fields to objects you don't own. +* You can scaffold new objects with `yarn twenty add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/l/zh/developers/extend/apps/getting-started/scaffolding). + + +**Base fields are added automatically.** When you define a custom object, Twenty creates standard fields like `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and `deletedAt` for you. You don't need to declare them in your `fields` array — only your custom fields. You can override a default field by declaring one with the same name, but this is rarely a good idea. + + +## What's next + +* **Connect this object to others** — see [Relations](/l/zh/developers/extend/apps/data/relations) for the bidirectional relation pattern. +* **Add fields to objects from other apps** — see [Extending Objects](/l/zh/developers/extend/apps/data/extending-objects) for `defineField()`. +* **Display this object in the UI** — see [Views](/l/zh/developers/extend/apps/layout/views) and [Navigation Menu Items](/l/zh/developers/extend/apps/layout/navigation-menu-items) to put it in the sidebar. diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/data/overview.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/data/overview.mdx new file mode 100644 index 0000000000..d4227816c6 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/data/overview.mdx @@ -0,0 +1,52 @@ +--- +title: Overview +description: Shape the data your app adds to a workspace — objects, fields, and relations. +icon: database +--- + +A Twenty app's **data layer** is the data your app *adds* to a workspace — the new record types it declares, the columns it adds to existing objects, and how those records connect to each other. + +```text +┌──────────────────────────────────────────────────┐ +│ Object — a record type, e.g. PostCard │ +│ ├─ Field (name, type, label) │ +│ ├─ Field │ +│ └─ Relation (link to another object) │ +└──────────────────────────────────────────────────┘ + │ + ├── lives in your app, OR + │ + ▼ +┌──────────────────────────────────────────────────┐ +│ Standard / other apps' objects │ +│ └─ Field added by your app via defineField │ +└──────────────────────────────────────────────────┘ +``` + +## In this section + + + + `defineObject` — declare new record types with their own fields. + + + `defineField` — add fields to standard or other apps' objects. + + + Bidirectional `MANY_TO_ONE` / `ONE_TO_MANY` connections between objects. + + + +## Entities at a glance + +| Entity | Purpose | Defined with | +| ------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| **Object** | A new custom record type (e.g. PostCard, Invoice) with its own fields | `defineObject()` | +| **Field** | A column on an object. Standalone fields can extend objects you didn't create (e.g. add `loyaltyTier` to Company) | `defineField()` | +| **Relation** | A bidirectional link between two objects — both sides declared as fields | `defineField()` with `FieldType.RELATION` | + +The SDK detects these via AST analysis at build time, so file organization is up to you — the convention is `src/objects/` and `src/fields/`. Stable `universalIdentifier` UUIDs tie everything together across deploys. + + +Looking for **Application Config** or **Roles & Permissions**? Those describe the app itself rather than the data it adds — they live under [Config](/l/zh/developers/extend/apps/config/overview). Looking for **Connections** (Linear, GitHub, Slack OAuth)? Those exist to be called *from* logic functions and live under [Logic](/l/zh/developers/extend/apps/logic/connections). + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/data/relations.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/data/relations.mdx new file mode 100644 index 0000000000..b64a2ece3a --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/data/relations.mdx @@ -0,0 +1,160 @@ +--- +title: 关系 +description: Connect objects together with bidirectional MANY_TO_ONE / ONE_TO_MANY relations. +icon: diagram-project +--- + +Relations connect two objects together. In Twenty, relations are always **bidirectional** — every relation has two sides, and each side is declared as a field that references the other. + +| 关系类型 | 描述 | 是否有外键? | +| ------------- | ------------------- | --------------------- | +| `MANY_TO_ONE` | 该对象的多条记录指向目标对象的一条记录 | 是(`joinColumnName`) | +| `ONE_TO_MANY` | 该对象的一条记录拥有目标对象的多条记录 | No (the inverse side) | + +## How relations work + +Every relation requires **two fields** that reference each other: + +1. The **MANY_TO_ONE** side — lives on the object that holds the foreign key. +2. The **ONE_TO_MANY** side — lives on the object that owns the collection. + +Both fields use `FieldType.RELATION` and cross-reference each other via `relationTargetFieldMetadataUniversalIdentifier`. + +## Example: Post Card has many Recipients + +A `PostCard` can be sent to many `PostCardRecipient` records. Each recipient belongs to exactly one post card. + +**Step 1: Define the ONE_TO_MANY side on PostCard** (the "one" side): + +```ts src/fields/post-card-recipients-on-post-card.field.ts +import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111'; +// Import from the other side +import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field'; + +export default defineField({ + universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCardRecipients', + label: 'Post Card Recipients', + icon: 'IconUsers', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID, + universalSettings: { + relationType: RelationType.ONE_TO_MANY, + }, +}); +``` + +**Step 2: Define the MANY_TO_ONE side on PostCardRecipient** (the "many" side — holds the foreign key): + +```ts src/fields/post-card-on-post-card-recipient.field.ts +import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; +import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; + +// Export so the other side can reference it +export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222'; +// Import from the other side +import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field'; + +export default defineField({ + universalIdentifier: POST_CARD_FIELD_ID, + objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + icon: 'IconMail', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, +}); +``` + + +**Circular imports:** both relation fields reference each other's `universalIdentifier`. To avoid circular import issues, export your field IDs as named constants from each file and import them in the other. The build system resolves these at compile time. + + +## Relating to standard objects + +To create a relation with a built-in Twenty object (Person, Company, etc.), use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`: + +```ts src/fields/person-on-self-hosting-user.field.ts +import { + defineField, + FieldType, + RelationType, + OnDeleteAction, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk/define'; +import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object'; + +export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333'; +export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444'; + +export default defineField({ + universalIdentifier: PERSON_FIELD_ID, + objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER, + type: FieldType.RELATION, + name: 'person', + label: 'Person', + description: 'Person matching with the self hosting user', + isNullable: true, + relationTargetObjectMetadataUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, + relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.SET_NULL, + joinColumnName: 'personId', + }, +}); +``` + +## Relation field properties + +| Property | Required | Description | +| ------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------- | +| `type` | Yes | Must be `FieldType.RELATION` | +| `relationTargetObjectMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the target object | +| `relationTargetFieldMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the matching field on the target object | +| `universalSettings.relationType` | Yes | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` | +| `universalSettings.onDelete` | MANY_TO_ONE only | What happens when the referenced record is deleted: `CASCADE`, `SET_NULL`, `RESTRICT`, or `NO_ACTION` | +| `universalSettings.joinColumnName` | MANY_TO_ONE only | Database column name for the foreign key (e.g., `postCardId`) | + +## Inline relation fields + +You can also declare a relation directly inside [`defineObject`](/l/zh/developers/extend/apps/data/objects). When inline, omit `objectUniversalIdentifier` — it's inherited from the parent object: + +```ts +export default defineObject({ + universalIdentifier: '...', + nameSingular: 'postCardRecipient', + // ... + fields: [ + { + universalIdentifier: POST_CARD_FIELD_ID, + type: FieldType.RELATION, + name: 'postCard', + label: 'Post Card', + relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, + universalSettings: { + relationType: RelationType.MANY_TO_ONE, + onDelete: OnDeleteAction.CASCADE, + joinColumnName: 'postCardId', + }, + }, + // … other fields + ], +}); +``` diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/concepts.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/concepts.mdx new file mode 100644 index 0000000000..f9043ce366 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/concepts.mdx @@ -0,0 +1,101 @@ +--- +title: Concepts +description: How Twenty apps work — entity model, sandboxing, and the install lifecycle. +icon: sitemap +--- + +Twenty apps are TypeScript packages that extend your workspace with custom objects, logic, UI components, and AI capabilities. They run on the Twenty platform with full sandboxing and permission controls. + +## How apps work + +An app is a collection of **entities** declared using `defineEntity()` functions from the `twenty-sdk` package. The SDK detects these declarations via AST analysis at build time and produces a **manifest** — a complete description of what your app adds to a workspace. These functions validate your configuration at build time and provide IDE autocompletion and type safety. + +``` +your-app/ +├── src/ +│ ├── application-config.ts ← defineApplication (required, one per app) +│ ├── roles/ ← defineRole +│ ├── objects/ ← defineObject +│ ├── fields/ ← defineField +│ ├── logic-functions/ ← defineLogicFunction +│ ├── front-components/ ← defineFrontComponent +│ ├── skills/ ← defineSkill +│ ├── agents/ ← defineAgent +│ ├── views/ ← defineView +│ ├── navigation-menu-items/ ← defineNavigationMenuItem +│ └── page-layouts/ ← definePageLayout +├── public/ ← Static assets (images, icons) +└── package.json +``` + + + **File organization is up to you.** Entity detection is AST-based — the SDK finds `export default defineEntity(...)` calls regardless of where the file lives. The folder structure above is a convention, not a requirement. + + +## Entity types + +| Entity | Purpose | Docs | +| ------------------------ | ------------------------------------------ | ----------------------------------------------------------------------------- | +| **Application** | App identity, default role, variables | [Application Config](/l/zh/developers/extend/apps/config/application) | +| **Role** | Permission sets on objects and fields | [Roles & Permissions](/l/zh/developers/extend/apps/config/roles) | +| **Object** | Custom record types with fields | [Objects](/l/zh/developers/extend/apps/data/objects) | +| **Field** | Add fields to objects from other apps | [Extending Objects](/l/zh/developers/extend/apps/data/extending-objects) | +| **Relation** | Bidirectional links between objects | [Relations](/l/zh/developers/extend/apps/data/relations) | +| **Logic Function** | Server-side TypeScript with triggers | [Logic Functions](/l/zh/developers/extend/apps/logic/logic-functions) | +| **Skill** | Reusable AI agent instructions | [Skills & Agents](/l/zh/developers/extend/apps/logic/skills-and-agents) | +| **Agent** | AI assistants with custom prompts | [Skills & Agents](/l/zh/developers/extend/apps/logic/skills-and-agents) | +| **Connection Provider** | OAuth credentials for third-party APIs | [Connections](/l/zh/developers/extend/apps/logic/connections) | +| **View** | Pre-configured record list views | [Views](/l/zh/developers/extend/apps/layout/views) | +| **Navigation Menu Item** | Custom sidebar entries | [Navigation Menu Items](/l/zh/developers/extend/apps/layout/navigation-menu-items) | +| **Page Layout** | Tabs and widgets on a record's detail page | [Page Layouts](/l/zh/developers/extend/apps/layout/page-layouts) | +| **Front Component** | Sandboxed React UI inside Twenty | [Front Components](/l/zh/developers/extend/apps/layout/front-components) | +| **Command Menu Item** | Quick actions and Cmd+K entries | [Command Menu Items](/l/zh/developers/extend/apps/layout/command-menu-items) | + +## Sandboxing + +* **Logic functions** run in isolated Node.js processes on the server. They only access data through the typed API client, scoped to the app's role permissions. +* **Front components** run in Web Workers using Remote DOM — sandboxed from the main page but rendering native DOM elements (not iframes). They communicate with Twenty via a message-passing host API. +* **Permissions** are enforced at the API level. The runtime token (`TWENTY_APP_ACCESS_TOKEN`) is derived from the role defined in `defineApplication()`. + +## App lifecycle + +``` +┌─────────────────────────────────────────────────────────┐ +│ Development │ +│ npx create-twenty-app → yarn twenty dev (live sync) │ +├─────────────────────────────────────────────────────────┤ +│ Build & Deploy │ +│ yarn twenty build → yarn twenty deploy │ +├─────────────────────────────────────────────────────────┤ +│ Install flow │ +│ upload → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +├─────────────────────────────────────────────────────────┤ +│ Publish │ +│ npm publish → appears in Twenty marketplace │ +└─────────────────────────────────────────────────────────┘ +``` + +* **`yarn twenty dev`** — watches your source files and live-syncs changes to a connected Twenty server. The typed API client is regenerated automatically when the schema changes. +* **`yarn twenty build`** — compiles TypeScript, bundles logic functions and front components with esbuild, and produces a manifest. +* **Pre/post-install hooks** — optional functions that run during installation. See [Install Hooks](/l/zh/developers/extend/apps/config/install-hooks) for details. + +## Next steps + + + + Application identity, default role, and install hooks. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/local-server.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/local-server.mdx new file mode 100644 index 0000000000..a424e5203a --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/local-server.mdx @@ -0,0 +1,72 @@ +--- +title: Local Server +description: Manage the local Twenty Docker server — start, stop, upgrade, parallel test instance, and manual SDK setup. +icon: server +--- + +## Managing the local server + +Use `yarn twenty server` to control the local Twenty container: + +| Command | What it does | +| -------------------------------------- | -------------------------------------------- | +| `yarn twenty server start` | Start the server (pulls the image if needed) | +| `yarn twenty server start --port 3030` | Start on a custom port | +| `yarn twenty server stop` | Stop the server (preserves data) | +| `yarn twenty server status` | Show URL, version, and login credentials | +| `yarn twenty server logs` | Stream server logs | +| `yarn twenty server reset` | Wipe data and start fresh | +| `yarn twenty server upgrade` | Pull the latest `twenty-app-dev` image | +| `yarn twenty server upgrade 2.2.0` | Upgrade to a specific version | + +Data persists across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything. + +## Upgrading the server image + +`yarn twenty server upgrade` pulls the latest image, compares digests, and only recreates the container if anything actually changed. Volumes are preserved — only the container is replaced. If a new image was pulled and the container was running, the upgrade automatically starts a new container; run `yarn twenty server start` afterward to wait for it to become healthy. + +```bash filename="Terminal" +yarn twenty server upgrade # Latest +yarn twenty server upgrade 2.2.0 # Specific version +``` + +Verify the running version with `yarn twenty server status` (it shows the `APP_VERSION` baked into the container). + +## Running a parallel test instance + +Pass `--test` to any `server` command to manage a second, fully isolated instance — useful for integration tests or experiments without touching your main dev data: + +| Command | What it does | +| ----------------------------------- | ----------------------------------------------- | +| `yarn twenty server start --test` | Start the test instance (defaults to port 2021) | +| `yarn twenty server stop --test` | Stop it | +| `yarn twenty server status --test` | Show its status | +| `yarn twenty server logs --test` | Stream its logs | +| `yarn twenty server reset --test` | Wipe its data | +| `yarn twenty server upgrade --test` | Upgrade its image | + +The test instance has its own container (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`), and config — it runs alongside your main instance without conflicts. Combine `--test` with `--port` to override 2021. + +## Manual setup (without the scaffolder) + +Skip the scaffolder if you're adding the SDK to an existing project: + +```bash filename="Terminal" +yarn add twenty-sdk twenty-client-sdk +``` + +Add the script to `package.json`: + +```json filename="package.json" +{ + "scripts": { + "twenty": "twenty" + } +} +``` + +You can now run `yarn twenty dev`, `yarn twenty server start`, and the rest. + + +Don't install `twenty-sdk` globally — pin it per project so each app uses its own version. + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/project-structure.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/project-structure.mdx new file mode 100644 index 0000000000..ad85a70452 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/project-structure.mdx @@ -0,0 +1,40 @@ +--- +title: Project Structure +description: What's inside a scaffolded Twenty app — files, folders, and what each one does. +icon: folder-tree +--- + +A new app generated by `npx create-twenty-app` looks like this: + +```text filename="my-twenty-app/" +my-twenty-app/ + package.json + src/ + application-config.ts # Required — your app's entry point + default-role.ts # Permissions for logic functions + constants/ + universal-identifiers.ts # Auto-generated UUIDs and metadata + __tests__/ + setup-test.ts + app-install.integration-test.ts + .github/workflows/ci.yml # GitHub Actions + public/ # Static assets + vitest.config.ts # Test runner config + tsconfig.json, tsconfig.spec.json + .nvmrc, .yarnrc.yml, .oxlintrc.json + README.md, LLMS.md +``` + +## Key files + +| File / Folder | Purpose | +| ---------------------------------------- | -------------------------------------------------------------- | +| `src/application-config.ts` | **Required.** The main configuration file for your app. | +| `src/default-role.ts` | Default role controlling what your logic functions can access. | +| `src/constants/universal-identifiers.ts` | Auto-generated UUIDs and metadata (display name, description). | +| `src/__tests__/` | Integration tests (setup + example test). | +| `public/` | Static assets (images, fonts) served with your app. | + + +**File organization is up to you.** The folders above are conventions — the SDK detects entities via AST analysis on `export default defineEntity(...)` calls regardless of where the file lives. + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/quick-start.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/quick-start.mdx new file mode 100644 index 0000000000..10b61e6dcf --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/quick-start.mdx @@ -0,0 +1,184 @@ +--- +title: Quick Start +icon: rocket +description: Create your first Twenty app in minutes. +--- + +## Prerequisites + +* **Node.js 24+** — [Download](https://nodejs.org/) +* **Yarn 4** — bundled with Node via Corepack. Enable it: `corepack enable` +* **Docker** — [Download](https://www.docker.com/products/docker-desktop/). Needed to run a local Twenty server. Skip if you already have Twenty running elsewhere. + +Building a Twenty app has three phases. The scaffolder collapses them into one happy-path command, but each phase is a separate concept — when something fails, knowing which phase you're in tells you what to fix. + +| Phase | What you do | Tool | Result | +| ------------------- | ---------------------------------- | ----------------------------- | ----------------------------- | +| **1. Scaffold** | Generate the app's source code | `npx create-twenty-app` | A TypeScript project on disk | +| **2. Run a server** | Start a Twenty server to sync into | Docker + `yarn twenty server` | A running Twenty instance | +| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` | Your changes appear in the UI | + +--- + +## Phase 1 — Scaffold your project + +Create a new app from the template: + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app +``` + +You'll be prompted for a name and description — press **Enter** for the defaults. This generates a TypeScript project in `my-twenty-app/` with a starter `application-config.ts`, a default role, a CI workflow, and an integration test. + +**After this phase:** you have an app's source code on your machine. It isn't running yet — that's Phase 2. + +--- + +## Phase 2 — Run a local Twenty server + +Your app needs a Twenty server to sync into. The server is a full Twenty instance — UI, GraphQL API, PostgreSQL — running locally in Docker. Your local code uploads its definitions to that server, which makes them appear in the UI. + +The scaffolder offers to start one for you: + +> **Would you like to set up a local Twenty instance?** + +* **Yes (recommended)** — pulls the `twentycrm/twenty-app-dev` Docker image and starts it on port `2020`. Make sure Docker is running first. +* **No** — choose this if you already have a Twenty server you want to connect to. You can wire it up later with `yarn twenty remote add`. + +
+ Should start local instance? +
+ +Once the server is up, a browser opens for sign-in. Use the pre-seeded demo account: + +* **Email:** `tim@apple.dev` +* **Password:** `tim@apple.dev` + +
+ Twenty login screen +
+ +Click **Authorize** on the next screen — this gives the CLI access to your workspace. + +
+ Twenty CLI authorization screen +
+ +Your terminal will confirm everything is set up. + +
+ App scaffolded successfully +
+ +**After this phase:** you have a running Twenty server at [http://localhost:2020](http://localhost:2020) with your CLI authorized to sync to it. + + +If Docker isn't installed or running, the scaffolder will tell you the right start command for your OS. Once Docker is up, you can resume with `yarn twenty server start` — no need to re-scaffold. + + +--- + +## Phase 3 — Sync your changes + +This is the inner loop you'll spend most of your time in. + +```bash filename="Terminal" +cd my-twenty-app +yarn twenty dev +``` + +This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a second the server reflects the change. You'll see a live status panel in your terminal. + +For more detailed output (build logs, sync requests, error traces), add `--verbose`. + +
+ Dev mode terminal output +
+ +Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). You should see your app under **Your Apps**. + +
+ Your Apps list showing My twenty app +
+ +Click **My twenty app** to see its **application registration** — a server-level record describing your app (name, identifier, OAuth credentials, source). One registration can be installed across multiple workspaces on the same server. + +
+ Application registration details +
+ +Click **View installed app** to see the workspace install. The **About** tab shows version and management options. + +
+ Installed app +
+ +**After this phase:** you have a live development loop. Edit any file in `src/` and it appears in the UI. + +### One-shot sync for CI and scripts + +Pass `--once` to run a single build + sync and exit — same pipeline, no watcher: + +```bash filename="Terminal" +yarn twenty dev --once +``` + +| Command | Behavior | When to use | +| ------------------------ | ------------------------------------------------------------- | ---------------------------------------------------- | +| `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. | +| `yarn twenty dev --once` | Single build + sync, exits `0` on success, `1` on failure. | CI, pre-commit hooks, AI agents, scripted workflows. | + +Both modes need a server in development mode and an authenticated remote. + + +Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests — use `yarn twenty deploy` to deploy to production servers. See [Publishing](/l/zh/developers/extend/apps/operations/publishing). + + +--- + +## Starting from an example + +Use `--example` to start with a more complete project (custom objects, fields, logic functions, front components): + +```bash filename="Terminal" +npx create-twenty-app@latest my-twenty-app --example postcard +``` + +Examples live in [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). You can also scaffold individual entities into an existing project with `yarn twenty add` — see [Scaffolding](/l/zh/developers/extend/apps/getting-started/scaffolding). + +--- + +## What you can build + +Apps are composed of **entities** — each defined as a TypeScript file with a single `export default`: + +| Entity | What it does | +| ---------------------- | ----------------------------------------------------------------------------------- | +| **Objects & Fields** | Custom data models (Post Card, Invoice, etc.) with typed fields | +| **Logic functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events | +| **Front components** | React components that render inside Twenty's UI (side panel, widgets, command menu) | +| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants | +| **Views & Navigation** | Pre-configured list views and sidebar menu items | +| **Page layouts** | Custom record detail pages with tabs and widgets | + +Full reference: [Concepts](/l/zh/developers/extend/apps/getting-started/concepts). + +## Next steps + + + + Application identity, default role, install hooks, public assets. + + + Objects, fields, and bidirectional relations. + + + Logic functions, skills, agents, and OAuth connections. + + + Views, navigation, page layouts, front components. + + + CLI, testing, remotes, CI, and publishing your app. + + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/scaffolding.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/scaffolding.mdx new file mode 100644 index 0000000000..b2853cadf2 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/scaffolding.mdx @@ -0,0 +1,58 @@ +--- +title: Scaffolding +description: Generate entity files interactively with yarn twenty add — objects, fields, views, logic functions, and more. +icon: wand-magic-sparkles +--- + +Instead of creating entity files by hand, use the interactive scaffolder: + +```bash filename="Terminal" +yarn twenty add +``` + +It prompts you to pick an entity type and walks you through the required fields, then writes a ready-to-use file with a stable `universalIdentifier` and the correct `defineEntity()` call. + +You can also pass the entity type directly to skip the first prompt: + +```bash filename="Terminal" +yarn twenty add object +yarn twenty add logicFunction +yarn twenty add frontComponent +``` + +## Available entity types + +| Entity type | Command | Generated file | +| -------------------- | ------------------------------------ | ------------------------------------------------------- | +| Object | `yarn twenty add object` | `src/objects/\.ts` | +| Field | `yarn twenty add field` | `src/fields/\.ts` | +| Logic function | `yarn twenty add logicFunction` | `src/logic-functions/\.ts` | +| Front component | `yarn twenty add frontComponent` | `src/front-components/\.tsx` | +| Role | `yarn twenty add role` | `src/roles/\.ts` | +| Skill | `yarn twenty add skill` | `src/skills/\.ts` | +| Agent | `yarn twenty add agent` | `src/agents/\.ts` | +| View | `yarn twenty add view` | `src/views/\.ts` | +| Navigation menu item | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\.ts` | +| Page layout | `yarn twenty add pageLayout` | `src/page-layouts/\.ts` | + +## What the scaffolder generates + +Each entity type has its own template. For example, `yarn twenty add object` asks for: + +1. **Name (singular)** — e.g., `invoice` +2. **Name (plural)** — e.g., `invoices` +3. **Label (singular)** — auto-populated from the name (e.g., `Invoice`) +4. **Label (plural)** — auto-populated (e.g., `Invoices`) +5. **Create a view and navigation item?** — if you answer yes, the scaffolder also generates a matching view and sidebar link for the new object. + +Other entity types have simpler prompts — most only ask for a name. + +The `field` entity type is more detailed: it asks for the field name, label, type (from a list of all available field types like `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.), and the target object's `universalIdentifier`. + +## Custom output path + +Use the `--path` flag to place the generated file in a custom location: + +```bash filename="Terminal" +yarn twenty add logicFunction --path src/custom-folder +``` diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/troubleshooting.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/troubleshooting.mdx new file mode 100644 index 0000000000..c879b2d198 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/getting-started/troubleshooting.mdx @@ -0,0 +1,12 @@ +--- +title: 故障排除 +description: 常见的首次运行问题 — Docker、Node 版本、Yarn、依赖项。 +icon: wrench +--- + +* **Docker 错误** — 在运行 `yarn twenty server start` 之前,请确保 Docker Desktop(或守护进程)已在运行。 错误消息会显示适用于你的操作系统的正确启动命令。 +* **Node 版本不正确** — 需要 24+。 使用 `node -v` 检查。 +* **缺少 Yarn 4** — 运行 `corepack enable`。 +* **依赖损坏** — `rm -rf node_modules && yarn install`。 + +卡住了吗? 在 [Twenty 的 Discord](https://discord.com/channels/1130383047699738754/1130386664812982322) 上提问。 diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/layout/command-menu-items.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/layout/command-menu-items.mdx new file mode 100644 index 0000000000..4130082e20 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/layout/command-menu-items.mdx @@ -0,0 +1,144 @@ +--- +title: Command Menu Items +description: Surface front components as quick actions and command menu (Cmd+K) entries with defineCommandMenuItem. +icon: terminal +--- + +A **command menu item** is the bridge between the user and a [front component](/l/zh/developers/extend/apps/layout/front-components). It registers the component in Twenty's command menu (Cmd+K) and, optionally, as a pinned quick-action button in the top-right corner of the page. + +```ts src/command-menu-items/open-dashboard.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + label: 'Open Dashboard', + shortLabel: 'Dashboard', + icon: 'IconLayoutDashboard', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +## Configuration fields + +| Field | Required | Description | +| --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `universalIdentifier` | Yes | Stable unique ID for the command | +| `label` | Yes | Full label shown in the command menu (Cmd+K) | +| `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens | +| `shortLabel` | No | Shorter label displayed on the pinned quick-action button | +| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) | +| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page | +| `availabilityType` | No | Controls where the command appears: `'GLOBAL'` (always available), `'RECORD_SELECTION'` (only when records are selected), or `'FALLBACK'` (shown when no other commands match) | +| `availabilityObjectUniversalIdentifier` | No | Restrict the command to pages of a specific object type (e.g. only on Company records) | +| `conditionalAvailabilityExpression` | No | A boolean expression that dynamically controls visibility (see below) | + +## Headless commands + +A command menu item paired with a [headless front component](/l/zh/developers/extend/apps/layout/front-components#headless-vs-non-headless) is the idiomatic way to ship a one-click action — run code, navigate, or confirm and execute. The Front Components page covers the [SDK Command components](/l/zh/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) that handle the action-and-unmount pattern. + +A typical flow: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +## Conditional availability expressions + +The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions: + +```ts src/command-menu-items/bulk-update.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; +import { + objectPermissions, + everyEquals, +} from 'twenty-sdk/front-component'; + +export default defineCommandMenuItem({ + universalIdentifier: '...', + label: 'Bulk Update', + availabilityType: 'RECORD_SELECTION', + frontComponentUniversalIdentifier: '...', + conditionalAvailabilityExpression: everyEquals( + objectPermissions, + 'canUpdateObjectRecords', + true, + ), +}); +``` + +### Context variables + +These represent the current state of the page: + +| Variable | Type | Description | +| ------------------------------ | --------- | ---------------------------------------------------------------- | +| `pageType` | `string` | Current page type (e.g. `'RecordIndexPage'`, `'RecordShowPage'`) | +| `isInSidePanel` | `boolean` | Whether the component is rendered in a side panel | +| `numberOfSelectedRecords` | `number` | Number of currently selected records | +| `isSelectAll` | `boolean` | Whether "select all" is active | +| `selectedRecords` | `array` | The selected record objects | +| `favoriteRecordIds` | `array` | IDs of favorited records | +| `objectPermissions` | `object` | Permissions for the current object type | +| `targetObjectReadPermissions` | `object` | Read permissions for the target object | +| `targetObjectWritePermissions` | `object` | Write permissions for the target object | +| `featureFlags` | `object` | Active feature flags | +| `objectMetadataItem` | `object` | Metadata of the current object type | +| `hasAnySoftDeleteFilterOnView` | `boolean` | Whether the current view has a soft-delete filter | + +### Operators + +Combine variables into boolean expressions: + +| Operator | Description | +| ----------------------------------- | ----------------------------------------------------------------- | +| `isDefined(value)` | `true` if the value is not null/undefined | +| `isNonEmptyString(value)` | `true` if the value is a non-empty string | +| `includes(array, value)` | `true` if the array contains the value | +| `includesEvery(array, prop, value)` | `true` if every item's property includes the value | +| `every(array, prop)` | `true` if the property is truthy on every item | +| `everyDefined(array, prop)` | `true` if the property is defined on every item | +| `everyEquals(array, prop, value)` | `true` if the property equals the value on every item | +| `some(array, prop)` | `true` if the property is truthy on at least one item | +| `someDefined(array, prop)` | `true` if the property is defined on at least one item | +| `someEquals(array, prop, value)` | `true` if the property equals the value on at least one item | +| `someNonEmptyString(array, prop)` | `true` if the property is a non-empty string on at least one item | +| `none(array, prop)` | `true` if the property is falsy on every item | +| `noneDefined(array, prop)` | `true` if the property is undefined on every item | +| `noneEquals(array, prop, value)` | `true` if the property does not equal the value on any item | diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/layout/front-components.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/layout/front-components.mdx new file mode 100644 index 0000000000..579d61582d --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/layout/front-components.mdx @@ -0,0 +1,404 @@ +--- +title: Front Components +description: Build React components that render inside Twenty's UI with sandboxed isolation. +icon: window-maximize +--- + +Front components are React components that render directly inside Twenty's UI. They run in an **isolated Web Worker** using Remote DOM — your code is sandboxed but renders natively in the page, not in an iframe. + +## Where front components can be used + +Front components can render in two locations within Twenty: + +* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu. +* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside [page layouts](/l/zh/developers/extend/apps/layout/page-layouts). When configuring a dashboard or a record page layout, users can add a front component widget. + +A front component on its own isn't reachable from the UI — you need to *surface* it. The two ways to do that are: + +* **Pair it with a [command menu item](/l/zh/developers/extend/apps/layout/command-menu-items)** — registers it in the command menu (Cmd+K) and, optionally, as a pinned quick-action. +* **Embed it as a widget in a [page layout](/l/zh/developers/extend/apps/layout/page-layouts)** — places it on a record's detail page or dashboard. + +## Basic example + +The quickest way to see a front component in action is to pair it with a [`defineCommandMenuItem`](/l/zh/developers/extend/apps/layout/command-menu-items), so it appears as a quick-action button in the top-right corner of the page: + +```tsx src/front-components/hello-world.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; + +const HelloWorld = () => { + return ( +
+

Hello from my app!

+

This component renders inside Twenty.

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', + name: 'hello-world', + description: 'A simple front component', + component: HelloWorld, +}); +``` + +```ts src/command-menu-items/hello-world.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345', + shortLabel: 'Hello', + label: 'Hello World', + icon: 'IconBolt', + isPinned: true, + availabilityType: 'GLOBAL', + frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', +}); +``` + +After syncing with `yarn twenty dev` (or running a one-shot `yarn twenty dev --once`), the quick action appears in the top-right corner of the page: + +
+ Quick action button in the top-right corner +
+ +Click it to render the component inline. + +## Configuration fields + +| Field | Required | Description | +| --------------------- | -------- | ------------------------------------------------------------ | +| `universalIdentifier` | Yes | Stable unique ID for this component | +| `component` | Yes | A React component function | +| `name` | No | Display name | +| `description` | No | Description of what the component does | +| `isHeadless` | No | Set to `true` if the component has no visible UI (see below) | + +## Placing a front component on a page + +Beyond commands, you can embed a front component directly into a record page by adding it as a widget in a **page layout**. See [Page Layouts](/l/zh/developers/extend/apps/layout/page-layouts) for details. + +## Headless vs non-headless + +Front components come in two rendering modes controlled by the `isHeadless` option: + +**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted. + +**Headless (`isHeadless: true`)** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below. + +```tsx src/front-components/sync-tracker.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component'; +import { useEffect } from 'react'; + +const SyncTracker = () => { + const recordId = useRecordId(); + + useEffect(() => { + enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' }); + }, [recordId]); + + return null; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'sync-tracker', + description: 'Tracks record views silently', + isHeadless: true, + component: SyncTracker, +}); +``` + +Because the component returns `null`, Twenty skips rendering a container for it — no empty space appears in the layout. The component still has access to all hooks and the host communication API. + +## SDK Command components + +The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done. + +Import them from `twenty-sdk/command`: + +* **`Command`** — Runs an async callback via the `execute` prop. +* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`. +* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`. +* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`. + +Here is a full example of a headless front component using `Command` to run an action from the command menu: + +```tsx src/front-components/run-action.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Command } from 'twenty-sdk/command'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const RunAction = () => { + const execute = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + createTask: { + __args: { data: { title: 'Created by my app' } }, + id: true, + }, + }); + }; + + return ; +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', + name: 'run-action', + description: 'Creates a task from the command menu', + component: RunAction, + isHeadless: true, +}); +``` + +```ts src/command-menu-items/run-action.command-menu-item.ts +import { defineCommandMenuItem } from 'twenty-sdk/define'; + +export default defineCommandMenuItem({ + universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', + label: 'Run my action', + icon: 'IconPlayerPlay', + frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', +}); +``` + +And an example using `CommandModal` to ask for confirmation before executing: + +```tsx src/front-components/delete-draft.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { CommandModal } from 'twenty-sdk/command'; + +const DeleteDraft = () => { + const execute = async () => { + // perform the deletion + }; + + return ( + + ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456', + name: 'delete-draft', + description: 'Deletes a draft with confirmation', + component: DeleteDraft, + isHeadless: true, +}); +``` + +## Accessing runtime context + +Inside your component, use SDK hooks to access the current user, record, and component instance: + +```tsx src/front-components/record-info.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { + useUserId, + useRecordId, + useFrontComponentId, +} from 'twenty-sdk/front-component'; + +const RecordInfo = () => { + const userId = useUserId(); + const recordId = useRecordId(); + const componentId = useFrontComponentId(); + + return ( +
+

User: {userId}

+

Record: {recordId ?? 'No record context'}

+

Component: {componentId}

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012', + name: 'record-info', + component: RecordInfo, +}); +``` + +Available hooks: + +| Hook | Returns | Description | +| --------------------------------------------- | ------------------ | ---------------------------------------------------------- | +| `useUserId()` | `string` or `null` | The current user's ID | +| `useSelectedRecordIds()` | `string[]` | All selected record IDs (empty array if none selected) | +| `useRecordId()` | `string` or `null` | **Deprecated.** Use `useSelectedRecordIds()` instead | +| `useFrontComponentId()` | `string` | This component instance's ID | +| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function | + +## Host communication API + +Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`: + +| Function | Description | +| ----------------------------------------------- | ----------------------------- | +| `navigate(to, params?, queryParams?, options?)` | Navigate to a page in the app | +| `openSidePanelPage(params)` | Open a side panel | +| `closeSidePanel()` | Close the side panel | +| `openCommandConfirmationModal(params)` | Show a confirmation dialog | +| `enqueueSnackbar(params)` | Show a toast notification | +| `unmountFrontComponent()` | Unmount the component | +| `updateProgress(progress)` | Update a progress indicator | + +Here is an example that uses the host API to show a snackbar and close the side panel after an action completes: + +```tsx src/front-components/archive-record.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useRecordId } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const ArchiveRecord = () => { + const recordId = useRecordId(); + + const handleArchive = async () => { + const client = new CoreApiClient(); + + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { status: 'ARCHIVED' } }, + id: true, + }, + }); + + await enqueueSnackbar({ + message: 'Record archived', + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Archive this record?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678', + name: 'archive-record', + description: 'Archives the current record', + component: ArchiveRecord, +}); +``` + +### Working with multiple records + +Use `useSelectedRecordIds()` to handle multiple selected records. This is useful for bulk operations: + +```tsx src/front-components/bulk-export.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component'; +import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; +import { CoreApiClient } from 'twenty-sdk/clients'; + +const BulkExport = () => { + const selectedRecordIds = useSelectedRecordIds(); + + const handleExport = async () => { + const client = new CoreApiClient(); + + for (const recordId of selectedRecordIds) { + await client.mutation({ + updateTask: { + __args: { id: recordId, data: { exported: true } }, + id: true, + }, + }); + } + + await enqueueSnackbar({ + message: `Exported ${selectedRecordIds.length} records`, + variant: 'success', + }); + + await closeSidePanel(); + }; + + return ( +
+

Export {selectedRecordIds.length} selected record(s)?

+ +
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901', + name: 'bulk-export', + description: 'Export selected records', + component: BulkExport, + command: { + universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902', + label: 'Bulk Export', + availabilityType: 'RECORD_SELECTION', + conditionalAvailabilityExpression: numberOfSelectedRecords > 0, + }, +}); +``` + +## Public assets + +Front components can access files from the app's `public/` directory using `getPublicAssetUrl`: + +```tsx +import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define'; + +const Logo = () => Logo; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'logo', + component: Logo, +}); +``` + +See the [public assets section](/l/zh/developers/extend/apps/config/public-assets) for details. + +## Styling + +Front components support multiple styling approaches. You can use: + +* **Inline styles** — `style={{ color: 'red' }}` +* **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more) +* **Emotion** — CSS-in-JS with `@emotion/react` +* **Styled-components** — `styled.div` patterns +* **Tailwind CSS** — utility classes +* **Any CSS-in-JS library** compatible with React + +```tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { Button, Tag, Status } from 'twenty-sdk/ui'; + +const StyledWidget = () => { + return ( +
+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456', + name: 'styled-widget', + component: StyledWidget, +}); +``` diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/layout/navigation-menu-items.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/layout/navigation-menu-items.mdx new file mode 100644 index 0000000000..f6eaed750f --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/layout/navigation-menu-items.mdx @@ -0,0 +1,44 @@ +--- +title: Navigation Menu Items +description: Add custom entries to the workspace sidebar — links to saved views or external URLs. +icon: bars +--- + +A **navigation menu item** is an entry in the left sidebar. Use `defineNavigationMenuItem()` to ship custom sidebar links — typically one per [view](/l/zh/developers/extend/apps/layout/views) you ship — or to point at external URLs. + +```ts src/navigation-menu-items/example-navigation-menu-item.ts +import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define'; +import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view'; + +export default defineNavigationMenuItem({ + universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c', + name: 'example-navigation-menu-item', + icon: 'IconList', + color: 'blue', + position: 0, + type: NavigationMenuItemType.VIEW, + viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER, +}); +``` + +## Key points + +* `type` determines what the menu item links to. Each type pairs with a specific identifier field: + + | 类型 | 作用 | Required field | + | ------------------------------------ | ------------------------------------ | ----------------------------------------------------------------------------- | + | `NavigationMenuItemType.VIEW` | Opens a saved view | `viewUniversalIdentifier` | + | `NavigationMenuItemType.LINK` | Opens an external URL | `link` | + | `NavigationMenuItemType.FOLDER` | Groups nested items under a label | `name` (and child items reference the folder via `folderUniversalIdentifier`) | + | `NavigationMenuItemType.OBJECT` | Opens an object's default index page | `targetObjectUniversalIdentifier` | + | `NavigationMenuItemType.PAGE_LAYOUT` | Opens a standalone page layout | `pageLayoutUniversalIdentifier` | + +* `position` controls ordering in the sidebar. + +* `icon` and `color` are optional and customize how the entry looks. + +* `folderUniversalIdentifier` is also available on any item to nest it inside a `FOLDER`-type parent. + + +**Common pitfall:** creating an object without an associated view + navigation menu item makes that object invisible to users. Unless it's a technical/internal object, every custom object should have a default view *and* a sidebar entry pointing at it. + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/layout/overview.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/layout/overview.mdx new file mode 100644 index 0000000000..80af81d76b --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/layout/overview.mdx @@ -0,0 +1,56 @@ +--- +title: Overview +description: Place your app inside Twenty's UI — sidebar entries, saved views, record page tabs, and sandboxed React components. +icon: table-columns +--- + +A Twenty app's **layout layer** is everything the user sees: where the app surfaces in the sidebar, which list views it ships, how its record detail pages are arranged, and which custom React components render inside those pages. + +```text + Sidebar Record list Record detail page + ─────── ─────────── ────────────────── + [📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐ + [📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │ + [📋 Inbox ] │ ──────── │ │ [Notes ] │ + ▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab + │ │ Acme │ │ │ adds a tab... + └ defineNavi- │ … │ │ ┌────────────────┐ │ + gationMenu- └────▲─────┘ │ │ │ │ + Item points │ │ │ React UI │◀── …with a + to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent + └ defineView │ │ a Worker) │ │ widget inside + picks columns │ └────────────────┘ │ + and filters └─────────────────────┘ +``` + +## In this section + + + + `defineView` — saved list configurations: visible columns, filters, groups. + + + `defineNavigationMenuItem` — sidebar entries pointing at views or external URLs. + + + `definePageLayout` and `definePageLayoutTab` — tabs and widgets on a record's detail page. + + + `defineFrontComponent` — sandboxed React components that render inside Twenty. + + + `defineCommandMenuItem` — register front components as Cmd+K entries and quick actions. + + + +## Where the app surfaces + +| Surface | What it controls | Entity | +| --------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------- | +| **Sidebar** | A custom entry linking to a saved view or external URL | `defineNavigationMenuItem` | +| **Record list** | A saved configuration for an object — visible columns, order, filters, groups | `defineView` | +| **Record detail page** | The tabs and widgets on a record page (your own object's, or a standard one) | `definePageLayout`, `definePageLayoutTab` | +| **Inside any of the above** | A custom React widget — buttons, forms, dashboards, integrations | `defineFrontComponent` | +| **Command menu (Cmd+K)** | A pinned quick action or hidden command | `defineCommandMenuItem` | + +Front components run inside an isolated Web Worker using Remote DOM — they render *natively* in the page (not inside an iframe), but cannot reach the host page or DOM directly. Communication with Twenty happens through a message-passing host API. diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/layout/page-layouts.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/layout/page-layouts.mdx new file mode 100644 index 0000000000..639dfbaa6d --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/layout/page-layouts.mdx @@ -0,0 +1,102 @@ +--- +title: Page Layouts +description: Customize record detail pages — tabs, widgets, and where front components render — using definePageLayout and definePageLayoutTab. +icon: table-columns +--- + +A **page layout** controls how a record's detail page is arranged: which tabs appear and what widgets they contain. Use `definePageLayout()` to declare a layout for an object you own, or `definePageLayoutTab()` to add a single tab to a layout that already exists (yours or a standard Twenty one). + +| Use case | Entity | +| ---------------------------------------------------------------------- | --------------------- | +| Define the entire layout for a record page on an object you own | `definePageLayout` | +| Add one tab to an existing layout (your own object, or a standard one) | `definePageLayoutTab` | + +## definePageLayout + +Use this when you own the entire detail page — typically for a custom object you defined yourself. + +```ts src/page-layouts/example-record-page-layout.ts +import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +export default definePageLayout({ + universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134', + name: 'Example Record Page', + type: 'RECORD_PAGE', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + tabs: [ + { + universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5', + title: 'Hello World', + position: 50, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], + }, + ], +}); +``` + +### Key points + +* `type` is typically `'RECORD_PAGE'` to customize the detail view of a specific object. +* `objectUniversalIdentifier` specifies which object this layout applies to. +* Each `tab` defines a section of the page with a `title`, `position`, and `layoutMode` (`CANVAS` for free-form layout). +* Each `widget` inside a tab can render a [front component](/l/zh/developers/extend/apps/layout/front-components), a relation list, or other built-in widget types. +* `position` on tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones. + +## definePageLayoutTab + +Use this when you only want to **add** a tab to an existing layout — for example, an analytics tab on the standard Company page, or an AI summary tab attached to your own object's layout. + +```ts src/page-layouts/example-extra-tab.ts +import { + definePageLayoutTab, + PageLayoutTabLayoutMode, +} from 'twenty-sdk/define'; +import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world'; + +const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = + '20202020-ab01-4001-8001-c0aba11c0100'; + +export default definePageLayoutTab({ + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001', + pageLayoutUniversalIdentifier: + COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER, + title: 'Hello World', + position: 1000, + icon: 'IconWorld', + layoutMode: PageLayoutTabLayoutMode.CANVAS, + widgets: [ + { + universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002', + title: 'Hello World', + type: 'FRONT_COMPONENT', + configuration: { + configurationType: 'FRONT_COMPONENT', + frontComponentUniversalIdentifier: + HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, + }, + }, + ], +}); +``` + +### Key points + +* `pageLayoutUniversalIdentifier` is **required** and must point to a page layout that already exists at install time — either a standard Twenty layout or one defined by your own app. Cross-app references to layouts owned by another installed app are not supported today. When the parent layout is missing, installation fails with a clear validation error. +* `widgets` are scoped to this tab only — they reference [front components](/l/zh/developers/extend/apps/layout/front-components), views, etc. exactly like widgets defined inline in `definePageLayout`. +* `position` controls ordering against existing tabs on the targeted layout. Pick a value that places your tab where you want it relative to built-in tabs. +* Use this instead of `definePageLayout` when you only want to add to an existing layout. Use `definePageLayout` when you own the entire layout. diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/layout/views.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/layout/views.mdx new file mode 100644 index 0000000000..da9d6c22f2 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/layout/views.mdx @@ -0,0 +1,43 @@ +--- +title: Views +description: Ship pre-configured saved views — column order, filters, groups — for objects in your app. +icon: list +--- + +A **view** is a saved configuration for how records of an object are displayed: which fields appear, their order, whether they're visible, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app — typically a default index view for each custom object you create. + +```ts src/views/example-view.ts +import { defineView, ViewKey } from 'twenty-sdk/define'; +import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; +import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object'; + +export default defineView({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'All example items', + objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, + icon: 'IconList', + key: ViewKey.INDEX, + position: 0, + fields: [ + { + universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0', + fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER, + position: 0, + isVisible: true, + size: 200, + }, + ], +}); +``` + +## Key points + +* `objectUniversalIdentifier` specifies which object this view applies to. It can be a custom object you defined or a standard Twenty object. +* `key` determines the view type — `ViewKey.INDEX` is the main list view for the object. +* `fields` controls which columns appear and in what order. Each field references a `fieldMetadataUniversalIdentifier`. +* You can also declare `filters`, `filterGroups`, `groups`, and `fieldGroups` for advanced configurations. +* `position` controls ordering when multiple views exist for the same object. + +## How views show up in the UI + +A view by itself isn't reachable from the sidebar. To make it appear there, pair it with a [navigation menu item](/l/zh/developers/extend/apps/layout/navigation-menu-items) of type `VIEW` that points at the view's `universalIdentifier`. That's the canonical pattern: every custom object typically ships a default view + a sidebar entry that opens it. diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/logic/connections.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/logic/connections.mdx new file mode 100644 index 0000000000..1a99c59742 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/logic/connections.mdx @@ -0,0 +1,193 @@ +--- +title: Connections +description: Let your app act on a user's behalf in third-party services via OAuth. +icon: plug +--- + +Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API. + +Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate. + + + + + +A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace. + +A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials. + +```ts src/connection-providers/linear-connection.ts +import { defineConnectionProvider } from 'twenty-sdk/define'; + +export default defineConnectionProvider({ + universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f', + name: 'linear', + displayName: 'Linear', + icon: 'IconBrandLinear', + type: 'oauth', + oauth: { + authorizationEndpoint: 'https://linear.app/oauth/authorize', + tokenEndpoint: 'https://api.linear.app/oauth/token', + scopes: ['read', 'write'], + // These must match keys in `defineApplication.serverVariables` below. + clientIdVariable: 'LINEAR_CLIENT_ID', + clientSecretVariable: 'LINEAR_CLIENT_SECRET', + // Optional: defaults to 'json'. Some providers (Linear, Slack) want + // 'form-urlencoded' for the token request. + tokenRequestContentType: 'form-urlencoded', + // Optional: defaults to true. Disable only if the provider rejects PKCE. + usePkce: false, + // Optional: extra query params on the authorize URL. + // authorizationParams: { prompt: 'consent' }, + // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect. + // revokeEndpoint: 'https://example.com/oauth/revoke', + }, +}); +``` + +```ts src/application.config.ts +import { defineApplication } from 'twenty-sdk/define'; + +export default defineApplication({ + universalIdentifier: '...', + displayName: 'Linear', + description: 'Connect Linear to Twenty.', + defaultRoleUniversalIdentifier: '...', + // OAuth client credentials live on the app registration (one OAuth app per + // Twenty server, configured by the admin) — not per-workspace. Declare them + // as serverVariables so the admin can fill them in once for all installs. + serverVariables: { + LINEAR_CLIENT_ID: { + description: 'OAuth client ID from your Linear OAuth application.', + isSecret: false, + isRequired: true, + }, + LINEAR_CLIENT_SECRET: { + description: 'OAuth client secret from your Linear OAuth application.', + isSecret: true, + isRequired: true, + }, + }, +}); +``` + +Key points: + +* `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`). +* `displayName` shows in the per-app settings tab and in the AI tool list. +* `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo. +* Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server. +* Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled. +* `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`. + +The OAuth callback URL your provider needs to whitelist is: + +``` +https:///apps/oauth/callback +``` + + + + + +Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens. + +```ts src/logic-functions/handlers/create-linear-issue-handler.ts +import { listConnections } from 'twenty-sdk/logic-function'; + +export const createLinearIssueHandler = async (input: { + teamId?: string; + title?: string; +}) => { + if (!input.teamId || !input.title) { + return { success: false, error: 'teamId and title are required' }; + } + + const connections = await listConnections({ providerName: 'linear' }); + + // Workspace-shared credentials win when present; fall back to the first + // user-visibility one. For HTTP-route triggers you typically pick the + // request user's connection via event.userWorkspaceId instead. + const connection = + connections.find((c) => c.visibility === 'workspace') ?? connections[0]; + + if (!connection) { + return { + success: false, + error: + 'Linear is not connected. Open the app settings and click "Add connection".', + }; + } + + // Use connection.accessToken to call the third-party API. + const response = await fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + Authorization: `Bearer ${connection.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`, + }), + }); + + return { success: response.ok }; +}; +``` + +Each connection has: + +| Field | Description | +| ----------------- | -------------------------------------------------------------------------------------------------------- | +| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one | +| `visibility` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) | +| `scopes` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) | +| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers | +| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) | +| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) | +| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect | + +Key points: + +* Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers. +* The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set). +* `getConnection(id)` is the single-row equivalent. + + + + + +When a user clicks "Add connection," they're prompted to pick a visibility: + +* **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not. +* **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user. + +Use the right one for each handler: + +```ts +// HTTP-route trigger — prefer the request user's own connection. +const conn = + connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ?? + connections.find((c) => c.visibility === 'workspace'); + +// Cron trigger — no request user; only shared credentials are sensible. +const conn = connections.find((c) => c.visibility === 'workspace'); +``` + +Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side. + + + + + +For each connection provider, the server admin needs to register an OAuth app at the third party first. + +1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new). +2. Set the **Redirect URI** to `\/apps/oauth/callback`. +3. Copy the generated **Client ID** and **Client Secret**. +4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`. +5. Workspace members can then add connections from the per-app **Connections** section. + + + + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/logic/logic-functions.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/logic/logic-functions.mdx new file mode 100644 index 0000000000..48c15d0d4f --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/logic/logic-functions.mdx @@ -0,0 +1,375 @@ +--- +title: Logic Functions +description: Define server-side TypeScript functions with HTTP, cron, and database event triggers. +icon: bolt +--- + +Logic functions are server-side TypeScript functions that run on the Twenty platform. They can be triggered by HTTP requests, cron schedules, or database events — and can also be exposed as tools for AI agents. + + + + +Each function file uses `defineLogicFunction()` to export a configuration with a handler and optional triggers. + +```ts src/logic-functions/createPostCard.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define'; +import { CoreApiClient, type Person } from 'twenty-client-sdk/core'; + +const handler = async (params: RoutePayload) => { + const client = new CoreApiClient(); + const body = (params.body ?? {}) as { name?: string }; + const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'; + + const result = await client.mutation({ + createPostCard: { + __args: { data: { name } }, + id: true, + name: true, + }, + }); + return result; +}; + +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'create-new-post-card', + timeoutSeconds: 2, + handler, + httpRouteTriggerSettings: { + path: '/post-card/create', + httpMethod: 'POST', + isAuthRequired: true, + }, + /*databaseEventTriggerSettings: { + eventName: 'people.created', + },*/ + /*cronTriggerSettings: { + pattern: '0 0 1 1 *', + },*/ +}); +``` + +Available trigger types: +* **httpRoute**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**: +> e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create` +* **cron**: Runs your function on a schedule using a CRON expression. +* **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function. +> e.g. `person.updated`, `*.created`, `company.*` + + +You can also manually execute a function using the CLI: + +```bash filename="Terminal" +yarn twenty exec -n create-new-post-card -p '{"key": "value"}' +``` + +```bash filename="Terminal" +yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + +You can watch logs with: + +```bash filename="Terminal" +yarn twenty logs +``` + + +#### Route trigger payload + +When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the +[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). +Import the `RoutePayload` type from `twenty-sdk`: + +```ts +import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define'; + +const handler = async (event: RoutePayload) => { + const { headers, queryStringParameters, pathParameters, body } = event; + const { method, path } = event.requestContext.http; + + return { message: 'Success' }; +}; +``` + +The `RoutePayload` type has the following structure: + + | Property | Type | Description | Example | + | ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | + | `headers` | `Record\` | HTTP headers (only those listed in `forwardedRequestHeaders`) | see section below | + | `queryStringParameters` | `Record\` | Query string parameters (multiple values joined with commas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` | + | `pathParameters` | `Record\` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` | + | `body` | `object \| null` | Parsed request body (JSON) | `{ id: 1 }` -> `{ id: 1 }` | + | `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | | + | `isBase64Encoded` | `boolean` | Whether the body is base64 encoded | | + | `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) | | + | `requestContext.http.path` | `string` | Raw request path | | + + +#### forwardedRequestHeaders + +By default, HTTP headers from incoming requests are **not** passed to your logic function for security reasons. +To access specific headers, list them in the `forwardedRequestHeaders` array: + +```ts +export default defineLogicFunction({ + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'webhook-handler', + handler, + httpRouteTriggerSettings: { + path: '/webhook', + httpMethod: 'POST', + isAuthRequired: false, + forwardedRequestHeaders: ['x-webhook-signature', 'content-type'], + }, +}); +``` + +In your handler, access the forwarded headers like this: + +```ts +const handler = async (event: RoutePayload) => { + const signature = event.headers['x-webhook-signature']; + const contentType = event.headers['content-type']; + + // Validate webhook signature... + return { received: true }; +}; +``` + + +Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`). + + +#### Exposing a function as an AI tool or workflow action + +Logic functions can be exposed on two surfaces, each with its own trigger: + +* **`toolTriggerSettings`** — makes the function discoverable by Twenty's AI features (chat, MCP, function calling). Uses standard JSON Schema, the format LLMs natively understand. +* **`workflowActionTriggerSettings`** — makes the function appear as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper field editors, variable pickers, and labels. + +A function can opt into one, the other, or both. They sit alongside `cronTriggerSettings`, `databaseEventTriggerSettings`, and `httpRouteTriggerSettings` — same pattern, same shape. + +```ts src/logic-functions/enrich-company.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const handler = async (params: { companyName: string; domain?: string }) => { + const client = new CoreApiClient(); + + 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, + toolTriggerSettings: {}, +}); +``` + +Key points: + +* A function can mix surfaces — declare both `toolTriggerSettings` and `workflowActionTriggerSettings` to expose it in chat AND in the workflow builder. +* `toolTriggerSettings.inputSchema` and `workflowActionTriggerSettings.inputSchema` are both optional. When omitted, the manifest builder infers them from the handler source code (JSON Schema for the AI tool, Twenty's `InputSchema` for the workflow action). Provide one explicitly when you want richer typing — for example, with `FieldMetadataType`-aware fields like `CURRENCY` or `RELATION` for the workflow builder, or with `description` fields the AI agent can read: + +```ts +export default defineLogicFunction({ + ..., + toolTriggerSettings: { + inputSchema: { + 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'], + }, + }, +}); +``` + + +**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. + + + + + + +**Install hooks** — pre-install and post-install handlers — share this runtime but are declared with their own define functions and don't take trigger settings. See [Install Hooks](/l/zh/developers/extend/apps/config/install-hooks) for `definePreInstallLogicFunction` and `definePostInstallLogicFunction`. + + +## Typed API clients (twenty-client-sdk) + +The `twenty-client-sdk` package provides two typed GraphQL clients for interacting with the Twenty API from your logic functions and front components. + +| Client | Import | Endpoint | Generated? | +| ------------------- | ---------------------------- | ---------------------------------------------- | ---------------------- | +| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — workspace data (records, objects) | Yes, at dev/build time | +| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — workspace config, file uploads | No, ships pre-built | + + + + +`CoreApiClient` is the main client for querying and mutating workspace data. It is **generated from your workspace schema** during `yarn twenty dev` or `yarn twenty build`, so it is fully typed to match your objects and fields. + +```ts +import { CoreApiClient } from 'twenty-client-sdk/core'; + +const client = new CoreApiClient(); + +// Query records +const { companies } = await client.query({ + companies: { + edges: { + node: { + id: true, + name: true, + domainName: { + primaryLinkLabel: true, + primaryLinkUrl: true, + }, + }, + }, + }, +}); + +// Create a record +const { createCompany } = await client.mutation({ + createCompany: { + __args: { + data: { + name: 'Acme Corp', + }, + }, + id: true, + name: true, + }, +}); +``` + +The client uses a selection-set syntax: pass `true` to include a field, use `__args` for arguments, and nest objects for relations. You get full autocompletion and type checking based on your workspace schema. + + +**CoreApiClient is generated at dev/build time.** If you use it without running `yarn twenty dev` or `yarn twenty build` first, it throws an error. The generation happens automatically — the CLI introspects your workspace's GraphQL schema and generates a typed client using `@genql/cli`. + + +#### Using CoreSchema for type annotations + +`CoreSchema` provides TypeScript types matching your workspace objects — useful for typing component state or function parameters: + +```ts +import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core'; +import { useState } from 'react'; + +const [company, setCompany] = useState< + Pick | undefined +>(undefined); + +const client = new CoreApiClient(); +const result = await client.query({ + company: { + __args: { filter: { position: { eq: 1 } } }, + id: true, + name: true, + }, +}); +setCompany(result.company); +``` + + + + +`MetadataApiClient` ships pre-built with the SDK (no generation required). It queries the `/metadata` endpoint for workspace configuration, applications, and file uploads. + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; + +const metadataClient = new MetadataApiClient(); + +// List first 10 objects in the workspace +const { objects } = await metadataClient.query({ + objects: { + edges: { + node: { + id: true, + nameSingular: true, + namePlural: true, + labelSingular: true, + isCustom: true, + }, + }, + __args: { + filter: {}, + paging: { first: 10 }, + }, + }, +}); +``` + +#### Uploading files + +`MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields: + +```ts +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import * as fs from 'fs'; + +const metadataClient = new MetadataApiClient(); + +const fileBuffer = fs.readFileSync('./invoice.pdf'); + +const uploadedFile = await metadataClient.uploadFile( + fileBuffer, // file contents as a Buffer + 'invoice.pdf', // filename + 'application/pdf', // MIME type + '58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier +); + +console.log(uploadedFile); +// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' } +``` + +| Parameter | Type | Description | +| ---------------------------------- | -------- | --------------------------------------------------------------- | +| `fileBuffer` | `Buffer` | The raw file contents | +| `filename` | `string` | The name of the file (used for storage and display) | +| `contentType` | `string` | MIME type (defaults to `application/octet-stream` if omitted) | +| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object | + +Key points: +* Uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed. +* The returned `url` is a signed URL you can use to access the uploaded file. + + + + + + When your code runs on Twenty (logic functions or front components), the platform injects credentials as environment variables: + + * `TWENTY_API_URL` — Base URL of the Twenty API + * `TWENTY_APP_ACCESS_TOKEN` — Short-lived key scoped to your application's default function role + + You do **not** need to pass these to the clients — they read from `process.env` automatically. The API key's permissions are determined by the role referenced in `defaultRoleUniversalIdentifier` in your `application-config.ts`. + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/logic/overview.mdx new file mode 100644 index 0000000000..62bc5d5872 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/logic/overview.mdx @@ -0,0 +1,55 @@ +--- +title: Overview +description: Server-side TypeScript that runs inside Twenty — triggered by HTTP routes, cron schedules, database events, AI tools, or workflow actions. +icon: bolt +--- + +A Twenty app's **logic layer** is the code that *runs* — server-side TypeScript handlers reacting to HTTP requests, cron schedules, and record changes; AI skills and agents that live inside the workspace; and OAuth connections that let your functions act on a user's behalf in third-party services. + +```text + ┌─ HTTP route ──┐ + │ Cron schedule │ + │ Database event │ ┌────────────────────┐ + triggers ─┤ AI tool call ├─────▶│ Logic function │ + │ Workflow action │ │ (your handler) │ + │ Manual exec │ └────────────────────┘ + └────────────────────┘ │ + ▼ + ┌────────────────────────────┐ + │ Twenty API (records) │ + │ Third-party API │ + │ (via Connection token) │ + └────────────────────────────┘ +``` + +## In this section + + + + The core building block — trigger types, payloads, and the typed API client. + + + Reusable AI agent instructions and assistants with custom system prompts. + + + OAuth credentials your app holds for third-party services — Linear, GitHub, Slack, and more. + + + +## Trigger types at a glance + +A logic function picks one or more triggers — every entry below is a separate field on `defineLogicFunction()`: + +| Trigger | When it runs | Setting | +| ------------------- | ---------------------------------------------------------- | ------------------------------- | +| **HTTP route** | A request hits your `/s/\` endpoint | `httpRouteTriggerSettings` | +| **Cron** | A CRON expression matches | `cronTriggerSettings` | +| **Database event** | A workspace record is created, updated, or deleted | `databaseEventTriggerSettings` | +| **AI tool** | A Twenty AI feature decides to call your function | `toolTriggerSettings` | +| **Workflow action** | A workflow step invokes your function | `workflowActionTriggerSettings` | + +Functions run sandboxed in isolated Node.js processes and access the workspace through a typed API client scoped to the role declared on [`defineApplication()`](/l/zh/developers/extend/apps/config/application). + + +**Install-time hooks** — code that runs before or after the install — share this runtime but use their own define functions and live under [Config → Install Hooks](/l/zh/developers/extend/apps/config/install-hooks). + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/logic/skills-and-agents.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/logic/skills-and-agents.mdx new file mode 100644 index 0000000000..82a66ef20f --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/logic/skills-and-agents.mdx @@ -0,0 +1,69 @@ +--- +title: Skills & Agents +description: Define AI skills and agents for your app. +icon: robot +--- + + + Skills and agents are currently in alpha. The feature works but is still evolving. + + +Apps can define AI capabilities that live inside the workspace — reusable skill instructions and agents with custom system prompts. + + + + +Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation: + +```ts src/skills/example-skill.ts +import { defineSkill } from 'twenty-sdk/define'; + +export default defineSkill({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'sales-outreach', + label: 'Sales Outreach', + description: 'Guides the AI agent through a structured sales outreach process', + icon: 'IconBrain', + content: `You are a sales outreach assistant. When reaching out to a prospect: +1. Research the company and recent news +2. Identify the prospect's role and likely pain points +3. Draft a personalized message referencing specific details +4. Keep the tone professional but conversational`, +}); +``` + +Key points: +* `name` is a unique identifier string for the skill (kebab-case recommended). +* `label` is the human-readable display name shown in the UI. +* `content` contains the skill instructions — this is the text the AI agent uses. +* `icon` (optional) sets the icon displayed in the UI. +* `description` (optional) provides additional context about the skill's purpose. + + + + +Agents are AI assistants that live inside your workspace. Use `defineAgent()` to create agents with a custom system prompt: + +```ts src/agents/example-agent.ts +import { defineAgent } from 'twenty-sdk/define'; + +export default defineAgent({ + universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123', + name: 'sales-assistant', + label: 'Sales Assistant', + description: 'Helps the sales team draft outreach emails and research prospects', + icon: 'IconRobot', + prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.', +}); +``` + +Key points: +* `name` is the unique identifier string for the agent (kebab-case recommended). +* `label` is the display name shown in the UI. +* `prompt` is the system prompt that defines the agent's behavior. +* `description` (optional) provides context about what the agent does. +* `icon` (optional) sets the icon displayed in the UI. +* `modelId` (optional) overrides the default AI model used by the agent. + + + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/operations/cli.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/operations/cli.mdx new file mode 100644 index 0000000000..0cccebdbb5 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/operations/cli.mdx @@ -0,0 +1,78 @@ +--- +title: CLI +description: yarn twenty commands for executing functions, streaming logs, managing app installations, and switching remotes. +icon: terminal +--- + +Beyond `dev`, `build`, `add`, and `typecheck`, the `yarn twenty` CLI provides commands for executing functions, viewing logs, and managing app installations. + +## Executing functions (`yarn twenty exec`) + +Run a logic function manually without triggering it via HTTP, cron, or database event: + +```bash filename="Terminal" +# Execute by function name +yarn twenty exec -n create-new-post-card + +# Execute by universalIdentifier +yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf + +# Pass a JSON payload +yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}' + +# Execute the post-install function +yarn twenty exec --postInstall +``` + +## Viewing function logs (`yarn twenty logs`) + +Stream execution logs for your app's logic functions: + +```bash filename="Terminal" +# Stream all function logs +yarn twenty logs + +# Filter by function name +yarn twenty logs -n create-new-post-card + +# Filter by universalIdentifier +yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf +``` + + +This is different from `yarn twenty server logs`, which shows the Docker container logs. `yarn twenty logs` shows your app's function execution logs from the Twenty server. + + +## Uninstalling an app (`yarn twenty uninstall`) + +Remove your app from the active workspace: + +```bash filename="Terminal" +yarn twenty uninstall + +# Skip the confirmation prompt +yarn twenty uninstall --yes +``` + +## Managing remotes + +A **remote** is a Twenty server that your app connects to. During setup, the scaffolder creates one for you automatically. You can add more remotes or switch between them at any time. + +```bash filename="Terminal" +# Add a new remote (opens a browser for OAuth login) +yarn twenty remote add + +# Connect to a local Twenty server (auto-detects port 2020 or 3000) +yarn twenty remote add --local + +# Add a remote non-interactively (useful for CI) +yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote + +# List all configured remotes +yarn twenty remote list + +# Switch the active remote +yarn twenty remote switch +``` + +Your credentials are stored in `~/.twenty/config.json`. diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/operations/overview.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/operations/overview.mdx new file mode 100644 index 0000000000..64d040caa4 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/operations/overview.mdx @@ -0,0 +1,29 @@ +--- +title: Overview +description: Build, test, and ship your app — CLI commands, integration tests, CI, and publishing to a server or to npm. +icon: rocket +--- + +The **operations layer** is everything you do *to* your app rather than *with* it: invoking CLI commands, running integration tests against a real Twenty server, configuring CI, and shipping releases — either as a tarball deployed to a single server or as an npm package listed in the marketplace. + +```text + develop ─▶ test ─▶ build ─▶ deploy / publish + ─────── ──── ───── ───────────────── + yarn yarn yarn yarn twenty deploy (tarball → one server) + twenty test twenty + dev build yarn twenty publish (npm → marketplace) +``` + +## In this section + + + + `yarn twenty` reference — exec, logs, uninstall, remotes. + + + Vitest setup, integration tests, type checking, CI workflow. + + + Build, deploy a tarball, publish to npm, install. + + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/operations/publishing.mdx new file mode 100644 index 0000000000..76105d2deb --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/operations/publishing.mdx @@ -0,0 +1,295 @@ +--- +title: Publishing +icon: upload +description: Distribute your Twenty app to the marketplace or deploy it internally. +--- + +## Overview + +Once your app is [built and tested locally](/l/zh/developers/extend/apps/getting-started/concepts), you have two paths for distributing it: + +* **Deploy a tarball** — upload your app directly to a specific Twenty server for internal or private use. +* **Publish to npm** — list your app in the Twenty marketplace for any workspace to discover and install. + +Both paths start from the same **build** step. + +## Building your app + +Run the build command to compile your app and generate a distribution-ready `manifest.json`: + +```bash filename="Terminal" +yarn twenty build +``` + +This compiles TypeScript sources, transpiles logic functions and front components, and writes everything to `.twenty/output/`. Add `--tarball` to also produce a `.tgz` package for manual distribution or the deploy command. + +## Deploying to a server (tarball) + +For apps you don't want publicly available — proprietary tools, enterprise-only integrations, or experimental builds — you can deploy a tarball directly to a Twenty server. + +### Prerequisites + +Before deploying, you need a configured remote pointing to the target server. Remotes store the server URL and authentication credentials locally in `~/.twenty/config.json`. + +Add a remote: + +```bash filename="Terminal" +yarn twenty remote add --api-url https://your-twenty-server.com --as production +``` + +### Deploying + +Build and upload your app to the server in one step: + +```bash filename="Terminal" +yarn twenty deploy +# To deploy to a specific remote: +# yarn twenty deploy --remote production +``` + +### Sharing a deployed app + + +Sharing private (tarball) apps across workspaces is an **Enterprise** feature. The **Distribution** tab will show an upgrade prompt instead of the share controls until your workspace has a valid Enterprise key. See [Settings > Admin Panel > Enterprise](/settings/admin-panel#enterprise) to activate it. + + +Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. Once your workspace is on the Enterprise plan, you can share a deployed app like this: + +1. Go to **Settings > Applications > Registrations** and open your app +2. In the **Distribution** tab, click **Copy share link** +3. Share this link with users on other workspaces — it takes them directly to the app's install page + +The share link uses the server's base URL (without any workspace subdomain) so it works for any workspace on the server. + +### Version management + +When updating an already deployed tarball app, the server requires the `version` in `package.json` to be **strictly higher** (per [semver](https://semver.org) ordering) than the currently deployed version. Re-deploying the same version, or pushing a lower one, is rejected before the tarball is stored — you'll see a `VERSION_ALREADY_EXISTS` error from the CLI. + +To release an update: + +1. Bump the `version` field in your `package.json` (e.g. `1.2.3` → `1.2.4`, `1.3.0`, or `2.0.0`) +2. Run `yarn twenty deploy` (or `yarn twenty deploy --remote production`) +3. Workspaces that have the app installed will see the upgrade available in their settings + + +Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allowed, and a final release like `1.0.0` is correctly recognized as higher than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver string. + + +{/* TODO: add screenshot of the Upgrade button */} + +### Server version compatibility + +If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`: + +```json filename="package.json" +{ + "name": "twenty-my-app", + "version": "1.0.0", + "engines": { + "node": "^24.5.0", + "twenty": ">=2.3.0" + } +} +``` + +The value is a standard [semver range](https://github.com/npm/node-semver#ranges). Common patterns: + +| Range | Meaning | +| ---------------------------------- | ------------------------------------------ | +| `>=2.3.0` | Any server from 2.3.0 onward | +| `>=2.3.0 \<3.0.0` | 2.3.0 or later, but below the next major | +| `^2.3.0` | Same as `>=2.3.0 \<3.0.0` | + +**What happens at deploy and install time:** + +* If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version. +* If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps). +* If the server has no `APP_VERSION` configured, the check is skipped. + + +The server is the authoritative check — it validates `engines.twenty` on both tarball upload and workspace install. If you deploy a tarball out-of-band or install from the marketplace, the server still enforces compatibility. + + +## Automated CI/CD (scaffolded workflows) + +Apps generated with `create-twenty-app` ship with two GitHub Actions workflows out of the box, under `.github/workflows/`. They are ready to run as soon as you push the repo to GitHub — no extra setup is needed for CI, and CD only requires a single secret. + +### CI — `ci.yml` + +Runs integration tests on every push to `main` and every pull request. + +**What it does:** + +1. Checks out your app's source. +2. Spawns an isolated Twenty test instance using the `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` composite action (the CI equivalent of `yarn twenty server start --test`). +3. Enables Corepack, sets up Node.js from your `.nvmrc`, and installs dependencies with `yarn install --immutable`. +4. Runs `yarn test`, passing `TWENTY_API_URL` and `TWENTY_API_KEY` from the spawned instance so your tests can talk to a real server. + +**Config knobs:** + +* `TWENTY_VERSION` (env, defaults to `latest`) — pin the Twenty server version used in CI by editing this in `ci.yml`. +* Concurrency is grouped by `github.ref` and cancels in-progress runs on new pushes. + +No secrets are required — the test instance is ephemeral and lives only for the duration of the job. + +### CD — `cd.yml` + +Deploys your app to a configured Twenty server on every push to `main`, and optionally from a pull request when the `deploy` label is applied. + +**What it does:** + +1. Checks out the PR head (for labeled PRs) or the pushed commit. +2. Runs `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — the CI equivalent of `yarn twenty deploy`. +3. Runs `twentyhq/twenty/.github/actions/install-twenty-app@main` so the newly deployed version is installed into the target workspace. + +**Required configuration:** + +| Setting | Where | Purpose | +| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `TWENTY_DEPLOY_URL` | `env` in `cd.yml` (defaults to `http://localhost:3000`) | The Twenty server to deploy to. Change this to your real server URL before first use. | +| `TWENTY_DEPLOY_API_KEY` | GitHub repo **Settings → Secrets and variables → Actions** | API key with deploy permission on the target server. | + + +The default `TWENTY_DEPLOY_URL` of `http://localhost:3000` is a placeholder — it will not reach anything from a GitHub-hosted runner. Update it to your server's public URL (or use a self-hosted runner with network access) before enabling CD. + + +**Triggering a preview deploy from a PR:** + +Add the `deploy` label to a pull request. The `if:` guard in `cd.yml` will run the job for that PR using the PR's head commit, letting you validate a change on the target server before merging. + +### Pinning the reusable actions + +Both workflows reference reusable actions at `@main`, so action updates in the `twentyhq/twenty` repo are picked up automatically. If you want deterministic builds, replace `@main` with a commit SHA or release tag on each `uses:` line. + +## Publishing to npm + +Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twenty workspace can browse, install, and upgrade marketplace apps directly from the UI. + +### Requirements + +* An [npm](https://www.npmjs.com) account +* The `twenty-app` keyword in your `package.json` `keywords` array (add it manually — it is not included by default in the `create-twenty-app` template) + +```json filename="package.json" +{ + "name": "twenty-app-postcard-sender", + "version": "1.0.0", + "keywords": ["twenty-app"] +} +``` + +### Marketplace metadata + +The `defineApplication()` config supports optional fields that control how your app appears in the marketplace. Use `logoUrl` and `screenshots` to reference images from the `public/` folder: + +```ts src/application-config.ts +export default defineApplication({ + universalIdentifier: '...', + displayName: 'My App', + description: 'A great app', + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + logoUrl: 'public/logo.png', + screenshots: [ + 'public/screenshot-1.png', + 'public/screenshot-2.png', + ], +}); +``` + +See the [defineApplication accordion](/l/zh/developers/extend/apps/config/application#marketplace-metadata) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.). + +#### Recommended screenshot dimensions + +The marketplace renders `screenshots` in a fixed `8:5` container (for example, `1600×1000 px`). + + +Screenshots of any aspect ratio are displayed in full and are never cropped, but anything significantly taller or narrower than `8:5` will show empty bands on the sides. + + +### Publish + +```bash filename="Terminal" +yarn twenty publish +``` + +To publish under a specific dist-tag (e.g., `beta` or `next`): + +```bash filename="Terminal" +yarn twenty publish --tag beta +``` + +### How marketplace discovery works + +The Twenty server syncs its marketplace catalog from the npm registry **every hour**. + +You can trigger the sync immediately instead of waiting: + +```bash filename="Terminal" +yarn twenty server catalog-sync +# To target a specific remote: +# yarn twenty server catalog-sync --remote production +``` + +The metadata shown in the marketplace comes from your `defineApplication()` config — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`. + + +If your app does not define an `aboutDescription` in `defineApplication()`, the marketplace will automatically use your package's `README.md` from npm as the about page content. This means you can maintain a single README for both npm and the Twenty marketplace. If you want a different description in the marketplace, explicitly set `aboutDescription`. + + +### CI publishing + +Use this GitHub Actions workflow to publish automatically on every release (uses [OIDC](https://docs.npmjs.com/trusted-publishers)): + +```yaml filename=".github/workflows/publish.yml" +name: Publish +on: + release: + types: [published] + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: https://registry.npmjs.org + - run: yarn install --immutable + - run: npx twenty build + - run: npm publish --provenance --access public + working-directory: .twenty/output +``` + +For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `yarn twenty build`, then `npm publish` from `.twenty/output`. + + +**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions. + + +## Installing apps + +Once an app is published (npm) or deployed (tarball), workspaces can install it through the UI. + +Go to the **Settings > Applications** page in Twenty, where both marketplace and tarball-deployed apps can be browsed and installed. + +{/* TODO: add screenshot of the UI when the app is registered */} + +You can also install apps from the command line: + +```bash filename="Terminal" +yarn twenty install +``` + + +The server enforces semver versioning on install, mirroring the rules on deploy: + +* Installing the same version that is already installed in your workspace is rejected with an `APP_ALREADY_INSTALLED` error. +* Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error. + +To install a newer version, deploy or publish it first, then re-run `yarn twenty install`. + diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/operations/testing.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/operations/testing.mdx new file mode 100644 index 0000000000..b1959be0ed --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/operations/testing.mdx @@ -0,0 +1,301 @@ +--- +title: Testing +description: Vitest setup, integration tests against a real Twenty server, type checking, and CI with GitHub Actions. +icon: flask +--- + +The SDK provides programmatic APIs that let you build, deploy, install, and uninstall your app from test code. Combined with [Vitest](https://vitest.dev/) and the typed API clients, you can write integration tests that verify your app works end-to-end against a real Twenty server. + +## Using npm packages + +You can install and use any npm package in your app. Both logic functions and front components are bundled with [esbuild](https://esbuild.github.io/), which inlines all dependencies into the output — no `node_modules` are needed at runtime. + +### Installing a package + +```bash filename="Terminal" +yarn add axios +``` + +Then import it in your code: + +```ts src/logic-functions/fetch-data.ts +import { defineLogicFunction } from 'twenty-sdk/define'; +import axios from 'axios'; + +const handler = async (): Promise => { + const { data } = await axios.get('https://api.example.com/data'); + + return { data }; +}; + +export default defineLogicFunction({ + universalIdentifier: '...', + name: 'fetch-data', + description: 'Fetches data from an external API', + timeoutSeconds: 10, + handler, +}); +``` + +The same works for front components: + +```tsx src/front-components/chart.tsx +import { defineFrontComponent } from 'twenty-sdk/define'; +import { format } from 'date-fns'; + +const DateWidget = () => { + return

Today is {format(new Date(), 'MMMM do, yyyy')}

; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### How bundling works + +The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle. + +**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed. + +**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment. + +Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server. + +## Setup + +The scaffolded app already includes Vitest. If you set it up manually, install the dependencies: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Create a `vitest.config.ts` at the root of your app: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Create a setup file that verifies the server is reachable before tests run: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## Programmatic SDK APIs + +The `twenty-sdk/cli` subpath exports functions you can call directly from test code: + +| Function | Description | +| -------------- | ------------------------------------------- | +| `appBuild` | Build the app and optionally pack a tarball | +| `appDeploy` | Upload a tarball to the server | +| `appInstall` | Install the app on the active workspace | +| `appUninstall` | Uninstall the app from the active workspace | + +Each function returns a result object with `success: boolean` and either `data` or `error`. + +## Writing an integration test + +Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Running tests + +Make sure your local Twenty server is running, then: + +```bash filename="Terminal" +yarn test +``` + +Or in watch mode during development: + +```bash filename="Terminal" +yarn test:watch +``` + +## Type checking + +You can also run type checking on your app without running tests: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +This runs `tsc --noEmit` and reports any type errors. + +## CI with GitHub Actions + +The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests. + +The workflow: + +1. Checks out your code +2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action +3. Installs dependencies with `yarn install --immutable` +4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub. + +To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow.