diff --git a/packages/create-twenty-app/README.md b/packages/create-twenty-app/README.md index 85660d03c6..0bf3117cc3 100644 --- a/packages/create-twenty-app/README.md +++ b/packages/create-twenty-app/README.md @@ -48,11 +48,11 @@ Examples are sourced from [twentyhq/twenty/packages/twenty-apps/examples](https: ## Documentation -Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started)**: +Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start)**: -- [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started) — step-by-step setup, project structure, server management, CI -- [Building Apps](https://docs.twenty.com/developers/extend/apps/building) — entity definitions, API clients, testing -- [Publishing](https://docs.twenty.com/developers/extend/apps/publishing) — deploy, npm publish, marketplace +- [Quick Start](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start) — scaffold, run a local server, sync your code +- [Concepts](https://docs.twenty.com/developers/extend/apps/getting-started/concepts) — how apps work: entity model, sandboxing, lifecycle +- [Operations](https://docs.twenty.com/developers/extend/apps/operations/overview) — CLI, testing, CI, deploy and publish ## Troubleshooting diff --git a/packages/create-twenty-app/src/constants/template/AGENT.md b/packages/create-twenty-app/src/constants/template/AGENT.md deleted file mode 100644 index b10256427b..0000000000 --- a/packages/create-twenty-app/src/constants/template/AGENT.md +++ /dev/null @@ -1,14 +0,0 @@ -## Base documentation - -- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started -- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard - -## UUID requirement - -- All generated UUIDs must be valid UUID v4. - -## Common Pitfalls - -- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it. -- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar. -- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab. diff --git a/packages/create-twenty-app/src/constants/template/AGENTS.md b/packages/create-twenty-app/src/constants/template/AGENTS.md new file mode 100644 index 0000000000..d4865d21a6 --- /dev/null +++ b/packages/create-twenty-app/src/constants/template/AGENTS.md @@ -0,0 +1,67 @@ +## Base documentation + +- Getting started: + - https://docs.twenty.com/developers/extend/apps/getting-started/quick-start.md + - https://docs.twenty.com/developers/extend/apps/getting-started/concepts.md + - https://docs.twenty.com/developers/extend/apps/getting-started/project-structure.md + - https://docs.twenty.com/developers/extend/apps/getting-started/local-server.md + - https://docs.twenty.com/developers/extend/apps/getting-started/scaffolding.md + - https://docs.twenty.com/developers/extend/apps/getting-started/troubleshooting.md +- Config: + - https://docs.twenty.com/developers/extend/apps/config/overview.md + - https://docs.twenty.com/developers/extend/apps/config/application.md + - https://docs.twenty.com/developers/extend/apps/config/roles.md + - https://docs.twenty.com/developers/extend/apps/config/install-hooks.md + - https://docs.twenty.com/developers/extend/apps/config/public-assets.md +- Data: + - https://docs.twenty.com/developers/extend/apps/data/overview.md + - https://docs.twenty.com/developers/extend/apps/data/objects.md + - https://docs.twenty.com/developers/extend/apps/data/extending-objects.md + - https://docs.twenty.com/developers/extend/apps/data/relations.md +- Logic: + - https://docs.twenty.com/developers/extend/apps/logic/overview.md + - https://docs.twenty.com/developers/extend/apps/logic/logic-functions.md + - https://docs.twenty.com/developers/extend/apps/logic/skills-and-agents.md + - https://docs.twenty.com/developers/extend/apps/logic/connections.md +- Layout: + - https://docs.twenty.com/developers/extend/apps/layout/overview.md + - https://docs.twenty.com/developers/extend/apps/layout/views.md + - https://docs.twenty.com/developers/extend/apps/layout/navigation-menu-items.md + - https://docs.twenty.com/developers/extend/apps/layout/page-layouts.md + - https://docs.twenty.com/developers/extend/apps/layout/front-components.md + - https://docs.twenty.com/developers/extend/apps/layout/command-menu-items.md +- Operations: + - https://docs.twenty.com/developers/extend/apps/operations/overview.md + - https://docs.twenty.com/developers/extend/apps/operations/cli.md + - https://docs.twenty.com/developers/extend/apps/operations/testing.md + - https://docs.twenty.com/developers/extend/apps/operations/publishing.md +- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard + +## UUID requirement + +- All generated UUIDs must be valid UUID v4. + +## Common Pitfalls + +- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it. +- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar. +- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab. + +## Best practice + +It's highly recommended to create new app entities using `yarn twenty add`. These are the options: + +| 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` | + +This helps automatically generate required IDs etc. diff --git a/packages/create-twenty-app/src/constants/template/CLAUDE.md b/packages/create-twenty-app/src/constants/template/CLAUDE.md deleted file mode 100644 index b10256427b..0000000000 --- a/packages/create-twenty-app/src/constants/template/CLAUDE.md +++ /dev/null @@ -1,14 +0,0 @@ -## Base documentation - -- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started -- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard - -## UUID requirement - -- All generated UUIDs must be valid UUID v4. - -## Common Pitfalls - -- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it. -- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar. -- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab. diff --git a/packages/create-twenty-app/src/constants/template/LLMS.md b/packages/create-twenty-app/src/constants/template/LLMS.md deleted file mode 100644 index b10256427b..0000000000 --- a/packages/create-twenty-app/src/constants/template/LLMS.md +++ /dev/null @@ -1,14 +0,0 @@ -## Base documentation - -- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started -- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard - -## UUID requirement - -- All generated UUIDs must be valid UUID v4. - -## Common Pitfalls - -- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it. -- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar. -- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab. diff --git a/packages/create-twenty-app/src/constants/template/README.md b/packages/create-twenty-app/src/constants/template/README.md index 16b8e5ac96..70b075e526 100644 --- a/packages/create-twenty-app/src/constants/template/README.md +++ b/packages/create-twenty-app/src/constants/template/README.md @@ -6,6 +6,6 @@ Run `yarn twenty help` to list all available commands. ## Learn More -- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started) +- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start) - [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk) - [Discord](https://discord.gg/cx5n4Jzs57) diff --git a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts index f3fe529616..b7de51bfd1 100644 --- a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts +++ b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts @@ -76,11 +76,16 @@ describe('copyBaseApplicationProject', () => { appDirectory: testAppDirectory, }); - expect(fs.copy).toHaveBeenCalledTimes(1); + // Two fs.copy calls: (1) the template directory, (2) AGENTS.md → CLAUDE.md + expect(fs.copy).toHaveBeenCalledTimes(2); expect(fs.copy).toHaveBeenCalledWith( expect.stringContaining('template'), testAppDirectory, ); + expect(fs.copy).toHaveBeenCalledWith( + join(testAppDirectory, 'AGENTS.md'), + join(testAppDirectory, 'CLAUDE.md'), + ); }); it('should replace placeholders in universal-identifiers.ts with real values', async () => { diff --git a/packages/create-twenty-app/src/utils/app-template.ts b/packages/create-twenty-app/src/utils/app-template.ts index 9771ea9f26..fe7ba74d1d 100644 --- a/packages/create-twenty-app/src/utils/app-template.ts +++ b/packages/create-twenty-app/src/utils/app-template.ts @@ -23,6 +23,8 @@ export const copyBaseApplicationProject = async ({ await renameDotfiles({ appDirectory }); + await mirrorAgentsToClaude({ appDirectory }); + await addEmptyPublicDirectory({ appDirectory }); await generateUniversalIdentifiers({ @@ -51,6 +53,19 @@ const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => { } }; +// AGENTS.md is the cross-tool standard; Claude Code prefers CLAUDE.md and only +// falls back to AGENTS.md, so we mirror the file to keep a single source of truth. +const mirrorAgentsToClaude = async ({ + appDirectory, +}: { + appDirectory: string; +}) => { + await fs.copy( + join(appDirectory, 'AGENTS.md'), + join(appDirectory, 'CLAUDE.md'), + ); +}; + const addEmptyPublicDirectory = async ({ appDirectory, }: { diff --git a/packages/twenty-docs/developers/extend/apps/config/application.mdx b/packages/twenty-docs/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..2ae98f34cf --- /dev/null +++ b/packages/twenty-docs/developers/extend/apps/config/application.mdx @@ -0,0 +1,64 @@ +--- +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](/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()`](/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](/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/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/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..1763d6bcb5 --- /dev/null +++ b/packages/twenty-docs/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](/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()`](/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/developers/extend/apps/config/overview.mdx b/packages/twenty-docs/developers/extend/apps/config/overview.mdx new file mode 100644 index 0000000000..4d642b2af4 --- /dev/null +++ b/packages/twenty-docs/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](/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/developers/extend/apps/config/public-assets.mdx b/packages/twenty-docs/developers/extend/apps/config/public-assets.mdx new file mode 100644 index 0000000000..f6e8db92c2 --- /dev/null +++ b/packages/twenty-docs/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/developers/extend/apps/config/roles.mdx b/packages/twenty-docs/developers/extend/apps/config/roles.mdx new file mode 100644 index 0000000000..bae6d40be4 --- /dev/null +++ b/packages/twenty-docs/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`](/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/developers/extend/apps/data-model.mdx b/packages/twenty-docs/developers/extend/apps/data-model.mdx deleted file mode 100644 index c573440493..0000000000 --- a/packages/twenty-docs/developers/extend/apps/data-model.mdx +++ /dev/null @@ -1,493 +0,0 @@ ---- -title: Data Model -description: Define objects, fields, roles, and application metadata with the Twenty SDK. -icon: "database" ---- - -The `twenty-sdk` package provides `defineEntity` functions to declare your app's data model. You must use `export default defineEntity({...})` for the SDK to detect your entities. These functions validate your configuration at build time and provide IDE autocompletion and type safety. - - - **File organization is up to you.** - Entity detection is AST-based — the SDK finds `export default defineEntity(...)` calls regardless of where the file lives. Grouping files by type (e.g., `logic-functions/`, `roles/`) is just a convention, not a requirement. - - - - - -Roles encapsulate permissions on your workspace's objects and actions. - -```ts 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], -}); -``` - - - - -Every app must have exactly one `defineApplication` call that describes: - -- **Identity**: identifiers, display name, and description. -- **Permissions**: which role its functions and front components use. -- **(Optional) Variables**: key–value pairs exposed to your functions as environment variables. -- **(Optional) Pre-install / post-install functions**: logic functions that run before or after installation. - -```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()` (see above). -- Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. - -#### Marketplace metadata - -If you plan to [publish your app](/developers/extend/apps/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 | - -#### Roles and permissions - -The `defaultRoleUniversalIdentifier` in `application-config.ts` designates the default role used by your app's logic functions and front components. See `defineRole` above for details. - -- The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. -- The typed client is restricted to the permissions granted to that role. -- Follow least-privilege: create a dedicated role with only the permissions your functions need. - -##### 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 in `application-config.ts` as `defaultRoleUniversalIdentifier`: - -- **\*.role.ts** defines what the role can do. -- **application-config.ts** points to that role so your functions inherit its permissions. - -Notes: -- Start from the scaffolded role, then progressively restrict it following least-privilege. -- Replace `objectPermissions` and `fieldPermissions` with the 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). - - - - -Custom objects describe both schema and behavior for records in your workspace. Use `defineObject()` to define objects with built-in validation: - -```ts postCard.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: - -- Use `defineObject()` for built-in validation and better IDE support. -- The `universalIdentifier` must be unique and stable across deployments. -- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`. -- The `fields` array is optional — you can define objects without custom fields. -- You can scaffold new objects using `yarn twenty add`, which guides you through naming, fields, and relationships. - - -**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields - such as `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` and `deletedAt`. - You don't need to define these in your `fields` array — only add your custom fields. - You can override default fields by defining a field with the same name in your `fields` array, - but this is not recommended. - - - - - -Use `defineField()` to add fields to objects you don't own — such as standard Twenty objects (Person, Company, etc.) or objects from other apps. Unlike inline fields in `defineObject()`, 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 objects, use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` exported from `twenty-sdk`. -- When defining fields inline in `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()`. - - - - -Relations connect objects together. In Twenty, relations are always **bidirectional** — you define both sides, and each side references the other. - -There are two relation types: - -| Relation type | Description | Has foreign key? | -|---------------|-------------|-----------------| -| `MANY_TO_ONE` | Many records of this object point to one record of the target | Yes (`joinColumnName`) | -| `ONE_TO_MANY` | One record of this object has many records of the target | No (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 - -Suppose 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 file. 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 in defineObject - -You can also define relation fields directly inside `defineObject()`. In that case, 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 - ], -}); -``` - - - -## Scaffolding entities with `yarn twenty add` - -Instead of creating entity files by hand, you can use the interactive scaffolder: - -```bash filename="Terminal" -yarn twenty add -``` - -This prompts you to pick an entity type and walks you through the required fields. It generates 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/developers/extend/apps/data/extending-objects.mdx b/packages/twenty-docs/developers/extend/apps/data/extending-objects.mdx new file mode 100644 index 0000000000..9e58396cbd --- /dev/null +++ b/packages/twenty-docs/developers/extend/apps/data/extending-objects.mdx @@ -0,0 +1,46 @@ +--- +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`](/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](/developers/extend/apps/data/relations) for the bidirectional pattern. diff --git a/packages/twenty-docs/developers/extend/apps/data/objects.mdx b/packages/twenty-docs/developers/extend/apps/data/objects.mdx new file mode 100644 index 0000000000..ab24c3744f --- /dev/null +++ b/packages/twenty-docs/developers/extend/apps/data/objects.mdx @@ -0,0 +1,93 @@ +--- +title: Objects +description: Declare new record types — custom tables with their own fields — using defineObject. +icon: "table" +--- + +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 + +- The `universalIdentifier` must be unique and stable across deployments. +- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`. +- The `fields` array is optional — you can define objects without custom fields. +- Inline fields defined here do **not** need an `objectUniversalIdentifier` — it's inherited from the parent object. Use [`defineField()`](/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](/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](/developers/extend/apps/data/relations) for the bidirectional relation pattern. +- **Add fields to objects from other apps** — see [Extending Objects](/developers/extend/apps/data/extending-objects) for `defineField()`. +- **Display this object in the UI** — see [Views](/developers/extend/apps/layout/views) and [Navigation Menu Items](/developers/extend/apps/layout/navigation-menu-items) to put it in the sidebar. diff --git a/packages/twenty-docs/developers/extend/apps/data/overview.mdx b/packages/twenty-docs/developers/extend/apps/data/overview.mdx new file mode 100644 index 0000000000..775657f1a2 --- /dev/null +++ b/packages/twenty-docs/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](/developers/extend/apps/config/overview). Looking for **Connections** (Linear, GitHub, Slack OAuth)? Those exist to be called *from* logic functions and live under [Logic](/developers/extend/apps/logic/connections). + diff --git a/packages/twenty-docs/developers/extend/apps/data/relations.mdx b/packages/twenty-docs/developers/extend/apps/data/relations.mdx new file mode 100644 index 0000000000..cbe004e9e0 --- /dev/null +++ b/packages/twenty-docs/developers/extend/apps/data/relations.mdx @@ -0,0 +1,160 @@ +--- +title: Relations +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. + +| Relation type | Description | Has foreign key? | +|---------------|-------------|------------------| +| `MANY_TO_ONE` | Many records of this object point to one record of the target | Yes (`joinColumnName`) | +| `ONE_TO_MANY` | One record of this object has many records of the target | 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`](/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/developers/extend/apps/building.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/concepts.mdx similarity index 63% rename from packages/twenty-docs/developers/extend/apps/building.mdx rename to packages/twenty-docs/developers/extend/apps/getting-started/concepts.mdx index 9de42c30a6..26e3367572 100644 --- a/packages/twenty-docs/developers/extend/apps/building.mdx +++ b/packages/twenty-docs/developers/extend/apps/getting-started/concepts.mdx @@ -1,6 +1,6 @@ --- -title: Architecture -description: How Twenty apps work — sandboxing, lifecycle, and the building blocks. +title: Concepts +description: How Twenty apps work — entity model, sandboxing, and the install lifecycle. icon: "sitemap" --- @@ -8,7 +8,7 @@ Twenty apps are TypeScript packages that extend your workspace with custom objec ## 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. +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/ @@ -36,17 +36,20 @@ your-app/ | Entity | Purpose | Docs | |--------|---------|------| -| **Application** | App identity, permissions, variables | [Data Model](/developers/extend/apps/data-model) | -| **Role** | Permission sets for objects and fields | [Data Model](/developers/extend/apps/data-model) | -| **Object** | Custom data tables with fields | [Data Model](/developers/extend/apps/data-model) | -| **Field** | Extend existing objects, define relations | [Data Model](/developers/extend/apps/data-model) | -| **Logic Function** | Server-side TypeScript with triggers | [Logic Functions](/developers/extend/apps/logic-functions) | -| **Front Component** | Sandboxed React UI in Twenty's page | [Front Components](/developers/extend/apps/front-components) | -| **Skill** | Reusable AI agent instructions | [Skills & Agents](/developers/extend/apps/skills-and-agents) | -| **Agent** | AI assistants with custom prompts | [Skills & Agents](/developers/extend/apps/skills-and-agents) | -| **View** | Pre-configured record list views | [Layout](/developers/extend/apps/layout) | -| **Navigation Menu Item** | Custom sidebar entries | [Layout](/developers/extend/apps/layout) | -| **Page Layout** | Custom record page tabs and widgets | [Layout](/developers/extend/apps/layout) | +| **Application** | App identity, default role, variables | [Application Config](/developers/extend/apps/config/application) | +| **Role** | Permission sets on objects and fields | [Roles & Permissions](/developers/extend/apps/config/roles) | +| **Object** | Custom record types with fields | [Objects](/developers/extend/apps/data/objects) | +| **Field** | Add fields to objects from other apps | [Extending Objects](/developers/extend/apps/data/extending-objects) | +| **Relation** | Bidirectional links between objects | [Relations](/developers/extend/apps/data/relations) | +| **Logic Function** | Server-side TypeScript with triggers | [Logic Functions](/developers/extend/apps/logic/logic-functions) | +| **Skill** | Reusable AI agent instructions | [Skills & Agents](/developers/extend/apps/logic/skills-and-agents) | +| **Agent** | AI assistants with custom prompts | [Skills & Agents](/developers/extend/apps/logic/skills-and-agents) | +| **Connection Provider** | OAuth credentials for third-party APIs | [Connections](/developers/extend/apps/logic/connections) | +| **View** | Pre-configured record list views | [Views](/developers/extend/apps/layout/views) | +| **Navigation Menu Item** | Custom sidebar entries | [Navigation Menu Items](/developers/extend/apps/layout/navigation-menu-items) | +| **Page Layout** | Tabs and widgets on a record's detail page | [Page Layouts](/developers/extend/apps/layout/page-layouts) | +| **Front Component** | Sandboxed React UI inside Twenty | [Front Components](/developers/extend/apps/layout/front-components) | +| **Command Menu Item** | Quick actions and Cmd+K entries | [Command Menu Items](/developers/extend/apps/layout/command-menu-items) | ## Sandboxing @@ -75,30 +78,24 @@ your-app/ - **`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 logic functions that run during installation. See [Logic Functions](/developers/extend/apps/logic-functions) for details. +- **Pre/post-install hooks** — optional functions that run during installation. See [Install Hooks](/developers/extend/apps/config/install-hooks) for details. ## Next steps - - Define objects, fields, roles, and relations. + + Application identity, default role, and install hooks. - - Server-side functions with HTTP, cron, and event triggers. + + Objects, fields, and bidirectional relations. - - Sandboxed React components inside Twenty's UI. + + Logic functions, skills, agents, and OAuth connections. - - Views, navigation items, and record page layouts. + + Views, navigation, page layouts, front components. - - AI skills and agents with custom prompts. - - - CLI commands, testing, assets, remotes, and CI. - - - Deploy to a server or publish to the marketplace. + + CLI, testing, remotes, CI, and publishing your app. diff --git a/packages/twenty-docs/developers/extend/apps/getting-started/local-server.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/local-server.mdx new file mode 100644 index 0000000000..d3c5be7bfb --- /dev/null +++ b/packages/twenty-docs/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/developers/extend/apps/getting-started/project-structure.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx new file mode 100644 index 0000000000..0638365d70 --- /dev/null +++ b/packages/twenty-docs/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/developers/extend/apps/getting-started.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/quick-start.mdx similarity index 59% rename from packages/twenty-docs/developers/extend/apps/getting-started.mdx rename to packages/twenty-docs/developers/extend/apps/getting-started/quick-start.mdx index da3bd02432..312b1ce889 100644 --- a/packages/twenty-docs/developers/extend/apps/getting-started.mdx +++ b/packages/twenty-docs/developers/extend/apps/getting-started/quick-start.mdx @@ -1,5 +1,5 @@ --- -title: Getting Started +title: Quick Start icon: "rocket" description: Create your first Twenty app in minutes. --- @@ -131,11 +131,23 @@ yarn twenty dev --once 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 Apps](/developers/extend/apps/publishing). +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](/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](/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`: @@ -149,125 +161,24 @@ Apps are composed of **entities** — each defined as a TypeScript file with a s | **Views & Navigation** | Pre-configured list views and sidebar menu items | | **Page layouts** | Custom record detail pages with tabs and widgets | -Full reference: [Building Apps](/developers/extend/apps/building). +Full reference: [Concepts](/developers/extend/apps/getting-started/concepts). -## Project structure +## Next steps -```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 -``` - -| 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. | - -### 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 [Building Apps](/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add). - ---- - -## 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. - - ---- - -## Troubleshooting - -- **Docker errors** — Make sure Docker Desktop (or the daemon) is running before `yarn twenty server start`. The error message will show the right start command for your OS. -- **Wrong Node version** — Need 24+. Check with `node -v`. -- **Yarn 4 missing** — Run `corepack enable`. -- **Dependencies broken** — `rm -rf node_modules && yarn install`. - -Stuck? Ask on the [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322). + + + 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/developers/extend/apps/getting-started/scaffolding.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/scaffolding.mdx new file mode 100644 index 0000000000..de505b98ac --- /dev/null +++ b/packages/twenty-docs/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/developers/extend/apps/getting-started/troubleshooting.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/troubleshooting.mdx new file mode 100644 index 0000000000..73bc5a8500 --- /dev/null +++ b/packages/twenty-docs/developers/extend/apps/getting-started/troubleshooting.mdx @@ -0,0 +1,12 @@ +--- +title: Troubleshooting +description: Common first-run issues — Docker, Node version, Yarn, dependencies. +icon: "wrench" +--- + +- **Docker errors** — Make sure Docker Desktop (or the daemon) is running before `yarn twenty server start`. The error message will show the right start command for your OS. +- **Wrong Node version** — Need 24+. Check with `node -v`. +- **Yarn 4 missing** — Run `corepack enable`. +- **Dependencies broken** — `rm -rf node_modules && yarn install`. + +Stuck? Ask on the [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322). diff --git a/packages/twenty-docs/developers/extend/apps/layout.mdx b/packages/twenty-docs/developers/extend/apps/layout.mdx deleted file mode 100644 index 90e1098e86..0000000000 --- a/packages/twenty-docs/developers/extend/apps/layout.mdx +++ /dev/null @@ -1,178 +0,0 @@ ---- -title: Layout -description: Define views, navigation menu items, and page layouts to shape how your app appears in Twenty. -icon: "table-columns" ---- - -Layout entities control how your app surfaces inside Twenty's UI — what lives in the sidebar, which saved views ship with the app, and how a record detail page is arranged. - -## Layout concepts - -| Concept | What it controls | Entity | -|---------|------------------|--------| -| **View** | A saved list configuration for an object — visible fields, order, filters, groups | `defineView` | -| **Navigation Menu Item** | An entry in the left sidebar that links to a view or an external URL | `defineNavigationMenuItem` | -| **Page Layout** | The tabs and widgets that make up a record's detail page | `definePageLayout` | -| **Page Layout Tab** | A standalone tab attached to an existing page layout (standard or your own app's) | `definePageLayoutTab` | - -Views, navigation items, and page layouts reference each other by `universalIdentifier`: - -- A **navigation menu item** of type `VIEW` points at a `defineView` identifier, so the sidebar link opens that saved view. -- A **page layout** of type `RECORD_PAGE` targets an object and can embed [front components](/developers/extend/apps/front-components) inside its tabs as widgets. - - - - -Views are saved configurations for how records of an object are displayed — including which fields are visible, their order, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app: - -```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. -- `key` determines the view type (e.g., `ViewKey.INDEX` for the main list view). -- `fields` controls which columns appear and their order. Each field references a `fieldMetadataUniversalIdentifier`. -- You can also define `filters`, `filterGroups`, `groups`, and `fieldGroups` for more advanced configurations. -- `position` controls the ordering when multiple views exist for the same object. - - - - -Navigation menu items add custom entries to the workspace sidebar. Use `defineNavigationMenuItem()` to link to views, external URLs, or objects: - -```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: `NavigationMenuItemType.VIEW` for a saved view, or `NavigationMenuItemType.LINK` for an external URL. -- For view links, set `viewUniversalIdentifier`. For external links, set `link`. -- `position` controls the ordering in the sidebar. -- `icon` and `color` (optional) customize the appearance. - - - - -Page layouts let you customize how a record detail page looks — which tabs appear, what widgets are inside each tab, and how they are arranged. Use `definePageLayout()` to ship custom layouts with your app: - -```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, 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` lets your app attach a single tab — with optional widgets — to an **existing** page layout. The most common use case is adding a custom tab (for example, an analytics or AI summary tab) to one of Twenty's built-in record pages, or to a page layout your own app already ships. - -The targeted page layout must be either a **standard** Twenty page layout or one defined by **your own app**; cross-app references to page layouts owned by another installed app are not supported today. - -```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** when using `definePageLayoutTab` and must point to a page layout that already exists at install time (standard or your app's). When the parent page layout is missing, installation fails with a clear validation error. -- `widgets` are scoped to this tab only — they reference 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 (typically a `RECORD_PAGE` for an object you ship in your app, or a `STANDALONE_PAGE`). - - - diff --git a/packages/twenty-docs/developers/extend/apps/layout/command-menu-items.mdx b/packages/twenty-docs/developers/extend/apps/layout/command-menu-items.mdx new file mode 100644 index 0000000000..91f258cb2f --- /dev/null +++ b/packages/twenty-docs/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](/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](/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](/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/developers/extend/apps/front-components.mdx b/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx similarity index 70% rename from packages/twenty-docs/developers/extend/apps/front-components.mdx rename to packages/twenty-docs/developers/extend/apps/layout/front-components.mdx index 464f702398..1c7ab6fb9a 100644 --- a/packages/twenty-docs/developers/extend/apps/front-components.mdx +++ b/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx @@ -11,11 +11,16 @@ Front components are React components that render directly inside Twenty's UI. T 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. When configuring a dashboard or a record page layout, users can add a front component widget. +- **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside [page layouts](/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](/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](/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 register it with a **command menu item**. Use `defineCommandMenuItem` in a separate file to make the component appear as a quick-action button in the top-right corner of the page: +The quickest way to see a front component in action is to pair it with a [`defineCommandMenuItem`](/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'; @@ -71,7 +76,7 @@ Click it to render the component inline. ## 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 the [definePageLayout](/developers/extend/apps/skills-and-agents#definepagelayout) section for details. +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](/developers/extend/apps/layout/page-layouts) for details. ## Headless vs non-headless @@ -348,96 +353,6 @@ export default defineFrontComponent({ }); ``` -## defineCommandMenuItem - -Use `defineCommandMenuItem` to register a front component in the command menu (Cmd+K). If `isPinned` is `true`, it also appears as a 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', -}); -``` - -| 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 to dynamically control whether the command is visible (see below) | - -## 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 | - ## Public assets Front components can access files from the app's `public/` directory using `getPublicAssetUrl`: @@ -454,7 +369,7 @@ export default defineFrontComponent({ }); ``` -See the [public assets section](/developers/extend/apps/cli-and-testing#public-assets-public-folder) for details. +See the [public assets section](/developers/extend/apps/config/public-assets) for details. ## Styling diff --git a/packages/twenty-docs/developers/extend/apps/layout/navigation-menu-items.mdx b/packages/twenty-docs/developers/extend/apps/layout/navigation-menu-items.mdx new file mode 100644 index 0000000000..2174116b52 --- /dev/null +++ b/packages/twenty-docs/developers/extend/apps/layout/navigation-menu-items.mdx @@ -0,0 +1,42 @@ +--- +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](/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: + + | Type | What it does | 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/developers/extend/apps/layout/overview.mdx b/packages/twenty-docs/developers/extend/apps/layout/overview.mdx new file mode 100644 index 0000000000..bb92c9c390 --- /dev/null +++ b/packages/twenty-docs/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/developers/extend/apps/layout/page-layouts.mdx b/packages/twenty-docs/developers/extend/apps/layout/page-layouts.mdx new file mode 100644 index 0000000000..e0e8bcaf22 --- /dev/null +++ b/packages/twenty-docs/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](/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](/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/developers/extend/apps/layout/views.mdx b/packages/twenty-docs/developers/extend/apps/layout/views.mdx new file mode 100644 index 0000000000..12144fa371 --- /dev/null +++ b/packages/twenty-docs/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](/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/developers/extend/apps/logic-functions.mdx b/packages/twenty-docs/developers/extend/apps/logic-functions.mdx deleted file mode 100644 index 2cee6836e0..0000000000 --- a/packages/twenty-docs/developers/extend/apps/logic-functions.mdx +++ /dev/null @@ -1,567 +0,0 @@ ---- -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 name = 'name' in params.queryStringParameters - ? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world' - : '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: 'GET', - 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. - - - - - -A post-install function is a logic function that runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services. - -```ts src/logic-functions/post-install.ts -import { definePostInstallLogicFunction, type 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()`. -- 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 is a logic function that runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped. - -```ts src/logic-functions/pre-install.ts -import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/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. - -``` -┌─────────────────────────────────────────────────────────────┐ -│ install flow │ -│ │ -│ upload package → [pre-install] → metadata migration → │ -│ generate SDK → [post-install] │ -│ │ -│ old schema visible new schema visible │ -└─────────────────────────────────────────────────────────────┘ -``` - -Pre-install is always **synchronous** (it blocks the install and can abort it). Post-install is **asynchronous by default** — enqueued on a worker with automatic retries — but can opt into synchronous execution with `shouldRunSynchronously: true`. See the `definePostInstallLogicFunction` accordion above for when to use each mode. - -**Use `post-install` for anything that needs the new schema to exist.** This is the common case: - -- Seeding default data (creating initial records, default views, demo content) against newly-added objects and fields. -- Registering webhooks with third-party services now that the app has its credentials. -- Calling your own API to finish setup that depends on the synchronized metadata. -- Idempotent "ensure this exists" logic that should reconcile state on every upgrade — combine with `shouldRunOnVersionUpgrade: true`. - -Example — seed a default `PostCard` record after install: - -```ts src/logic-functions/post-install.ts -import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/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. - - - - - -## 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/developers/extend/apps/connections.mdx b/packages/twenty-docs/developers/extend/apps/logic/connections.mdx similarity index 100% rename from packages/twenty-docs/developers/extend/apps/connections.mdx rename to packages/twenty-docs/developers/extend/apps/logic/connections.mdx diff --git a/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx b/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx new file mode 100644 index 0000000000..f2712aad20 --- /dev/null +++ b/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx @@ -0,0 +1,376 @@ +--- +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](/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/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/developers/extend/apps/logic/overview.mdx new file mode 100644 index 0000000000..fb0107689e --- /dev/null +++ b/packages/twenty-docs/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()`](/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](/developers/extend/apps/config/install-hooks). + diff --git a/packages/twenty-docs/developers/extend/apps/skills-and-agents.mdx b/packages/twenty-docs/developers/extend/apps/logic/skills-and-agents.mdx similarity index 100% rename from packages/twenty-docs/developers/extend/apps/skills-and-agents.mdx rename to packages/twenty-docs/developers/extend/apps/logic/skills-and-agents.mdx diff --git a/packages/twenty-docs/developers/extend/apps/operations/cli.mdx b/packages/twenty-docs/developers/extend/apps/operations/cli.mdx new file mode 100644 index 0000000000..1eff007902 --- /dev/null +++ b/packages/twenty-docs/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/developers/extend/apps/operations/overview.mdx b/packages/twenty-docs/developers/extend/apps/operations/overview.mdx new file mode 100644 index 0000000000..46374ecf44 --- /dev/null +++ b/packages/twenty-docs/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/developers/extend/apps/publishing.mdx b/packages/twenty-docs/developers/extend/apps/operations/publishing.mdx similarity index 97% rename from packages/twenty-docs/developers/extend/apps/publishing.mdx rename to packages/twenty-docs/developers/extend/apps/operations/publishing.mdx index 8ed1f40b99..c5e15d5c4f 100644 --- a/packages/twenty-docs/developers/extend/apps/publishing.mdx +++ b/packages/twenty-docs/developers/extend/apps/operations/publishing.mdx @@ -6,7 +6,7 @@ description: Distribute your Twenty app to the marketplace or deploy it internal ## Overview -Once your app is [built and tested locally](/developers/extend/apps/building), you have two paths for distributing it: +Once your app is [built and tested locally](/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. @@ -196,7 +196,7 @@ export default defineApplication({ }); ``` -See the [defineApplication accordion](/developers/extend/apps/building#defineentity-functions) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.). +See the [defineApplication accordion](/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 diff --git a/packages/twenty-docs/developers/extend/apps/cli-and-testing.mdx b/packages/twenty-docs/developers/extend/apps/operations/testing.mdx similarity index 63% rename from packages/twenty-docs/developers/extend/apps/cli-and-testing.mdx rename to packages/twenty-docs/developers/extend/apps/operations/testing.mdx index 5fd98f66f3..e77b2716a8 100644 --- a/packages/twenty-docs/developers/extend/apps/cli-and-testing.mdx +++ b/packages/twenty-docs/developers/extend/apps/operations/testing.mdx @@ -1,64 +1,10 @@ --- -title: CLI & Testing -description: CLI commands, testing setup, public assets, npm packages, remotes, and CI configuration. -icon: "terminal" +title: Testing +description: Vitest setup, integration tests against a real Twenty server, type checking, and CI with GitHub Actions. +icon: "flask" --- -## Public assets (`public/` folder) - -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. +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 @@ -118,11 +64,7 @@ The build step uses esbuild to produce a single self-contained file per logic fu 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. -## Testing your app - -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. - -### Setup +## Setup The scaffolded app already includes Vitest. If you set it up manually, install the dependencies: @@ -196,7 +138,7 @@ beforeAll(async () => { }); ``` -### Programmatic SDK APIs +## Programmatic SDK APIs The `twenty-sdk/cli` subpath exports functions you can call directly from test code: @@ -209,7 +151,7 @@ The `twenty-sdk/cli` subpath exports functions you can call directly from test c Each function returns a result object with `success: boolean` and either `data` or `error`. -### Writing an integration test +## Writing an integration test Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace: @@ -274,7 +216,7 @@ describe('App installation', () => { }); ``` -### Running tests +## Running tests Make sure your local Twenty server is running, then: @@ -288,7 +230,7 @@ Or in watch mode during development: yarn test:watch ``` -### Type checking +## Type checking You can also run type checking on your app without running tests: @@ -298,81 +240,6 @@ yarn twenty typecheck This runs `tsc --noEmit` and reports any type errors. -## CLI reference - -Beyond `dev`, `build`, `add`, and `typecheck`, the 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`. - ## 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. diff --git a/packages/twenty-docs/docs.json b/packages/twenty-docs/docs.json index 8cca449574..b651e42d60 100644 --- a/packages/twenty-docs/docs.json +++ b/packages/twenty-docs/docs.json @@ -374,15 +374,65 @@ { "group": "Apps", "pages": [ - "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" + { + "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" + ] + } ] }, { @@ -5404,6 +5454,58 @@ } }, "redirects": [ + { + "source": "/developers/extend/apps/getting-started", + "destination": "/developers/extend/apps/getting-started/quick-start" + }, + { + "source": "/developers/extend/apps/building", + "destination": "/developers/extend/apps/getting-started/concepts" + }, + { + "source": "/developers/extend/apps/data-model", + "destination": "/developers/extend/apps/data/overview" + }, + { + "source": "/developers/extend/apps/data/data-model", + "destination": "/developers/extend/apps/data/overview" + }, + { + "source": "/developers/extend/apps/data/application", + "destination": "/developers/extend/apps/config/application" + }, + { + "source": "/developers/extend/apps/data/roles", + "destination": "/developers/extend/apps/config/roles" + }, + { + "source": "/developers/extend/apps/cli-and-testing", + "destination": "/developers/extend/apps/operations/overview" + }, + { + "source": "/developers/extend/apps/publishing", + "destination": "/developers/extend/apps/operations/publishing" + }, + { + "source": "/developers/extend/apps/logic-functions", + "destination": "/developers/extend/apps/logic/logic-functions" + }, + { + "source": "/developers/extend/apps/skills-and-agents", + "destination": "/developers/extend/apps/logic/skills-and-agents" + }, + { + "source": "/developers/extend/apps/front-components", + "destination": "/developers/extend/apps/layout/front-components" + }, + { + "source": "/developers/extend/apps/layout", + "destination": "/developers/extend/apps/layout/overview" + }, + { + "source": "/developers/extend/apps/layout/views-and-pages", + "destination": "/developers/extend/apps/layout/overview" + }, { "source": "/developers/extend/capabilities/api", "destination": "/developers/extend/api"