i18n - docs translations (#18541)

Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
github-actions[bot]
2026-03-10 17:44:34 +01:00
committed by GitHub
parent 621962e049
commit e7fe435f60
26 changed files with 3392 additions and 107 deletions
@@ -837,6 +837,255 @@ export default defineFrontComponent({
* **مُنشأ بالقالب**: شغّل `yarn twenty entity:add` واختر خيار إضافة مكوّن أمامي جديد.
* **يدوي**: أنشئ ملفًا جديدًا `.tsx` واستخدم `defineFrontComponent()` مع اتباع النمط نفسه.
#### Where front components can be used
Front components can render in two locations within Twenty:
* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside page layouts. When configuring a dashboard or a record page layout, users can add a front component widget.
#### Headless vs non-headless
Front components come in two rendering modes controlled by the `isHeadless` option:
**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted.
**Headless** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below.
```typescript
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-action',
description: 'Runs an action without opening the side panel',
component: MyAction,
isHeadless: true,
command: {
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
label: 'Run my action',
},
});
```
#### Adding command menu items
To make a front component appear as an item in Twenty's command menu, add the `command` property to `defineFrontComponent()`. When users open the command menu (Cmd+K / Ctrl+K), the item shows up and triggers the front component on click.
The `command` object accepts the following fields:
| الحقل | النوع | الوصف |
| --------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `universalIdentifier` | `string` (required) | Unique ID for the command menu item |
| `التسمية` | `string` (required) | Display label shown in the command menu |
| `أيقونة` | `string` (optional) | Icon name (e.g., `'IconSparkles'`) |
| `isPinned` | `boolean` (optional) | Whether the command is pinned at the top of the menu |
| `availabilityType` | `'GLOBAL' \| 'RECORD_SELECTION'` (optional) | `GLOBAL` shows the command everywhere; `RECORD_SELECTION` shows it only in record contexts |
| `availabilityObjectUniversalIdentifier` | `string` (optional) | Restrict the command to a specific object type (e.g., Person) |
Here is an example from the call-recording app that adds a command scoped to Person records:
```typescript
import { defineFrontComponent } from 'twenty-sdk';
export default defineFrontComponent({
universalIdentifier: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
name: 'Summarize Person Call Recordings',
description: 'Generates a summary of call recordings for a person',
component: SummarizePersonRecordings,
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-234567890123',
label: 'Summarize call recordings',
icon: 'IconSparkles',
isPinned: false,
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
'20202020-e674-48e5-a542-72570eee7213',
},
});
```
When the command is synced, it appears in the command menu. If the front component is non-headless the side panel opens with the component rendered inside. If it is headless the component mounts in the background and executes its logic.
#### SDK Command components
The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done.
Import them from `twenty-sdk/command`:
* **`Command`** — Runs an async callback via the `execute` prop.
* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`.
Here is a full example of a headless front component using `Command` to run an action from the command menu:
```typescript
// src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
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 <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
And an example using `CommandModal` to ask for confirmation before executing:
```typescript
// src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
#### Execution context
Every front component receives an execution context that provides information about where and how it is running. Access context values using hooks from `twenty-sdk`:
| Hook | Return type | الوصف |
| ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useFrontComponentId()` | `string` | The unique ID of the current front component instance |
| `useRecordId()` | `string \| null` | The ID of the current record, when the component runs in a record context (e.g., a record page widget or a command scoped to a record). Returns `null` otherwise. |
| `useUserId()` | `string \| null` | The ID of the current user |
```typescript
import { useRecordId, useUserId } from 'twenty-sdk';
const MyWidget = () => {
const recordId = useRecordId();
const userId = useUserId();
return (
<div>
<p>Record: {recordId ?? 'none'}</p>
<p>User: {userId ?? 'anonymous'}</p>
</div>
);
};
```
The context is reactive — if the surrounding record changes, hooks automatically return the updated values.
#### Host API functions
Front components run in an isolated sandbox but can interact with Twenty's UI through a set of functions provided by the host. Import them directly from `twenty-sdk`:
```typescript
import {
navigate,
closeSidePanel,
enqueueSnackbar,
unmountFrontComponent,
openSidePanelPage,
openCommandConfirmationModal,
} from 'twenty-sdk';
```
| دالة | Signature | الوصف |
| ------------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `التنقل` | `(to, params?, queryParams?, options?) => Promise<void>` | Navigate to a typed app path within Twenty |
| `closeSidePanel` | `() => Promise<void>` | Close the side panel |
| `enqueueSnackbar` | `(params) => Promise<void>` | Show a snackbar notification. Params: `message`, `variant` (`'error'`, `'success'`, `'info'`, `'warning'`), optional `duration`, `detailedMessage`, `dedupeKey` |
| `unmountFrontComponent` | `() => Promise<void>` | Unmount the current front component (used by headless components to clean up after execution) |
| `openSidePanelPage` | `(params) => Promise<void>` | Open a page in the side panel. Params: `page`, `pageTitle`, `pageIcon`, `shouldResetSearchState` |
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>` | Show a confirmation modal and wait for the user's response. Params: `title`, `subtitle`, `confirmButtonText`, `confirmButtonAccent` (`'default'`, `'blue'`, `'danger'`) |
Here is an example that uses the host API to show a snackbar and close the side panel after an action completes:
```typescript
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### المهارات
تُحدِّد المهارات تعليمات وإمكانات قابلة لإعادة الاستخدام يمكن لوكلاء الذكاء الاصطناعي استخدامها داخل مساحة العمل لديك. استخدم `defineSkill()` لتعريف مهارات مع تحقّق مدمج:
@@ -77,7 +77,7 @@ To avoid unnecessary [re-renders](/l/de/developers/contribute/capabilities/front
### Zustandsverwaltung
[Jotai](https://jotai.org/) handles state management.
[Jotai](https://jotai.org/) übernimmt die Zustandsverwaltung.
Siehe [Best Practices](/l/de/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) für mehr Informationen zur Zustandsverwaltung.
@@ -1,20 +1,20 @@
---
title: Erste Schritte
description: Create your first Twenty app in minutes.
description: Erstellen Sie in wenigen Minuten Ihre erste Twenty-App.
---
<Warning>
Apps befinden sich derzeit in der Alpha-Testphase. Die Funktion ist funktionsfähig, entwickelt sich jedoch noch weiter.
</Warning>
Apps let you extend Twenty with custom objects, fields, logic functions, AI skills, and UI components — all managed as code.
Apps ermöglichen es Ihnen, Twenty mit benutzerdefinierten Objekten, Feldern, Logikfunktionen, KI-Fähigkeiten und UI-Komponenten zu erweitern — alles als Code verwaltet.
**Was Sie heute tun können:**
* Benutzerdefinierte Objekte und Felder als Code definieren (verwaltetes Datenmodell)
* Build logic functions with custom triggers (HTTP routes, cron, database events)
* Erstellen Sie Logikfunktionen mit benutzerdefinierten Triggern (HTTP-Routen, cron, Datenbankereignisse)
* Fähigkeiten für KI-Agenten definieren
* Build front components that render inside Twenty's UI
* Erstellen Sie Frontend-Komponenten, die in der Twenty-UI gerendert werden
* Dieselbe App in mehreren Workspaces bereitstellen
## Voraussetzungen
@@ -1,6 +1,6 @@
---
title: Publishing
description: Distribute your Twenty app to the marketplace or deploy it internally.
title: Veröffentlichen
description: Veröffentlichen Sie Ihre Twenty-App auf dem Twenty-Marktplatz oder stellen Sie sie intern bereit.
---
<Warning>
@@ -9,41 +9,41 @@ Apps befinden sich derzeit in der Alpha-Testphase. Die Funktion ist funktionsfä
## Übersicht
Once your app is [built and tested locally](/l/de/developers/extend/apps/building), you have two paths for distributing it:
Sobald Ihre App [lokal gebaut und getestet](/l/de/developers/extend/apps/building) wurde, haben Sie zwei Möglichkeiten, sie zu verteilen:
* **Publish to npm** — list your app in the Twenty marketplace for any workspace to discover and install.
* **Push a tarball** — deploy your app to a specific Twenty server for internal use without making it publicly available.
* **Auf npm veröffentlichen** — führen Sie Ihre App im Twenty-Marktplatz auf, damit jeder Arbeitsbereich sie entdecken und installieren kann.
* **Einen Tarball pushen** — stellen Sie Ihre App auf einem bestimmten Twenty-Server für die interne Nutzung bereit, ohne sie öffentlich verfügbar zu machen.
## Publishing to npm
## Auf npm veröffentlichen
Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twenty workspace can browse, install, and upgrade marketplace apps directly from the UI.
Die Veröffentlichung auf npm macht Ihre App im Twenty-Marktplatz auffindbar. Jeder Twenty-Arbeitsbereich kann Marktplatz-Apps direkt über die Benutzeroberfläche durchsuchen, installieren und aktualisieren.
### Requirements
### Anforderungen
* An [npm](https://www.npmjs.com) account
* Your package name **must** use the `twenty-app-` prefix (e.g., `twenty-app-postcard-sender`)
* Ein [npm](https://www.npmjs.com)-Konto
* Ihr Paketname **muss** das Präfix `twenty-app-` verwenden (z. B. `twenty-app-postcard-sender`)
### Schritte
1. **Build your app** — the CLI compiles your TypeScript sources and generates the application manifest:
1. **App erstellen** — die CLI kompiliert Ihre TypeScript-Quellen und erzeugt das Anwendungsmanifest:
```bash filename="Terminal"
yarn twenty app:build
```
2. **Publish to npm** — push the built package to the npm registry:
2. **Auf npm veröffentlichen** — pushen Sie das gebaute Paket in die npm-Registry:
```bash filename="Terminal"
npx twenty app:publish
```
### Auto-discovery
### Automatische Erkennung
Packages with the `twenty-app-` prefix are automatically discovered by the Twenty marketplace catalog. Once published, your app appears in the marketplace within a few minutes — no manual registration or approval required.
Pakete mit dem Präfix `twenty-app-` werden vom Twenty-Marktplatzkatalog automatisch erkannt. Nach der Veröffentlichung erscheint Ihre App innerhalb weniger Minuten im Marktplatz — keine manuelle Registrierung oder Genehmigung erforderlich.
### CI publishing
### CI-Veröffentlichung
The scaffolded project includes a GitHub Actions workflow that publishes on every release. It runs `app:build`, then `npm publish --provenance` from the build output:
Das vorgefertigte Projekt enthält einen GitHub-Actions-Workflow, der bei jedem Release eine Veröffentlichung durchführt. Er führt `app:build` aus und danach `npm publish --provenance` aus dem Build-Output:
```yaml filename=".github/workflows/publish.yml"
name: Publish
@@ -72,48 +72,48 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `npx twenty app:build`, then `npm publish` from `.twenty/output`.
Für andere CI-Systeme (GitLab CI, CircleCI usw.) gelten die gleichen drei Befehle: `yarn install`, `npx twenty app:build` und anschließend `npm publish` aus `.twenty/output`.
<Tip>
**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions.
**npm-Provenance** ist optional, wird jedoch empfohlen. Das Veröffentlichen mit `--provenance` fügt Ihrem npm-Eintrag ein Vertrauensabzeichen hinzu, sodass Nutzer überprüfen können, dass das Paket aus einem bestimmten Commit in einer öffentlichen CI-Pipeline gebaut wurde. Siehe die [npm-Provenance-Dokumentation](https://docs.npmjs.com/generating-provenance-statements) für Einrichtungshinweise.
</Tip>
## Internal distribution
## Interne Verteilung
For apps you don't want publicly available — proprietary tools, enterprise-only integrations, or experimental builds — you can push a tarball directly to a Twenty server.
Für Apps, die Sie nicht öffentlich verfügbar machen möchten — proprietäre Tools, nur für Unternehmen bestimmte Integrationen oder experimentelle Builds — können Sie einen Tarball direkt auf einen Twenty-Server pushen.
### Push a tarball
### Einen Tarball pushen
Build your app and deploy it to a specific server in one step:
Erstellen Sie Ihre App und stellen Sie sie in einem Schritt auf einem bestimmten Server bereit:
```bash filename="Terminal"
npx twenty app:publish --server <server-url>
```
Any workspace on that server can then install and upgrade the app from the **Applications** settings page.
Jeder Arbeitsbereich auf diesem Server kann die App anschließend über die Seite **Applications** in den Einstellungen installieren und aktualisieren.
### Version management
### Versionsverwaltung
To release an update:
So veröffentlichen Sie ein Update:
1. Bump the `version` field in your `package.json`
2. Push a new tarball with `npx twenty app:publish --server <server-url>`
3. Workspaces on that server will see the upgrade available in their settings
1. Erhöhen Sie das Feld `version` in Ihrer `package.json`
2. Pushen Sie einen neuen Tarball mit `npx twenty app:publish --server <server-url>`
3. Arbeitsbereiche auf diesem Server sehen in ihren Einstellungen, dass ein Upgrade verfügbar ist.
<Note>
Internal apps are scoped to the server they're pushed to. They won't appear in the public marketplace and can't be installed by workspaces on other servers.
Interne Apps sind auf den Server beschränkt, auf den sie gepusht werden. Sie erscheinen nicht im öffentlichen Marktplatz und können von Arbeitsbereichen auf anderen Servern nicht installiert werden.
</Note>
## App categories
## App-Kategorien
Twenty organizes apps into three categories based on how they're distributed:
Twenty organisiert Apps in drei Kategorien, basierend auf ihrer Vertriebsart:
| Kategorie | Wie es funktioniert | Visible in marketplace? |
| --------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------- |
| **Entwicklung** | Local dev mode apps running via `yarn twenty app:dev`. Used for building and testing. | Nein |
| **Published** | Apps published to npm with the `twenty-app-` prefix. Listed in the marketplace for any workspace to install. | Ja |
| **Internal** | Apps deployed via tarball to a specific server. Available only to workspaces on that server. | Nein |
| Kategorie | Wie es funktioniert | Im Marktplatz sichtbar? |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
| **Entwicklung** | Lokale Apps im Entwicklungsmodus, die über `yarn twenty app:dev` ausgeführt werden. Zum Erstellen und Testen verwendet. | Nein |
| **Veröffentlicht** | Auf npm veröffentlichte Apps mit dem Präfix `twenty-app-`. Im Marktplatz gelistet, damit jeder Arbeitsbereich sie installieren kann. | Ja |
| **Intern** | Apps, die per Tarball auf einen bestimmten Server bereitgestellt werden. Nur für Arbeitsbereiche auf diesem Server verfügbar. | Nein |
<Tip>
Start in **Development** mode while building your app. When it's ready, choose **Published** (npm) for broad distribution or **Internal** (tarball) for private deployment.
Beginnen Sie im **Entwicklungsmodus**, während Sie Ihre App erstellen. Wenn sie bereit ist, wählen Sie **Veröffentlicht** (npm) für die breite Verteilung oder **Intern** (Tarball) für die private Bereitstellung.
</Tip>
@@ -321,11 +321,11 @@ Sie können Standardfelder überschreiben, indem Sie in Ihrem `fields`-Array ein
dies wird jedoch nicht empfohlen.
</Note>
### Defining fields on existing objects
### Felder für bestehende Objekte definieren
Use `defineField()` to add custom fields to existing objects — both standard objects (like `company`, `person`, `opportunity`) and custom objects defined by other apps. Each field lives in its own file and references the target object by its `universalIdentifier`.
Verwenden Sie `defineField()`, um benutzerdefinierte Felder zu bestehenden Objekten hinzuzufügen — sowohl zu Standardobjekten (wie `company`, `person`, `opportunity`) als auch zu benutzerdefinierten Objekten, die von anderen Apps definiert werden. Jedes Feld befindet sich in einer eigenen Datei und verweist auf das Zielobjekt über dessen `universalIdentifier`.
To reference standard objects, import `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` from `twenty-sdk`. This constant provides stable identifiers for all built-in objects and their fields:
Um auf Standardobjekte zu verweisen, importieren Sie `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` aus `twenty-sdk`. Diese Konstante stellt stabile Bezeichner für alle integrierten Objekte und deren Felder bereit:
```typescript
// src/fields/apollo-total-funding.field.ts
@@ -349,22 +349,22 @@ export default defineField({
Hauptpunkte:
* `objectUniversalIdentifier` tells Twenty which object to attach the field to. Use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<objectName>.universalIdentifier` for standard objects.
* Each field requires its own stable `universalIdentifier`, a `name`, `type`, `label`, and the target `objectUniversalIdentifier`.
* You can scaffold new fields using `yarn twenty entity:add` and choosing the field option.
* `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` is also exported as `STANDARD_OBJECT` for convenience — both refer to the same constant.
* `objectUniversalIdentifier` teilt Twenty mit, an welches Objekt das Feld angehängt werden soll. Verwenden Sie `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<objectName>.universalIdentifier` für Standardobjekte.
* Jedes Feld benötigt einen eigenen stabilen `universalIdentifier`, `name`, `type`, `label` und den Ziel-`objectUniversalIdentifier`.
* Sie können mit `yarn twenty entity:add` neue Felder anlegen, indem Sie die Feldoption wählen.
* `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` wird der Einfachheit halber auch als `STANDARD_OBJECT` exportiert — beide verweisen auf dieselbe Konstante.
Available standard objects include: `attachment`, `blocklist`, `calendarChannel`, `calendarEvent`, `calendarEventParticipant`, `company`, `connectedAccount`, `dashboard`, `favorite`, `favoriteFolder`, `message`, `messageChannel`, `messageParticipant`, `messageThread`, `note`, `noteTarget`, `opportunity`, `person`, `task`, `taskTarget`, `timelineActivity`, `workflow`, `workflowAutomatedTrigger`, `workflowRun`, `workflowVersion`, and `workspaceMember`.
Verfügbare Standardobjekte sind unter anderem: `attachment`, `blocklist`, `calendarChannel`, `calendarEvent`, `calendarEventParticipant`, `company`, `connectedAccount`, `dashboard`, `favorite`, `favoriteFolder`, `message`, `messageChannel`, `messageParticipant`, `messageThread`, `note`, `noteTarget`, `opportunity`, `person`, `task`, `taskTarget`, `timelineActivity`, `workflow`, `workflowAutomatedTrigger`, `workflowRun`, `workflowVersion` und `workspaceMember`.
Each standard object also exposes its field identifiers. For example, to reference a specific field on a standard object in role permissions:
Jedes Standardobjekt stellt außerdem seine Feldbezeichner bereit. Beispielsweise, um in Rollenberechtigungen auf ein bestimmtes Feld eines Standardobjekts zu verweisen:
```typescript
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier
```
#### Relation fields on existing objects
#### Beziehungsfelder bei bestehenden Objekten
You can also define relation fields that link existing objects to your custom objects:
Sie können auch Beziehungsfelder definieren, die bestehende Objekte mit Ihren benutzerdefinierten Objekten verknüpfen:
```typescript
// src/fields/people-on-call-recording.field.ts
@@ -837,6 +837,255 @@ Sie können neue Frontend-Komponenten auf zwei Arten erstellen:
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Frontend-Komponente.
* **Manuell**: Erstellen Sie eine neue `.tsx`-Datei und verwenden Sie `defineFrontComponent()` nach demselben Muster.
#### Where front components can be used
Front components can render in two locations within Twenty:
* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside page layouts. When configuring a dashboard or a record page layout, users can add a front component widget.
#### Headless vs non-headless
Front components come in two rendering modes controlled by the `isHeadless` option:
**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted.
**Headless** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below.
```typescript
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-action',
description: 'Runs an action without opening the side panel',
component: MyAction,
isHeadless: true,
command: {
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
label: 'Run my action',
},
});
```
#### Adding command menu items
To make a front component appear as an item in Twenty's command menu, add the `command` property to `defineFrontComponent()`. When users open the command menu (Cmd+K / Ctrl+K), the item shows up and triggers the front component on click.
The `command` object accepts the following fields:
| Feld | Typ | Beschreibung |
| --------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `universalIdentifier` | `string` (required) | Unique ID for the command menu item |
| `beschriftung` | `string` (required) | Display label shown in the command menu |
| `symbol` | `string` (optional) | Icon name (e.g., `'IconSparkles'`) |
| `isPinned` | `boolean` (optional) | Whether the command is pinned at the top of the menu |
| `availabilityType` | `'GLOBAL' \| 'RECORD_SELECTION'` (optional) | `GLOBAL` shows the command everywhere; `RECORD_SELECTION` shows it only in record contexts |
| `availabilityObjectUniversalIdentifier` | `string` (optional) | Restrict the command to a specific object type (e.g., Person) |
Here is an example from the call-recording app that adds a command scoped to Person records:
```typescript
import { defineFrontComponent } from 'twenty-sdk';
export default defineFrontComponent({
universalIdentifier: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
name: 'Summarize Person Call Recordings',
description: 'Generates a summary of call recordings for a person',
component: SummarizePersonRecordings,
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-234567890123',
label: 'Summarize call recordings',
icon: 'IconSparkles',
isPinned: false,
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
'20202020-e674-48e5-a542-72570eee7213',
},
});
```
When the command is synced, it appears in the command menu. If the front component is non-headless the side panel opens with the component rendered inside. If it is headless the component mounts in the background and executes its logic.
#### SDK Command components
The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done.
Import them from `twenty-sdk/command`:
* **`Command`** — Runs an async callback via the `execute` prop.
* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`.
Here is a full example of a headless front component using `Command` to run an action from the command menu:
```typescript
// src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
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 <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
And an example using `CommandModal` to ask for confirmation before executing:
```typescript
// src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
#### Execution context
Every front component receives an execution context that provides information about where and how it is running. Access context values using hooks from `twenty-sdk`:
| Hook | Return type | Beschreibung |
| ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useFrontComponentId()` | `string` | The unique ID of the current front component instance |
| `useRecordId()` | `string \| null` | The ID of the current record, when the component runs in a record context (e.g., a record page widget or a command scoped to a record). Returns `null` otherwise. |
| `useUserId()` | `string \| null` | The ID of the current user |
```typescript
import { useRecordId, useUserId } from 'twenty-sdk';
const MyWidget = () => {
const recordId = useRecordId();
const userId = useUserId();
return (
<div>
<p>Record: {recordId ?? 'none'}</p>
<p>User: {userId ?? 'anonymous'}</p>
</div>
);
};
```
The context is reactive — if the surrounding record changes, hooks automatically return the updated values.
#### Host API functions
Front components run in an isolated sandbox but can interact with Twenty's UI through a set of functions provided by the host. Import them directly from `twenty-sdk`:
```typescript
import {
navigate,
closeSidePanel,
enqueueSnackbar,
unmountFrontComponent,
openSidePanelPage,
openCommandConfirmationModal,
} from 'twenty-sdk';
```
| Funktion | Signature | Beschreibung |
| ------------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `navigieren` | `(to, params?, queryParams?, options?) => Promise<void>` | Navigate to a typed app path within Twenty |
| `closeSidePanel` | `() => Promise<void>` | Close the side panel |
| `enqueueSnackbar` | `(params) => Promise<void>` | Show a snackbar notification. Params: `message`, `variant` (`'error'`, `'success'`, `'info'`, `'warning'`), optional `duration`, `detailedMessage`, `dedupeKey` |
| `unmountFrontComponent` | `() => Promise<void>` | Unmount the current front component (used by headless components to clean up after execution) |
| `openSidePanelPage` | `(params) => Promise<void>` | Open a page in the side panel. Params: `page`, `pageTitle`, `pageIcon`, `shouldResetSearchState` |
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>` | Show a confirmation modal and wait for the user's response. Params: `title`, `subtitle`, `confirmButtonText`, `confirmButtonAccent` (`'default'`, `'blue'`, `'danger'`) |
Here is an example that uses the host API to show a snackbar and close the side panel after an action completes:
```typescript
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### Fähigkeiten
Skills definieren wiederverwendbare Anweisungen und Fähigkeiten, die KI-Agenten in Ihrem Arbeitsbereich verwenden können. Verwenden Sie `defineSkill()`, um Skills mit eingebauter Validierung zu definieren:
@@ -974,12 +1223,12 @@ uploadFile(
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| Parameter | Typ | Beschreibung |
| ---------------------------------- | -------- | ------------------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Der Rohinhalt der Datei |
| `filename` | `string` | Der Name der Datei (wird für Speicherung und Anzeige verwendet) |
| `contentType` | `string` | MIME-Typ der Datei (standardmäßig `application/octet-stream`, wenn weggelassen) |
| `fieldMetadataUniversalIdentifier` | `string` | Der `universalIdentifier` des Dateityp-Felds in Ihrem Objekt |
| Parameter | Typ | Beschreibung |
| ---------------------------------- | -------------- | ------------------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Der Rohinhalt der Datei |
| `filename` | `string` | Der Name der Datei (wird für Speicherung und Anzeige verwendet) |
| `contentType` | `string` | MIME-Typ der Datei (standardmäßig `application/octet-stream`, wenn weggelassen) |
| `fieldMetadataUniversalIdentifier` | `Zeichenkette` | Der `universalIdentifier` des Dateityp-Felds in Ihrem Objekt |
Hauptpunkte:
@@ -15,7 +15,7 @@ Twenty ist darauf ausgelegt, erweiterbar zu sein. Verwenden Sie unsere APIs, Web
* **APIs**: Abfragen und ändern Sie Ihre CRM-Daten programmatisch mit REST oder GraphQL
* **Webhooks**: Erhalten Sie Benachrichtigungen in Echtzeit, wenn Ereignisse in Twenty auftreten
* **Apps**: Build custom applications that extend Twenty's capabilities
* **Apps**: Erstellen Sie benutzerdefinierte Anwendungen, die die Funktionalität von Twenty erweitern
## Erste Schritte
@@ -27,6 +27,6 @@ Twenty ist darauf ausgelegt, erweiterbar zu sein. Verwenden Sie unsere APIs, Web
Erhalten Sie Benachrichtigungen über Ereignisse in Echtzeit
</Card>
<Card title="Apps" icon="puzzle-piece" href="/l/de/developers/extend/apps/getting-started">
Build customizations as code
Erstellen Sie Anpassungen als Code
</Card>
</CardGroup>
@@ -77,7 +77,7 @@ Per evitare [re-render](/l/it/developers/contribute/capabilities/frontend-develo
### Gestione dello stato
[Jotai](https://jotai.org/) handles state management.
[Jotai](https://jotai.org/) gestisce lo stato.
Vedi [best practices](/l/it/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) per ulteriori informazioni sulla gestione dello stato.
@@ -837,6 +837,255 @@ Puoi creare nuovi componenti front-end in due modi:
* **Generata dallo scaffolder**: Esegui `yarn twenty entity:add` e scegli l'opzione per aggiungere un nuovo componente front-end.
* **Manuale**: Crea un nuovo file `.tsx` e usa `defineFrontComponent()`, seguendo lo stesso schema.
#### Where front components can be used
Front components can render in two locations within Twenty:
* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside page layouts. When configuring a dashboard or a record page layout, users can add a front component widget.
#### Headless vs non-headless
Front components come in two rendering modes controlled by the `isHeadless` option:
**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted.
**Headless** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below.
```typescript
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-action',
description: 'Runs an action without opening the side panel',
component: MyAction,
isHeadless: true,
command: {
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
label: 'Run my action',
},
});
```
#### Adding command menu items
To make a front component appear as an item in Twenty's command menu, add the `command` property to `defineFrontComponent()`. When users open the command menu (Cmd+K / Ctrl+K), the item shows up and triggers the front component on click.
The `command` object accepts the following fields:
| Campo | Tipo | Descrizione |
| --------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `universalIdentifier` | `string` (required) | Unique ID for the command menu item |
| `etichetta` | `string` (required) | Display label shown in the command menu |
| `icona` | `string` (optional) | Icon name (e.g., `'IconSparkles'`) |
| `isPinned` | `boolean` (optional) | Whether the command is pinned at the top of the menu |
| `availabilityType` | `'GLOBAL' \| 'RECORD_SELECTION'` (optional) | `GLOBAL` shows the command everywhere; `RECORD_SELECTION` shows it only in record contexts |
| `availabilityObjectUniversalIdentifier` | `string` (optional) | Restrict the command to a specific object type (e.g., Person) |
Here is an example from the call-recording app that adds a command scoped to Person records:
```typescript
import { defineFrontComponent } from 'twenty-sdk';
export default defineFrontComponent({
universalIdentifier: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
name: 'Summarize Person Call Recordings',
description: 'Generates a summary of call recordings for a person',
component: SummarizePersonRecordings,
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-234567890123',
label: 'Summarize call recordings',
icon: 'IconSparkles',
isPinned: false,
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
'20202020-e674-48e5-a542-72570eee7213',
},
});
```
When the command is synced, it appears in the command menu. If the front component is non-headless the side panel opens with the component rendered inside. If it is headless the component mounts in the background and executes its logic.
#### SDK Command components
The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done.
Import them from `twenty-sdk/command`:
* **`Command`** — Runs an async callback via the `execute` prop.
* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`.
Here is a full example of a headless front component using `Command` to run an action from the command menu:
```typescript
// src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
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 <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
And an example using `CommandModal` to ask for confirmation before executing:
```typescript
// src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
#### Execution context
Every front component receives an execution context that provides information about where and how it is running. Access context values using hooks from `twenty-sdk`:
| Hook | Return type | Descrizione |
| ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useFrontComponentId()` | `string` | The unique ID of the current front component instance |
| `useRecordId()` | `string \| null` | The ID of the current record, when the component runs in a record context (e.g., a record page widget or a command scoped to a record). Returns `null` otherwise. |
| `useUserId()` | `string \| null` | The ID of the current user |
```typescript
import { useRecordId, useUserId } from 'twenty-sdk';
const MyWidget = () => {
const recordId = useRecordId();
const userId = useUserId();
return (
<div>
<p>Record: {recordId ?? 'none'}</p>
<p>User: {userId ?? 'anonymous'}</p>
</div>
);
};
```
The context is reactive — if the surrounding record changes, hooks automatically return the updated values.
#### Host API functions
Front components run in an isolated sandbox but can interact with Twenty's UI through a set of functions provided by the host. Import them directly from `twenty-sdk`:
```typescript
import {
navigate,
closeSidePanel,
enqueueSnackbar,
unmountFrontComponent,
openSidePanelPage,
openCommandConfirmationModal,
} from 'twenty-sdk';
```
| Funzione | Signature | Descrizione |
| ------------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `naviga` | `(to, params?, queryParams?, options?) => Promise<void>` | Navigate to a typed app path within Twenty |
| `closeSidePanel` | `() => Promise<void>` | Close the side panel |
| `enqueueSnackbar` | `(params) => Promise<void>` | Show a snackbar notification. Params: `message`, `variant` (`'error'`, `'success'`, `'info'`, `'warning'`), optional `duration`, `detailedMessage`, `dedupeKey` |
| `unmountFrontComponent` | `() => Promise<void>` | Unmount the current front component (used by headless components to clean up after execution) |
| `openSidePanelPage` | `(params) => Promise<void>` | Open a page in the side panel. Params: `page`, `pageTitle`, `pageIcon`, `shouldResetSearchState` |
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>` | Show a confirmation modal and wait for the user's response. Params: `title`, `subtitle`, `confirmButtonText`, `confirmButtonAccent` (`'default'`, `'blue'`, `'danger'`) |
Here is an example that uses the host API to show a snackbar and close the side panel after an action completes:
```typescript
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### Abilità
Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation:
@@ -77,7 +77,7 @@ To avoid unnecessary [re-renders](/l/pt/developers/contribute/capabilities/front
### Gerenciamento de Estado
[Jotai](https://jotai.org/) handles state management.
[Jotai](https://jotai.org/) gerencia o estado.
Veja [melhores práticas](/l/pt/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) para mais informações sobre gerenciamento de estado.
@@ -838,6 +838,255 @@ Você pode criar novos componentes de front-end de duas formas:
* **Gerado automaticamente**: Execute `yarn twenty entity:add` e escolha a opção para adicionar um novo componente de front-end.
* **Manual**: Crie um novo ficheiro `.tsx` e use `defineFrontComponent()`, seguindo o mesmo padrão.
#### Where front components can be used
Front components can render in two locations within Twenty:
* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside page layouts. When configuring a dashboard or a record page layout, users can add a front component widget.
#### Headless vs non-headless
Front components come in two rendering modes controlled by the `isHeadless` option:
**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted.
**Headless** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below.
```typescript
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-action',
description: 'Runs an action without opening the side panel',
component: MyAction,
isHeadless: true,
command: {
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
label: 'Run my action',
},
});
```
#### Adding command menu items
To make a front component appear as an item in Twenty's command menu, add the `command` property to `defineFrontComponent()`. When users open the command menu (Cmd+K / Ctrl+K), the item shows up and triggers the front component on click.
The `command` object accepts the following fields:
| Campo | Tipo | Descrição |
| --------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `universalIdentifier` | `string` (required) | Unique ID for the command menu item |
| `etiqueta` | `string` (required) | Display label shown in the command menu |
| `ícone` | `string` (optional) | Icon name (e.g., `'IconSparkles'`) |
| `isPinned` | `boolean` (optional) | Whether the command is pinned at the top of the menu |
| `availabilityType` | `'GLOBAL' \| 'RECORD_SELECTION'` (optional) | `GLOBAL` shows the command everywhere; `RECORD_SELECTION` shows it only in record contexts |
| `availabilityObjectUniversalIdentifier` | `string` (optional) | Restrict the command to a specific object type (e.g., Person) |
Here is an example from the call-recording app that adds a command scoped to Person records:
```typescript
import { defineFrontComponent } from 'twenty-sdk';
export default defineFrontComponent({
universalIdentifier: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
name: 'Summarize Person Call Recordings',
description: 'Generates a summary of call recordings for a person',
component: SummarizePersonRecordings,
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-234567890123',
label: 'Summarize call recordings',
icon: 'IconSparkles',
isPinned: false,
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
'20202020-e674-48e5-a542-72570eee7213',
},
});
```
When the command is synced, it appears in the command menu. If the front component is non-headless the side panel opens with the component rendered inside. If it is headless the component mounts in the background and executes its logic.
#### SDK Command components
The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done.
Import them from `twenty-sdk/command`:
* **`Command`** — Runs an async callback via the `execute` prop.
* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`.
Here is a full example of a headless front component using `Command` to run an action from the command menu:
```typescript
// src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
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 <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
And an example using `CommandModal` to ask for confirmation before executing:
```typescript
// src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
#### Execution context
Every front component receives an execution context that provides information about where and how it is running. Access context values using hooks from `twenty-sdk`:
| Hook | Return type | Descrição |
| ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useFrontComponentId()` | `string` | The unique ID of the current front component instance |
| `useRecordId()` | `string \| null` | The ID of the current record, when the component runs in a record context (e.g., a record page widget or a command scoped to a record). Returns `null` otherwise. |
| `useUserId()` | `string \| null` | The ID of the current user |
```typescript
import { useRecordId, useUserId } from 'twenty-sdk';
const MyWidget = () => {
const recordId = useRecordId();
const userId = useUserId();
return (
<div>
<p>Record: {recordId ?? 'none'}</p>
<p>User: {userId ?? 'anonymous'}</p>
</div>
);
};
```
The context is reactive — if the surrounding record changes, hooks automatically return the updated values.
#### Host API functions
Front components run in an isolated sandbox but can interact with Twenty's UI through a set of functions provided by the host. Import them directly from `twenty-sdk`:
```typescript
import {
navigate,
closeSidePanel,
enqueueSnackbar,
unmountFrontComponent,
openSidePanelPage,
openCommandConfirmationModal,
} from 'twenty-sdk';
```
| Função | Signature | Descrição |
| ------------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `navegar` | `(to, params?, queryParams?, options?) => Promise<void>` | Navigate to a typed app path within Twenty |
| `closeSidePanel` | `() => Promise<void>` | Close the side panel |
| `enqueueSnackbar` | `(params) => Promise<void>` | Show a snackbar notification. Params: `message`, `variant` (`'error'`, `'success'`, `'info'`, `'warning'`), optional `duration`, `detailedMessage`, `dedupeKey` |
| `unmountFrontComponent` | `() => Promise<void>` | Unmount the current front component (used by headless components to clean up after execution) |
| `openSidePanelPage` | `(params) => Promise<void>` | Open a page in the side panel. Params: `page`, `pageTitle`, `pageIcon`, `shouldResetSearchState` |
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>` | Show a confirmation modal and wait for the user's response. Params: `title`, `subtitle`, `confirmButtonText`, `confirmButtonAccent` (`'default'`, `'blue'`, `'danger'`) |
Here is an example that uses the host API to show a snackbar and close the side panel after an action completes:
```typescript
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### Habilidades
As habilidades definem instruções e capacidades reutilizáveis que os agentes de IA podem usar no seu espaço de trabalho. Use `defineSkill()` para definir habilidades com validação integrada:
@@ -837,6 +837,255 @@ Puteți crea componente Front noi în două moduri:
* **Generat**: Rulați `yarn twenty entity:add` și alegeți opțiunea de a adăuga o componentă frontend nouă.
* **Manual**: Creați un fișier nou `.tsx` și folosiți `defineFrontComponent()`, urmând același model.
#### Where front components can be used
Front components can render in two locations within Twenty:
* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside page layouts. When configuring a dashboard or a record page layout, users can add a front component widget.
#### Headless vs non-headless
Front components come in two rendering modes controlled by the `isHeadless` option:
**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted.
**Headless** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below.
```typescript
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-action',
description: 'Runs an action without opening the side panel',
component: MyAction,
isHeadless: true,
command: {
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
label: 'Run my action',
},
});
```
#### Adding command menu items
To make a front component appear as an item in Twenty's command menu, add the `command` property to `defineFrontComponent()`. When users open the command menu (Cmd+K / Ctrl+K), the item shows up and triggers the front component on click.
The `command` object accepts the following fields:
| Câmp | Tip | Descriere |
| --------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `universalIdentifier` | `string` (required) | Unique ID for the command menu item |
| `etichetă` | `string` (required) | Display label shown in the command menu |
| `pictogramă` | `string` (optional) | Icon name (e.g., `'IconSparkles'`) |
| `isPinned` | `boolean` (optional) | Whether the command is pinned at the top of the menu |
| `availabilityType` | `'GLOBAL' \| 'RECORD_SELECTION'` (optional) | `GLOBAL` shows the command everywhere; `RECORD_SELECTION` shows it only in record contexts |
| `availabilityObjectUniversalIdentifier` | `string` (optional) | Restrict the command to a specific object type (e.g., Person) |
Here is an example from the call-recording app that adds a command scoped to Person records:
```typescript
import { defineFrontComponent } from 'twenty-sdk';
export default defineFrontComponent({
universalIdentifier: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
name: 'Summarize Person Call Recordings',
description: 'Generates a summary of call recordings for a person',
component: SummarizePersonRecordings,
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-234567890123',
label: 'Summarize call recordings',
icon: 'IconSparkles',
isPinned: false,
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
'20202020-e674-48e5-a542-72570eee7213',
},
});
```
When the command is synced, it appears in the command menu. If the front component is non-headless the side panel opens with the component rendered inside. If it is headless the component mounts in the background and executes its logic.
#### SDK Command components
The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done.
Import them from `twenty-sdk/command`:
* **`Command`** — Runs an async callback via the `execute` prop.
* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`.
Here is a full example of a headless front component using `Command` to run an action from the command menu:
```typescript
// src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
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 <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
And an example using `CommandModal` to ask for confirmation before executing:
```typescript
// src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
#### Execution context
Every front component receives an execution context that provides information about where and how it is running. Access context values using hooks from `twenty-sdk`:
| Hook | Return type | Descriere |
| ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useFrontComponentId()` | `string` | The unique ID of the current front component instance |
| `useRecordId()` | `string \| null` | The ID of the current record, when the component runs in a record context (e.g., a record page widget or a command scoped to a record). Returns `null` otherwise. |
| `useUserId()` | `string \| null` | The ID of the current user |
```typescript
import { useRecordId, useUserId } from 'twenty-sdk';
const MyWidget = () => {
const recordId = useRecordId();
const userId = useUserId();
return (
<div>
<p>Record: {recordId ?? 'none'}</p>
<p>User: {userId ?? 'anonymous'}</p>
</div>
);
};
```
The context is reactive — if the surrounding record changes, hooks automatically return the updated values.
#### Host API functions
Front components run in an isolated sandbox but can interact with Twenty's UI through a set of functions provided by the host. Import them directly from `twenty-sdk`:
```typescript
import {
navigate,
closeSidePanel,
enqueueSnackbar,
unmountFrontComponent,
openSidePanelPage,
openCommandConfirmationModal,
} from 'twenty-sdk';
```
| Funcție | Signature | Descriere |
| ------------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `navigate` | `(to, params?, queryParams?, options?) => Promise<void>` | Navigate to a typed app path within Twenty |
| `closeSidePanel` | `() => Promise<void>` | Close the side panel |
| `enqueueSnackbar` | `(params) => Promise<void>` | Show a snackbar notification. Params: `message`, `variant` (`'error'`, `'success'`, `'info'`, `'warning'`), optional `duration`, `detailedMessage`, `dedupeKey` |
| `unmountFrontComponent` | `() => Promise<void>` | Unmount the current front component (used by headless components to clean up after execution) |
| `openSidePanelPage` | `(params) => Promise<void>` | Open a page in the side panel. Params: `page`, `pageTitle`, `pageIcon`, `shouldResetSearchState` |
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>` | Show a confirmation modal and wait for the user's response. Params: `title`, `subtitle`, `confirmButtonText`, `confirmButtonAccent` (`'default'`, `'blue'`, `'danger'`) |
Here is an example that uses the host API to show a snackbar and close the side panel after an action completes:
```typescript
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### Abilități
Abilitățile definesc instrucțiuni și capabilități reutilizabile pe care agenții AI le pot folosi în spațiul dvs. de lucru. Folosiți `defineSkill()` pentru a defini abilități cu validare încorporată:
@@ -837,6 +837,255 @@ export default defineFrontComponent({
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового фронтенд-компонента.
* **Вручную**: Создайте новый файл `.tsx` и используйте `defineFrontComponent()`, следуя тому же шаблону.
#### Where front components can be used
Front components can render in two locations within Twenty:
* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside page layouts. When configuring a dashboard or a record page layout, users can add a front component widget.
#### Headless vs non-headless
Front components come in two rendering modes controlled by the `isHeadless` option:
**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted.
**Headless** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below.
```typescript
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-action',
description: 'Runs an action without opening the side panel',
component: MyAction,
isHeadless: true,
command: {
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
label: 'Run my action',
},
});
```
#### Adding command menu items
To make a front component appear as an item in Twenty's command menu, add the `command` property to `defineFrontComponent()`. When users open the command menu (Cmd+K / Ctrl+K), the item shows up and triggers the front component on click.
The `command` object accepts the following fields:
| Поле | Тип | Описание |
| --------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `universalIdentifier` | `string` (required) | Unique ID for the command menu item |
| `метка` | `string` (required) | Display label shown in the command menu |
| `иконка` | `string` (optional) | Icon name (e.g., `'IconSparkles'`) |
| `isPinned` | `boolean` (optional) | Whether the command is pinned at the top of the menu |
| `availabilityType` | `'GLOBAL' \| 'RECORD_SELECTION'` (optional) | `GLOBAL` shows the command everywhere; `RECORD_SELECTION` shows it only in record contexts |
| `availabilityObjectUniversalIdentifier` | `string` (optional) | Restrict the command to a specific object type (e.g., Person) |
Here is an example from the call-recording app that adds a command scoped to Person records:
```typescript
import { defineFrontComponent } from 'twenty-sdk';
export default defineFrontComponent({
universalIdentifier: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
name: 'Summarize Person Call Recordings',
description: 'Generates a summary of call recordings for a person',
component: SummarizePersonRecordings,
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-234567890123',
label: 'Summarize call recordings',
icon: 'IconSparkles',
isPinned: false,
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
'20202020-e674-48e5-a542-72570eee7213',
},
});
```
When the command is synced, it appears in the command menu. If the front component is non-headless the side panel opens with the component rendered inside. If it is headless the component mounts in the background and executes its logic.
#### SDK Command components
The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done.
Import them from `twenty-sdk/command`:
* **`Command`** — Runs an async callback via the `execute` prop.
* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`.
Here is a full example of a headless front component using `Command` to run an action from the command menu:
```typescript
// src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
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 <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
And an example using `CommandModal` to ask for confirmation before executing:
```typescript
// src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
#### Execution context
Every front component receives an execution context that provides information about where and how it is running. Access context values using hooks from `twenty-sdk`:
| Hook | Return type | Описание |
| ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useFrontComponentId()` | `строка` | The unique ID of the current front component instance |
| `useRecordId()` | `string \| null` | The ID of the current record, when the component runs in a record context (e.g., a record page widget or a command scoped to a record). Returns `null` otherwise. |
| `useUserId()` | `string \| null` | The ID of the current user |
```typescript
import { useRecordId, useUserId } from 'twenty-sdk';
const MyWidget = () => {
const recordId = useRecordId();
const userId = useUserId();
return (
<div>
<p>Record: {recordId ?? 'none'}</p>
<p>User: {userId ?? 'anonymous'}</p>
</div>
);
};
```
The context is reactive — if the surrounding record changes, hooks automatically return the updated values.
#### Host API functions
Front components run in an isolated sandbox but can interact with Twenty's UI through a set of functions provided by the host. Import them directly from `twenty-sdk`:
```typescript
import {
navigate,
closeSidePanel,
enqueueSnackbar,
unmountFrontComponent,
openSidePanelPage,
openCommandConfirmationModal,
} from 'twenty-sdk';
```
| Функция | Signature | Описание |
| ------------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `навигация` | `(to, params?, queryParams?, options?) => Promise<void>` | Navigate to a typed app path within Twenty |
| `closeSidePanel` | `() => Promise<void>` | Close the side panel |
| `enqueueSnackbar` | `(params) => Promise<void>` | Show a snackbar notification. Params: `message`, `variant` (`'error'`, `'success'`, `'info'`, `'warning'`), optional `duration`, `detailedMessage`, `dedupeKey` |
| `unmountFrontComponent` | `() => Promise<void>` | Unmount the current front component (used by headless components to clean up after execution) |
| `openSidePanelPage` | `(params) => Promise<void>` | Open a page in the side panel. Params: `page`, `pageTitle`, `pageIcon`, `shouldResetSearchState` |
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>` | Show a confirmation modal and wait for the user's response. Params: `title`, `subtitle`, `confirmButtonText`, `confirmButtonAccent` (`'default'`, `'blue'`, `'danger'`) |
Here is an example that uses the host API to show a snackbar and close the side panel after an action completes:
```typescript
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### Навыки
Навыки определяют многократно используемые инструкции и возможности, которые агенты ИИ могут использовать в вашем рабочем пространстве. Используйте `defineSkill()` для определения навыков со встроенной валидацией:
@@ -837,6 +837,255 @@ Yeni ön uç bileşenlerini iki şekilde oluşturabilirsiniz:
* **Şablondan**: `yarn twenty entity:add` çalıştırın ve yeni bir ön uç bileşeni ekleme seçeneğini seçin.
* **Manuel**: Aynı deseni izleyerek yeni bir `.tsx` dosyası oluşturun ve `defineFrontComponent()` kullanın.
#### Where front components can be used
Front components can render in two locations within Twenty:
* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside page layouts. When configuring a dashboard or a record page layout, users can add a front component widget.
#### Headless vs non-headless
Front components come in two rendering modes controlled by the `isHeadless` option:
**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted.
**Headless** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below.
```typescript
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-action',
description: 'Runs an action without opening the side panel',
component: MyAction,
isHeadless: true,
command: {
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
label: 'Run my action',
},
});
```
#### Adding command menu items
To make a front component appear as an item in Twenty's command menu, add the `command` property to `defineFrontComponent()`. When users open the command menu (Cmd+K / Ctrl+K), the item shows up and triggers the front component on click.
The `command` object accepts the following fields:
| Alan | Tür | Açıklama |
| --------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `universalIdentifier` | `string` (required) | Unique ID for the command menu item |
| `etiket` | `string` (required) | Display label shown in the command menu |
| `simge` | `string` (optional) | Icon name (e.g., `'IconSparkles'`) |
| `isPinned` | `boolean` (optional) | Whether the command is pinned at the top of the menu |
| `availabilityType` | `'GLOBAL' \| 'RECORD_SELECTION'` (optional) | `GLOBAL` shows the command everywhere; `RECORD_SELECTION` shows it only in record contexts |
| `availabilityObjectUniversalIdentifier` | `string` (optional) | Restrict the command to a specific object type (e.g., Person) |
Here is an example from the call-recording app that adds a command scoped to Person records:
```typescript
import { defineFrontComponent } from 'twenty-sdk';
export default defineFrontComponent({
universalIdentifier: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
name: 'Summarize Person Call Recordings',
description: 'Generates a summary of call recordings for a person',
component: SummarizePersonRecordings,
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-234567890123',
label: 'Summarize call recordings',
icon: 'IconSparkles',
isPinned: false,
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
'20202020-e674-48e5-a542-72570eee7213',
},
});
```
When the command is synced, it appears in the command menu. If the front component is non-headless the side panel opens with the component rendered inside. If it is headless the component mounts in the background and executes its logic.
#### SDK Command components
The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done.
Import them from `twenty-sdk/command`:
* **`Command`** — Runs an async callback via the `execute` prop.
* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`.
Here is a full example of a headless front component using `Command` to run an action from the command menu:
```typescript
// src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
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 <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
And an example using `CommandModal` to ask for confirmation before executing:
```typescript
// src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
#### Execution context
Every front component receives an execution context that provides information about where and how it is running. Access context values using hooks from `twenty-sdk`:
| Hook | Return type | Açıklama |
| ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useFrontComponentId()` | `string` | The unique ID of the current front component instance |
| `useRecordId()` | `string \| null` | The ID of the current record, when the component runs in a record context (e.g., a record page widget or a command scoped to a record). Returns `null` otherwise. |
| `useUserId()` | `string \| null` | The ID of the current user |
```typescript
import { useRecordId, useUserId } from 'twenty-sdk';
const MyWidget = () => {
const recordId = useRecordId();
const userId = useUserId();
return (
<div>
<p>Record: {recordId ?? 'none'}</p>
<p>User: {userId ?? 'anonymous'}</p>
</div>
);
};
```
The context is reactive — if the surrounding record changes, hooks automatically return the updated values.
#### Host API functions
Front components run in an isolated sandbox but can interact with Twenty's UI through a set of functions provided by the host. Import them directly from `twenty-sdk`:
```typescript
import {
navigate,
closeSidePanel,
enqueueSnackbar,
unmountFrontComponent,
openSidePanelPage,
openCommandConfirmationModal,
} from 'twenty-sdk';
```
| Fonksiyon | Signature | Açıklama |
| ------------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gezin` | `(to, params?, queryParams?, options?) => Promise<void>` | Navigate to a typed app path within Twenty |
| `closeSidePanel` | `() => Promise<void>` | Close the side panel |
| `enqueueSnackbar` | `(params) => Promise<void>` | Show a snackbar notification. Params: `message`, `variant` (`'error'`, `'success'`, `'info'`, `'warning'`), optional `duration`, `detailedMessage`, `dedupeKey` |
| `unmountFrontComponent` | `() => Promise<void>` | Unmount the current front component (used by headless components to clean up after execution) |
| `openSidePanelPage` | `(params) => Promise<void>` | Open a page in the side panel. Params: `page`, `pageTitle`, `pageIcon`, `shouldResetSearchState` |
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>` | Show a confirmation modal and wait for the user's response. Params: `title`, `subtitle`, `confirmButtonText`, `confirmButtonAccent` (`'default'`, `'blue'`, `'danger'`) |
Here is an example that uses the host API to show a snackbar and close the side panel after an action completes:
```typescript
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### Beceriler
Yetenekler, yapay zekâ ajanlarının çalışma alanınızda kullanabileceği yeniden kullanılabilir yönergeleri ve kabiliyetleri tanımlar. Yerleşik doğrulamayla yetenekleri tanımlamak için `defineSkill()` kullanın:
@@ -77,7 +77,7 @@ To avoid unnecessary [re-renders](/l/zh/developers/contribute/capabilities/front
### 状态管理
[Jotai](https://jotai.org/) handles state management.
[Jotai](https://jotai.org/) 处理状态管理。
查看[最佳实践](/l/zh/developers/contribute/capabilities/frontend-development/best-practices-front#state-management)以获取有关状态管理的更多信息。
@@ -0,0 +1,147 @@
---
title: 接口
description: 使用 REST 或 GraphQL 以编程方式查询和修改您的客户关系管理数据。
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Twenty 的设计对开发者友好,提供适配您自定义数据模型的强大 API。 我们提供四种不同的 API 类型来满足不同的集成需求。
## 开发者优先的方法
Twenty 会针对您的数据模型生成专用 API:
* **无需长 ID**:直接在端点中使用对象和字段名称
* **标准与自定义对象平等对待**:您的自定义对象将享有与内置对象相同的 API 支持
* **专用端点**:每个对象和字段都有自己的 API 端点
* **自定义文档**:专门为您的工作区的数据模型生成
<Note>
创建 API 密钥后,可在 **设置 → API & Webhooks** 中查看您的个性化 API 文档。 由于 Twenty 会生成与您的自定义数据模型相匹配的 API,因此文档对您的工作区是唯一的。
</Note>
## 两种 API 类型
### 核心 API
访问路径:`/rest/`或`/graphql/`。
处理您实际的**记录**(数据):
* 创建、读取、更新、删除 People、Companies、Opportunities 等。
* 查询并筛选数据
* 管理记录关系
### 元数据 API
访问路径:`/rest/metadata/`或`/metadata/`。
管理您的**工作区和数据模型**
* 创建、修改或删除对象和字段
* 配置工作区设置
* 定义对象之间的关系
## REST 与 GraphQL
核心 API 和元数据 API 均提供 REST 和 GraphQL 格式:
| 格式 | 可用操作 |
| ----------- | ------------------------------- |
| **REST** | CRUD、批量操作、Upsert |
| **GraphQL** | 同上 + **批量 Upsert**,在一次调用中进行关系查询 |
可根据需要选择 — 两种格式访问的是同一份数据。
## API 端点
| 环境 | 基础 URL |
| ------- | ------------------------- |
| **云端** | `https://api.twenty.com/` |
| **自托管** | `https://{your-domain}/` |
## 身份验证
每个 API 请求都需要在请求头中包含 API 密钥:
```
Authorization: Bearer YOUR_API_KEY
```
### 创建 API 密钥
1. 前往 **设置 → APIs & Webhooks**
2. 点击 **+ 创建密钥**
3. 配置:
* **名称**:密钥的描述性名称
* **到期日期**:密钥的到期时间
4. 单击 **保存**
5. **立即复制** — 密钥仅显示一次
<VimeoEmbed videoId="928786722" title="创建 API 密钥" />
<Warning>
您的 API 密钥可访问敏感数据。 不要与不受信任的服务共享它。 如果遭到泄露,请立即将其禁用并生成一个新的。
</Warning>
### 为 API 密钥分配角色
为提高安全性,请分配特定角色以限制访问:
1. 进入 **设置 → 角色**
2. 点击要分配的角色
3. 打开 **分配** 选项卡
4. 在 **API Keys** 下,点击 **+ Assign to API key**
5. 选择该 API 密钥
该密钥将继承该角色的权限。 详见 [权限](/l/zh/user-guide/permissions-access/capabilities/permissions)。
### 管理 API 密钥
**Regenerate**: 设置 → APIs & Webhooks → 点击密钥 → **Regenerate**
**Delete**: 设置 → APIs & Webhooks → 点击密钥 → **Delete**
## API 操作台
使用我们内置的操作台,可直接在浏览器中测试您的 API — 同时支持 **REST** 和 **GraphQL**。
### 访问操作台
1. 前往 **设置 → APIs & Webhooks**
2. 创建 API 密钥(必需)
3. 点击 **REST API** 或 **GraphQL API** 打开操作台
### 您将获得
* **交互式文档**:针对您的特定数据模型生成
* **实时测试**:对您的工作区执行真实的 API 调用
* **架构浏览器**:浏览可用的对象、字段和关系
* **请求构建器**:使用自动补全构建查询
操作台会反映您的自定义对象和字段,因此文档始终与您的工作区保持一致且准确。
## 批量操作
REST 和 GraphQL 均支持批量操作:
* **批量大小**:每个请求最多 60 条记录
* **操作**:创建、更新、删除多条记录
**仅 GraphQL 功能:**
* **批量 Upsert**:在一次调用中创建或更新
* 使用复数对象名称(例如,用 `CreateCompanies` 而不是 `CreateCompany`
## 速率限制
为确保平台稳定性,API 请求将受到限流:
| 限制 | 值 |
| -------- | ----------- |
| **请求** | 每分钟 100 次调用 |
| **批量大小** | 每次调用 60 条记录 |
<Tip>
使用批量操作以最大化吞吐量 — 在一次 API 调用中处理最多 60 条记录,而不是发起单独的请求。
</Tip>
@@ -0,0 +1,689 @@
---
title: 构建应用
description: 使用 Twenty SDK 定义对象、逻辑函数、前端组件等。
---
<Warning>
应用目前处于 Alpha 测试阶段。 该功能可用,但仍在演进中。
</Warning>
## 使用 SDK 资源(类型与配置)
twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以下是你最常接触的关键部分。
### 辅助函数
该 SDK 提供辅助函数用于定义你的应用实体。 如 [实体检测](/l/zh/developers/extend/apps/getting-started#entity-detection) 中所述,你必须使用 `export default define<Entity>({...})` 才能让你的实体被检测到:
| 函数 | 目的 |
| -------------------------------- | ------------------- |
| `defineApplication` | 配置应用元数据(必需,每个应用一个) |
| `defineObject` | 定义带字段的自定义对象 |
| `defineLogicFunction` | 定义带处理程序的逻辑函数 |
| `definePreInstallLogicFunction` | 定义一个安装前逻辑函数(每个应用一个) |
| `definePostInstallLogicFunction` | 定义一个安装后逻辑函数(每个应用一个) |
| `defineFrontComponent` | 为自定义 UI 定义前端组件 |
| `defineRole` | 配置角色权限和对象访问 |
| `defineField` | 为现有对象扩展额外字段 |
| `defineView` | 为对象定义已保存的视图 |
| `defineNavigationMenuItem` | 定义侧边栏导航链接 |
| `defineSkill` | 定义 AI 智能体技能 |
这些函数会在构建时校验你的配置,并提供 IDE 自动补全和类型安全。
### 定义对象
自定义对象同时描述工作空间中记录的架构与行为。 使用 `defineObject()` 以内置校验定义对象:
```typescript
// src/app/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
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,
},
],
});
```
关键点:
* 使用 `defineObject()` 以获得内置校验和更好的 IDE 支持。
* `universalIdentifier` 必须在各次部署间保持唯一且稳定。
* 每个字段都需要 `name`、`type`、`label` 以及其自身稳定的 `universalIdentifier`。
* `fields` 数组是可选的——你可以定义没有自定义字段的对象。
* 你可以使用 `yarn twenty entity:add` 脚手架创建新对象,它会引导你完成命名、字段和关系。
<Note>
**基础字段会自动创建。** 当你定义自定义对象时,Twenty 会自动添加标准字段
例如 `id`、`name`、`createdAt`、`updatedAt`、`createdBy`、`updatedBy` 和 `deletedAt`。
你无需在 `fields` 数组中定义这些字段——只需添加你的自定义字段。
你可以通过在你的 `fields` 数组中定义一个同名字段来覆盖默认字段,
但不建议这样做。
</Note>
### 应用配置(application-config.ts
每个应用都有一个 `application-config.ts` 文件,用于描述:
* **应用的身份**:标识符、显示名称和描述。
* **函数如何运行**:它们用于权限的角色。
* **(可选)变量**:以环境变量形式提供给函数的键值对。
* **(可选)安装前函数**:在应用安装之前运行的逻辑函数。
* **(可选)安装后函数**:在应用安装后运行的逻辑函数。
使用 `defineApplication()` 定义你的应用配置:
```typescript
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
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,
});
```
备注:
* `universalIdentifier` 字段是你拥有的确定性 ID;生成一次并在多次同步中保持稳定。
* `applicationVariables` 会变成函数可用的环境变量(例如,`DEFAULT_RECIPIENT_NAME` 可作为 `process.env.DEFAULT_RECIPIENT_NAME` 使用)。
* `defaultRoleUniversalIdentifier` 必须与角色文件一致(见下文)。
* 清单构建期间会自动检测安装前和安装后函数。 参见 [安装前函数](#pre-install-functions) 和 [安装后函数](#post-install-functions)。
#### 角色和权限
应用可以定义角色,以封装对工作空间对象与操作的权限。 `application-config.ts` 中的 `defaultRoleUniversalIdentifier` 字段指定你的应用逻辑函数所使用的默认角色。
* 作为 `TWENTY_API_KEY` 注入的运行时 API 密钥源自该默认函数角色。
* 类型化客户端将受限于该角色授予的权限。
* 遵循最小权限原则:仅授予函数所需权限来创建一个专用角色,然后引用其通用标识符。
##### 默认函数角色(*.role.ts
当你脚手架生成新应用时,CLI 也会创建一个默认角色文件。 使用 `defineRole()` 定义带内置校验的角色:
```typescript
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
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: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
});
```
随后,该角色的 `universalIdentifier` 会在 `application-config.ts` 中被引用为 `defaultRoleUniversalIdentifier`。 换句话说:
* **\*.role.ts** 定义默认函数角色可以执行的操作。
* **application-config.ts** 指向该角色,使你的函数继承其权限。
备注:
* 从脚手架生成的角色开始,然后按照最小权限原则逐步收紧权限。
* 将 `objectPermissions` 和 `fieldPermissions` 替换为你的函数所需的对象/字段。
* `permissionFlags` 控制对平台级能力的访问。 尽量保持最小化;仅添加所需项。
* 在 Hello World 应用中查看可运行示例:[`packages/twenty-apps/hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts)。
### 逻辑函数的配置与入口点
每个函数文件都使用 `defineLogicFunction()` 导出包含处理程序和可选触发器的配置。
```typescript
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
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,
triggers: [
// Public HTTP route trigger '/s/post-card/create'
{
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
type: 'route',
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
},
// Cron trigger (CRON pattern)
// {
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
// type: 'cron',
// pattern: '0 0 1 1 *',
// },
// Database event trigger
// {
// universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
// type: 'databaseEvent',
// eventName: 'person.updated',
// updatedFields: ['name'],
// },
],
});
```
常见触发器类型:
* **route**:在\*\*`/s/` 端点\*\*下通过 HTTP 路径与方法公开你的函数:
> 例如 `path: '/post-card/create',` -> 调用 `<APP_URL>/s/post-card/create`
* **cron**:使用 CRON 表达式按计划运行你的函数。
* **databaseEvent**:在工作空间对象生命周期事件上运行。 当事件操作为 `updated` 时,可以在 `updatedFields` 数组中指定要监听的特定字段。 如果未定义或为空,任何更新都会触发该函数。
> 例如 `person.updated`
备注:
* `triggers` 数组是可选的。 没有触发器的函数可作为实用函数,被其他函数调用。
* 你可以在单个函数中混用多种触发器类型。
### 安装前函数
安装前函数是在你的应用安装到工作区之前自动运行的逻辑函数。 这对于执行验证任务、先决条件检查,或在主安装开始前准备工作区状态很有用。
当你使用 `create-twenty-app` 脚手架创建一个新应用时,会在 `src/logic-functions/pre-install.ts` 为你生成一个安装前函数:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
你也可以随时使用 CLI 手动执行安装前函数:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
关键点:
* 安装前函数使用 `definePreInstallLogicFunction()` —— 这是一个省略触发器设置(`cronTriggerSettings`、`databaseEventTriggerSettings`、`httpRouteTriggerSettings`、`isTool`)的专用变体。
* 处理器会接收一个 `InstallLogicFunctionPayload`,其包含 `{ previousVersion: string }` —— 即之前安装的应用版本(全新安装则为空字符串)。
* 每个应用仅允许一个安装前函数。 如果检测到多个,清单构建将报错。
* 在构建期间,函数的 `universalIdentifier` 会自动设置为应用清单上的 `preInstallLogicFunctionUniversalIdentifier` —— 你无需在 `defineApplication()` 中引用它。
* 默认超时时间设置为 300 秒(5 分钟),以便支持更长的准备任务。
* 安装前函数不需要触发器——它们会在安装前由平台调用,或通过 `function:execute --preInstall` 手动调用。
### 安装后函数
安装后函数是在你的应用安装到工作区后自动运行的逻辑函数。 这对于一次性设置任务很有用,例如填充默认数据、创建初始记录或配置工作区设置。
当你使用 `create-twenty-app` 脚手架创建一个新应用时,会在 `src/logic-functions/post-install.ts` 为你生成一个安装后函数:
```typescript
// src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
你也可以随时使用 CLI 手动执行安装后函数:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
关键点:
* 安装后函数使用 `definePostInstallLogicFunction()` —— 这是一个省略触发器设置(`cronTriggerSettings`、`databaseEventTriggerSettings`、`httpRouteTriggerSettings`、`isTool`)的专用变体。
* 处理器会接收一个 `InstallLogicFunctionPayload`,其包含 `{ previousVersion: string }` —— 即之前安装的应用版本(全新安装则为空字符串)。
* 每个应用仅允许一个安装后函数。 如果检测到多个,清单构建将报错。
* 在构建期间,函数的 `universalIdentifier` 会自动设置为应用清单上的 `postInstallLogicFunctionUniversalIdentifier` —— 你无需在 `defineApplication()` 中引用它。
* 默认超时时间设置为 300 秒(5 分钟),以便支持更长的设置任务,如数据填充。
* 安装后函数不需要触发器——它们会在安装过程中由平台调用,或通过 `function:execute --postInstall` 手动调用。
### 路由触发器负载
<Warning>
**破坏性变更(v1.16,2026 年 1 月):** 路由触发器的负载格式已更改。 在 v1.16 之前,查询参数、路径参数和请求体会直接作为负载发送。 从 v1.16 开始,它们被嵌套在结构化的 `RoutePayload` 对象中。
**v1.16 之前:**
```typescript
const handler = async (params) => {
const { param1, param2 } = params; // Direct access
};
```
**v1.16 之后:**
```typescript
const handler = async (event: RoutePayload) => {
const { param1, param2 } = event.body; // Access via .body
const { queryParam } = event.queryStringParameters;
const { id } = event.pathParameters;
};
```
**迁移现有函数:** 将处理程序更新为从 `event.body`、`event.queryStringParameters` 或 `event.pathParameters` 解构,而不是直接从参数对象解构。
</Warning>
当路由触发器调用你的逻辑函数时,它会接收一个遵循 AWS HTTP API v2 格式的 `RoutePayload` 对象。 从 `twenty-sdk` 导入该类型:
```typescript
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
const { headers, queryStringParameters, pathParameters, body } = event;
// HTTP method and path are available in requestContext
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
```
`RoutePayload` 类型具有以下结构:
| 属性 | 类型 | 描述 |
| ---------------------------- | ------------------------------------- | ------------------------------------------------ |
| `headers` | `Record<string, string \| undefined>` | HTTP 请求头(仅限 `forwardedRequestHeaders` 中列出的那些) |
| `queryStringParameters` | `Record<string, string \| undefined>` | 查询字符串参数(多个值以逗号连接) |
| `pathParameters` | `Record<string, string \| undefined>` | 从路由模式中提取的路径参数(例如,`/users/:id` → `{ id: '123' }` |
| `body` | `object \| null` | 已解析的请求体(JSON) |
| `isBase64Encoded` | `boolean` | 请求体是否为 base64 编码 |
| `requestContext.http.method` | `string` | HTTP 方法(GET、POST、PUT、PATCH、DELETE |
| `requestContext.http.path` | `string` | 原始请求路径 |
### 转发 HTTP 请求头
出于安全原因,默认**不会**将传入请求的 HTTP 请求头传递给你的逻辑函数。 如需访问特定请求头,请在 `forwardedRequestHeaders` 数组中显式列出:
```typescript
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
triggers: [
{
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
type: 'route',
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
],
});
```
随后你可以在处理程序中访问这些请求头:
```typescript
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
```
<Note>
请求头名称会被规范化为小写。 请使用小写键访问它们(例如,`event.headers['content-type']`)。
</Note>
你可以通过两种方式创建新函数:
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新逻辑函数的选项。 这将生成一个包含处理程序和配置的入门文件。
* **手动**:创建一个新的 `*.logic-function.ts` 文件,并使用 `defineLogicFunction()`,遵循相同的模式。
### 将逻辑函数标记为工具
逻辑函数可以作为供 AI 智能体和工作流使用的**工具**对外提供。 当函数被标记为工具时,Twenty 的 AI 功能即可发现它,并可在工作流自动化中将其选作一个步骤。
要将逻辑函数标记为工具,请设置 `isTool: true`,并提供 `toolInputSchema`,使用 [JSON Schema](https://json-schema.org/) 描述预期的输入参数:
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
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,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
关键点:
* **`isTool`** (`boolean`, 默认: `false`): 当设置为 `true` 时,该函数会被注册为工具,并可供 AI 代理和工作流自动化使用。
* **`toolInputSchema`** (`object`, 可选): 描述函数可接受参数的 JSON Schema 对象。 AI 代理使用此架构来理解该工具期望的输入并验证调用。 如果省略,架构将默认为 `{ type: 'object', properties: {} }`(无参数)。
* 设置为 `isTool: false`(或未设置)的函数**不会**被暴露为工具。 它们仍可直接执行或被其他函数调用,但不会出现在工具发现中。
* **工具命名**: 当作为工具对外暴露时,函数名会被自动规范化为 `logic_function_<name>`(转换为小写,非字母数字字符替换为下划线)。 例如,`enrich-company` 将变为 `logic_function_enrich_company`。
* 你可以将 `isTool` 与触发器结合使用——一个函数既可以作为工具(由 AI 代理调用),也可以同时由事件(cron、数据库事件、路由)触发。
<Note>
**写一个好的 `description`。** AI 代理会依赖该函数的 `description` 字段来决定何时使用该工具。 明确说明该工具的作用以及应在何时调用。
</Note>
### 前端组件
前端组件使你可以构建在 Twenty 的 UI 中渲染的自定义 React 组件。 使用 `defineFrontComponent()` 以内置校验定义组件:
```typescript
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
关键点:
* 前端组件是在 Twenty 中的隔离上下文中渲染的 React 组件。
* `component` 字段引用你的 React 组件。
* 组件会在 `yarn twenty app:dev` 期间自动构建并同步。
你可以通过两种方式创建新的前端组件:
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新前端组件的选项。
* **手动**:创建一个新的 `.tsx` 文件,并使用 `defineFrontComponent()`,遵循相同的模式。
### 技能
技能定义了可复用的指令和能力,AI 智能体可在你的工作区中使用。 使用 `defineSkill()` 定义带内置校验的技能:
```typescript
// src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
关键点:
* `name` 是该技能的唯一标识字符串(推荐使用 kebab-case)。
* `label` 是在 UI 中显示的人类可读名称。
* `content` 包含技能指令——这是 AI 智能体使用的文本。
* `icon`(可选)设置在 UI 中显示的图标。
* `description`(可选)提供有关技能用途的更多上下文。
你可以通过两种方式创建新技能:
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新技能的选项。
* **手动**:创建一个新文件,并使用 `defineSkill()`,遵循相同的模式。
### 生成的类型化客户端
两个类型化客户端由 `yarn twenty app:dev` 自动生成(基于你的工作区架构),并存放在 `node_modules/twenty-sdk/generated`
* **`CoreApiClient`** — 查询 `/graphql` 端点以获取工作区数据
* **`MetadataApiClient`** — 查询 `/metadata` 端点以获取工作区配置并处理文件上传
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
每当你的对象或字段发生变化时,`yarn twenty app:dev` 都会自动重新生成这两个客户端。
#### 逻辑函数中的运行时凭据
当你的函数在 Twenty 上运行时,平台会在代码执行前将凭据作为环境变量注入:
* `TWENTY_API_URL`:你的应用所针对的 Twenty API 的基础 URL。
* `TWENTY_API_KEY`:作用域限定于你的应用默认函数角色的短期密钥。
备注:
* 你无需向生成的客户端传递 URL 或 API 密钥。 它会在运行时从 process.env 读取 `TWENTY_API_URL` 和 `TWENTY_API_KEY`。
* API 密钥的权限由 `application-config.ts` 中通过 `defaultRoleUniversalIdentifier` 引用的角色决定。 这是你的应用逻辑函数使用的默认角色。
* 应用可以定义角色以遵循最小权限原则。 仅授予函数所需的权限,然后将 `defaultRoleUniversalIdentifier` 指向该角色的通用标识符。
#### 上传文件
生成的 `MetadataApiClient` 包含一个 `uploadFile` 方法,用于将文件附加到你的工作区对象的文件类型字段。 由于标准 GraphQL 客户端不原生支持多部分文件上传,该客户端提供了一个专用方法,在底层实现了 [GraphQL 多部分请求规范](https://github.com/jaydenseric/graphql-multipart-request-spec)。
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
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 (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
方法签名:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| 参数 | 类型 | 描述 |
| ---------------------------------- | -------- | ------------------------------------------------ |
| `fileBuffer` | `Buffer` | 原始文件内容 |
| `filename` | `string` | 文件名称(用于存储和显示) |
| `contentType` | `string` | 文件的 MIME 类型(如果省略,默认为 `application/octet-stream` |
| `fieldMetadataUniversalIdentifier` | `string` | 你的对象上文件类型字段的 `universalIdentifier` |
关键点:
* `uploadFile` 方法可在 `MetadataApiClient` 上使用,因为上传 mutation 由 `/metadata` 端点解析。
* 它使用该字段的 `universalIdentifier`(而不是其工作区特定的 ID),因此你的上传代码可以在安装了你的应用的任何工作区中使用——这与应用在其他地方引用字段的方式保持一致。
* 返回的 `url` 是一个签名 URL,你可以用它来访问已上传的文件。
### Hello World 示例
在[此处](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world)查看一个最小的端到端示例,展示对象、逻辑函数、前端组件和多种触发器。
@@ -0,0 +1,233 @@
---
title: 开始使用
description: 几分钟内创建你的第一个 Twenty 应用。
---
<Warning>
应用目前处于 Alpha 测试阶段。 该功能可用,但仍在演进中。
</Warning>
应用可通过自定义对象、字段、逻辑函数、AI 技能和 UI 组件来扩展 Twenty——全部以代码进行管理。
**你现在可以做什么:**
* 以代码定义自定义对象和字段(受管理的数据模型)
* 使用自定义触发器(HTTP 路由、cron、数据库事件)构建逻辑函数
* 为 AI 智能体定义技能
* 构建在 Twenty 的 UI 中渲染的前端组件
* 将同一个应用部署到多个工作空间
## 先决条件
* Node.js 24+ 和 Yarn 4
* 一个 Twenty 工作空间和一个 API 密钥(在 https://app.twenty.com/settings/api-webhooks 创建)
## 开始使用
使用官方脚手架创建一个新应用,然后进行身份验证并开始开发:
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty app:dev
```
脚手架工具支持两种模式,用于控制包含哪些示例文件:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
```
从这里您可以:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty entity:add
# Watch your application's function logs
yarn twenty function:logs
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# Display commands' help
yarn twenty help
```
另请参阅:[create-twenty-app](https://www.npmjs.com/package/create-twenty-app) 和 [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk) 的 CLI 参考页面。
## 项目结构(脚手架生成)
当你运行 `npx create-twenty-app@latest my-twenty-app` 时,脚手架将:
* 将一个最小的基础应用复制到 `my-twenty-app/` 中
* 添加本地 `twenty-sdk` 依赖和 Yarn 4 配置
* 创建与 `twenty` CLI 关联的配置文件和脚本
* 生成核心文件(应用配置、默认函数角色、安装前/安装后函数),并基于脚手架模式生成示例文件
使用默认 `--exhaustive` 模式新搭建的应用如下所示:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
yarn.lock
.gitignore
.nvmrc
.yarnrc.yml
.yarn/
install-state.gz
.oxlintrc.json
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
src/
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # Default role for logic functions
├── objects/
│ └── example-object.ts # Example custom object definition
├── fields/
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Example logic function
│ ├── pre-install.ts # Pre-install logic function
│ └── post-install.ts # Post-install logic function
├── front-components/
│ └── hello-world.tsx # Example front component
├── views/
│ └── example-view.ts # Example saved view definition
├── navigation-menu-items/
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
└── skills/
└── example-skill.ts # Example AI agent skill definition
```
使用 `--minimal` 时,只会创建核心文件(`application-config.ts`、`roles/default-role.ts`、`logic-functions/pre-install.ts` 和 `logic-functions/post-install.ts`)。
总体来说:
* **package.json**:声明应用名称、版本、引擎(Node 24+、Yarn 4),并添加 `twenty-sdk` 以及一个 `twenty` 脚本,该脚本会委托给本地的 `twenty` CLI。 运行 `yarn twenty help` 以列出所有可用命令。
* **.gitignore**:忽略常见产物,如 `node_modules`、`.yarn`、`generated/`(类型化客户端)、`dist/`、`build/`、覆盖率文件夹、日志文件以及 `.env*` 文件。
* **yarn.lock**、**.yarnrc.yml**、**.yarn/**:锁定并配置项目使用的 Yarn 4 工具链。
* **.nvmrc**:固定项目期望的 Node.js 版本。
* **.oxlintrc.json** 和 **tsconfig.json**:为应用的 TypeScript 源码提供 Lint 与 TypeScript 配置。
* **README.md**:应用根目录中的简短 README,包含基本说明。
* **public/**: 一个用于存储公共资源(图像、字体、静态文件)的文件夹,这些资源将随你的应用程序一起提供。 放置在此处的文件会在同步期间上传,并可在运行时访问。
* **src/**:你以代码形式定义应用的主要位置
### 实体检测
该 SDK 通过在你的 TypeScript 文件中解析 **`export default define<Entity>({...})`** 调用来检测实体。 每种实体类型都有一个从 `twenty-sdk` 导出的对应辅助函数:
| 辅助函数 | 实体类型 |
| -------------------------------- | ---------------- |
| `defineObject` | 自定义对象定义 |
| `defineLogicFunction` | 逻辑函数定义 |
| `definePreInstallLogicFunction` | 安装前逻辑函数(在安装之前运行) |
| `definePostInstallLogicFunction` | 安装后逻辑函数(在安装之后运行) |
| `defineFrontComponent` | 前端组件定义 |
| `defineRole` | 角色定义 |
| `defineField` | 现有对象的字段扩展 |
| `defineView` | 已保存的视图定义 |
| `defineNavigationMenuItem` | 导航菜单项定义 |
| `defineSkill` | AI 代理技能定义 |
<Note>
**文件命名是灵活的。** 实体检测基于 AST — SDK 会扫描你的源文件以查找 `export default define<Entity>({...})` 模式。 你可以按照自己的喜好组织文件和文件夹。 按实体类型分组(例如 `logic-functions/`、`roles/`)只是代码组织的一种约定,并非必需。
</Note>
已检测实体的示例:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
后续命令将添加更多文件和文件夹:
* `yarn twenty app:dev` 会在 `node_modules/twenty-sdk/generated` 中自动生成两个类型化 API 客户端:`CoreApiClient`(通过 `/graphql` 获取工作区数据)和 `MetadataApiClient`(通过 `/metadata` 处理工作区配置和文件上传)。
* `yarn twenty entity:add` 会在 `src/` 下为你的自定义对象、函数、前端组件、角色、技能等添加实体定义文件。
## 身份验证
首次运行 `yarn twenty auth:login` 时,你将被提示输入:
* API URL(默认为 http://localhost:3000 或你当前的工作空间配置)
* API 密钥
你的凭据按用户存储在 `~/.twenty/config.json` 中。 你可以维护多个配置文件并在它们之间切换。
### 管理工作空间
```bash filename="Terminal"
# Login interactively (recommended)
yarn twenty auth:login
# Login to a specific workspace profile
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn twenty auth:switch
# Switch to a specific workspace
yarn twenty auth:switch production
# Check current authentication status
yarn twenty auth:status
```
使用 `yarn twenty auth:switch` 切换工作空间后,后续所有命令将默认使用该工作空间。 你仍可通过 `--workspace <name>` 临时覆盖。
## 手动设置(不使用脚手架)
虽然我们建议使用 `create-twenty-app` 以获得最佳的上手体验,但你也可以手动设置项目。 不要全局安装 CLI。 相反,请将 `twenty-sdk` 添加为本地依赖,并在你的 package.json 中配置一个脚本:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
然后添加一个 `twenty` 脚本:
```json filename="package.json"
{
"scripts": {
"twenty": "twenty"
}
}
```
现在你可以通过 `yarn twenty <command>` 运行所有命令,例如 `yarn twenty app:dev`、`yarn twenty help` 等。
## 故障排除
* 身份验证错误:运行 `yarn twenty auth:login`,并确保你的 API 密钥具有所需权限。
* 无法连接到服务器:请验证 API URL,并确保 Twenty 服务器可达。
* 类型或客户端缺失/过期:重启 `yarn twenty app:dev` — 它会自动生成类型化客户端。
* 开发模式未同步:确保 `yarn twenty app:dev` 正在运行,并且你的环境不会忽略变更。
Discord 帮助频道:https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -0,0 +1,119 @@
---
title: 发布
description: 将你的 Twenty 应用分发到应用市场,或进行内部部署。
---
<Warning>
应用目前处于 Alpha 测试阶段。 该功能可用,但仍在演进中。
</Warning>
## 概览
一旦你的应用已[在本地构建并完成测试](/l/zh/developers/extend/apps/building),你可以通过两种方式进行分发:
* **发布到 npm** — 将你的应用在 Twenty 应用市场上架,供任何工作区发现并安装。
* **推送 tar 包** — 将你的应用部署到特定的 Twenty 服务器供内部使用,而无需公开发布。
## 发布到 npm
发布到 npm 可让你的应用在 Twenty 应用市场中被发现。 任何 Twenty 工作区都可以直接通过 UI 浏览、安装和升级应用市场中的应用。
### 要求
* 一个 [npm](https://www.npmjs.com) 账户
* 你的包名**必须**使用 `twenty-app-` 前缀(例如,`twenty-app-postcard-sender`
### 步骤
1. **构建你的应用** — CLI 会编译你的 TypeScript 源码并生成应用清单:
```bash filename="Terminal"
yarn twenty app:build
```
2. **发布到 npm** — 将构建好的包推送到 npm 注册表:
```bash filename="Terminal"
npx twenty app:publish
```
### 自动发现
Twenty 应用市场目录会自动发现带有 `twenty-app-` 前缀的包。 发布后,你的应用会在几分钟内出现在应用市场中 — 无需手动注册或审批。
### CI 发布
脚手架项目包含一个 GitHub Actions 工作流,会在每次发版时自动发布。 它会先运行 `app:build`,然后在构建输出目录中执行 `npm publish --provenance`
```yaml filename=".github/workflows/publish.yml"
name: Publish
on:
release:
types: [published]
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: https://registry.npmjs.org
- run: yarn install --immutable
- run: npx twenty app:build
- run: npm publish --provenance --access public
working-directory: .twenty/output
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
对于其他 CI 系统(GitLab CI、CircleCI 等),同样适用以下三条命令:`yarn install`、`npx twenty app:build`,然后在 `.twenty/output` 目录下执行 `npm publish`。
<Tip>
**npm provenance** 可选,但建议启用。 使用 `--provenance` 发布会在你的 npm 列表中添加可信徽章,使用户可以验证该包是由公共 CI 流水线中的特定提交构建的。 有关设置说明,请参见 [npm provenance 文档](https://docs.npmjs.com/generating-provenance-statements)。
</Tip>
## 内部分发
对于你不希望公开的应用 — 例如专有工具、仅供企业使用的集成或实验性构建 — 你可以将 tar 包直接推送到某台 Twenty 服务器。
### 推送 tar 包
在一步中构建你的应用并将其部署到特定服务器:
```bash filename="Terminal"
npx twenty app:publish --server <server-url>
```
该服务器上的任何工作区随后都可以在**应用程序**设置页面安装和升级该应用。
### 版本管理
要发布更新:
1. 更新 `package.json` 中的 `version` 字段
2. 使用 `npx twenty app:publish --server <server-url>` 推送新的 tar 包
3. 该服务器上的工作区会在其设置中看到可用的升级
<Note>
内部应用的作用范围仅限于它们被推送到的服务器。 它们不会出现在公共应用市场中,其他服务器上的工作区也无法安装。
</Note>
## 应用类别
Twenty 会根据分发方式将应用归为三类:
| 类别 | 工作原理 | 在应用市场中可见? |
| ------- | ------------------------------------------------- | --------- |
| **开发** | 通过 `yarn twenty app:dev` 运行的本地开发模式应用。 用于构建和测试。 | 否 |
| **已发布** | 使用 `twenty-app-` 前缀发布到 npm 的应用。 在应用市场上架,供任何工作区安装。 | 是 |
| **内部** | 通过 tar 包部署到特定服务器的应用。 仅对该服务器上的工作区可用。 | 否 |
<Tip>
在构建你的应用时,从**开发**模式开始。 准备就绪后,选择用于广泛分发的**已发布**(npm),或用于私有部署的**内部**(tar 包)。
</Tip>
@@ -321,11 +321,11 @@ export default defineObject({
但不建议这样做。
</Note>
### Defining fields on existing objects
### 在现有对象上定义字段
Use `defineField()` to add custom fields to existing objects — both standard objects (like `company`, `person`, `opportunity`) and custom objects defined by other apps. Each field lives in its own file and references the target object by its `universalIdentifier`.
使用 `defineField()` 向现有对象添加自定义字段——包括标准对象(如 `company``person``opportunity`)以及由其他应用定义的自定义对象。 每个字段位于其各自的文件中,并通过其 `universalIdentifier` 引用目标对象。
To reference standard objects, import `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` from `twenty-sdk`. This constant provides stable identifiers for all built-in objects and their fields:
要引用标准对象,请从 `twenty-sdk` 导入 `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`。 该常量为所有内置对象及其字段提供稳定的标识符:
```typescript
// src/fields/apollo-total-funding.field.ts
@@ -349,22 +349,22 @@ export default defineField({
关键点:
* `objectUniversalIdentifier` tells Twenty which object to attach the field to. Use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<objectName>.universalIdentifier` for standard objects.
* Each field requires its own stable `universalIdentifier`, a `name`, `type`, `label`, and the target `objectUniversalIdentifier`.
* You can scaffold new fields using `yarn twenty entity:add` and choosing the field option.
* `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` is also exported as `STANDARD_OBJECT` for convenience — both refer to the same constant.
* `objectUniversalIdentifier` 告诉 Twenty 将该字段附加到哪个对象。 使用 `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<objectName>.universalIdentifier` 用于标准对象。
* 每个字段都需要其自身稳定的 `universalIdentifier``name``type``label`,以及目标 `objectUniversalIdentifier`
* 你可以使用 `yarn twenty entity:add` 脚手架创建新字段,并选择字段选项。
* `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` 也以 `STANDARD_OBJECT` 的名称导出以方便使用——二者指向同一常量。
Available standard objects include: `attachment`, `blocklist`, `calendarChannel`, `calendarEvent`, `calendarEventParticipant`, `company`, `connectedAccount`, `dashboard`, `favorite`, `favoriteFolder`, `message`, `messageChannel`, `messageParticipant`, `messageThread`, `note`, `noteTarget`, `opportunity`, `person`, `task`, `taskTarget`, `timelineActivity`, `workflow`, `workflowAutomatedTrigger`, `workflowRun`, `workflowVersion`, and `workspaceMember`.
可用的标准对象包括:`attachment``blocklist``calendarChannel``calendarEvent``calendarEventParticipant``company``connectedAccount``dashboard``favorite``favoriteFolder``message``messageChannel``messageParticipant``messageThread``note``noteTarget``opportunity``person``task``taskTarget``timelineActivity``workflow``workflowAutomatedTrigger``workflowRun``workflowVersion` `workspaceMember`
Each standard object also exposes its field identifiers. For example, to reference a specific field on a standard object in role permissions:
每个标准对象还会暴露其字段标识符。 例如,要在角色权限中引用标准对象上的特定字段:
```typescript
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier
```
#### Relation fields on existing objects
#### 现有对象上的关联字段
You can also define relation fields that link existing objects to your custom objects:
你还可以定义关联字段,将现有对象链接到你的自定义对象:
```typescript
// src/fields/people-on-call-recording.field.ts
@@ -837,6 +837,255 @@ export default defineFrontComponent({
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新前端组件的选项。
* **手动**:创建一个新的 `.tsx` 文件,并使用 `defineFrontComponent()`,遵循相同的模式。
#### Where front components can be used
Front components can render in two locations within Twenty:
* **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
* **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside page layouts. When configuring a dashboard or a record page layout, users can add a front component widget.
#### Headless vs non-headless
Front components come in two rendering modes controlled by the `isHeadless` option:
**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted.
**Headless** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below.
```typescript
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-action',
description: 'Runs an action without opening the side panel',
component: MyAction,
isHeadless: true,
command: {
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
label: 'Run my action',
},
});
```
#### Adding command menu items
To make a front component appear as an item in Twenty's command menu, add the `command` property to `defineFrontComponent()`. When users open the command menu (Cmd+K / Ctrl+K), the item shows up and triggers the front component on click.
The `command` object accepts the following fields:
| 字段 | 类型 | 描述 |
| --------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `universalIdentifier` | `string` (required) | Unique ID for the command menu item |
| `标签` | `string` (required) | Display label shown in the command menu |
| `图标` | `string` (optional) | Icon name (e.g., `'IconSparkles'`) |
| `isPinned` | `boolean` (optional) | Whether the command is pinned at the top of the menu |
| `availabilityType` | `'GLOBAL' \| 'RECORD_SELECTION'` (optional) | `GLOBAL` shows the command everywhere; `RECORD_SELECTION` shows it only in record contexts |
| `availabilityObjectUniversalIdentifier` | `string` (optional) | Restrict the command to a specific object type (e.g., Person) |
Here is an example from the call-recording app that adds a command scoped to Person records:
```typescript
import { defineFrontComponent } from 'twenty-sdk';
export default defineFrontComponent({
universalIdentifier: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
name: 'Summarize Person Call Recordings',
description: 'Generates a summary of call recordings for a person',
component: SummarizePersonRecordings,
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-234567890123',
label: 'Summarize call recordings',
icon: 'IconSparkles',
isPinned: false,
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
'20202020-e674-48e5-a542-72570eee7213',
},
});
```
When the command is synced, it appears in the command menu. If the front component is non-headless the side panel opens with the component rendered inside. If it is headless the component mounts in the background and executes its logic.
#### SDK Command components
The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done.
Import them from `twenty-sdk/command`:
* **`Command`** — Runs an async callback via the `execute` prop.
* **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`.
Here is a full example of a headless front component using `Command` to run an action from the command menu:
```typescript
// src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
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 <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
And an example using `CommandModal` to ask for confirmation before executing:
```typescript
// src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
#### Execution context
Every front component receives an execution context that provides information about where and how it is running. Access context values using hooks from `twenty-sdk`:
| Hook | Return type | 描述 |
| ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useFrontComponentId()` | `string` | The unique ID of the current front component instance |
| `useRecordId()` | `string \| null` | The ID of the current record, when the component runs in a record context (e.g., a record page widget or a command scoped to a record). Returns `null` otherwise. |
| `useUserId()` | `string \| null` | The ID of the current user |
```typescript
import { useRecordId, useUserId } from 'twenty-sdk';
const MyWidget = () => {
const recordId = useRecordId();
const userId = useUserId();
return (
<div>
<p>Record: {recordId ?? 'none'}</p>
<p>User: {userId ?? 'anonymous'}</p>
</div>
);
};
```
The context is reactive — if the surrounding record changes, hooks automatically return the updated values.
#### Host API functions
Front components run in an isolated sandbox but can interact with Twenty's UI through a set of functions provided by the host. Import them directly from `twenty-sdk`:
```typescript
import {
navigate,
closeSidePanel,
enqueueSnackbar,
unmountFrontComponent,
openSidePanelPage,
openCommandConfirmationModal,
} from 'twenty-sdk';
```
| 函数 | Signature | 描述 |
| ------------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `导航` | `(to, params?, queryParams?, options?) => Promise<void>` | Navigate to a typed app path within Twenty |
| `closeSidePanel` | `() => Promise<void>` | Close the side panel |
| `enqueueSnackbar` | `(params) => Promise<void>` | Show a snackbar notification. Params: `message`, `variant` (`'error'`, `'success'`, `'info'`, `'warning'`), optional `duration`, `detailedMessage`, `dedupeKey` |
| `unmountFrontComponent` | `() => Promise<void>` | Unmount the current front component (used by headless components to clean up after execution) |
| `openSidePanelPage` | `(params) => Promise<void>` | Open a page in the side panel. Params: `page`, `pageTitle`, `pageIcon`, `shouldResetSearchState` |
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>` | Show a confirmation modal and wait for the user's response. Params: `title`, `subtitle`, `confirmButtonText`, `confirmButtonAccent` (`'default'`, `'blue'`, `'danger'`) |
Here is an example that uses the host API to show a snackbar and close the side panel after an action completes:
```typescript
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### 技能
Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation:
@@ -1,7 +1,6 @@
---
title: 扩展
description: 使用 API、网络钩子和自定义应用扩展 Twenty 的功能。
redirect: /developers/introduction
---
<Frame>
@@ -16,18 +15,18 @@ Twenty 的设计旨在实现可扩展性。 使用我们的 API、网络钩子
* **API**:使用 REST 或 GraphQL 以编程方式查询和修改您的客户关系管理数据
* **网络钩子**:当 Twenty 中发生事件时接收实时通知
* **应用**:构建扩展 Twenty 功能的自定义应用程序 - 即将推出!
* **应用**:构建扩展 Twenty 功能的自定义应用程序
## 开始使用
<CardGroup cols={2}>
<Card title="接口" icon="代码" href="/l/zh/developers/api">
<Card title="接口" icon="代码" href="/l/zh/developers/extend/api">
以编程方式连接到 Twenty
</Card>
<Card title="Webhooks" icon="bell" href="/l/zh/developers/webhooks">
<Card title="Webhooks" icon="bell" href="/l/zh/developers/extend/webhooks">
实时接收事件通知
</Card>
<Card title="应用" icon="puzzle-piece" href="/l/zh/developers/apps/apps">
以代码方式构建自定义项 (Alpha)
<Card title="应用" icon="puzzle-piece" href="/l/zh/developers/extend/apps/getting-started">
以代码方式构建自定义项
</Card>
</CardGroup>
@@ -0,0 +1,116 @@
---
title: Webhooks
description: 当您的 CRM 中发生事件时接收实时通知。
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
当 Twenty 中发生事件时,Webhook 会实时将数据推送到您的系统 — 无需轮询。 使用它们来保持外部系统同步、触发自动化或发送警报。
## 创建 Webhook
1. 前往 **设置 → APIs & Webhooks → Webhooks**
2. 单击 **+ 创建 Webhook**
3. 输入您的 Webhook URL(必须可公开访问)
4. 单击 **保存**
Webhook 会立即激活并开始发送通知。
<VimeoEmbed videoId="928786708" title="创建 Webhook" />
### 管理 Webhooks
**编辑**:点击该 Webhook → 更新 URL → **保存**
**删除**:点击该 Webhook → **删除** → 确认
## 事件
Twenty 会针对以下事件类型发送 Webhook:
| 事件 | 示例 |
| --------- | ---------------------------------------------------------- |
| **记录已创建** | `person.created`, `company.created`, `note.created` |
| **记录已更新** | `person.updated`, `company.updated`, `opportunity.updated` |
| **记录已删除** | `person.deleted`, `company.deleted` |
所有事件类型都会发送到您的 Webhook URL。 事件过滤可能会在未来的版本中添加。
## 负载格式
每个 Webhook 都会发送一个带有 JSON 正文的 HTTP POST
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| 字段 | 描述 |
| ----------- | -------------------------- |
| `event` | 发生了什么(例如,`person.created` |
| `data` | 已创建/更新/删除的完整记录 |
| `timestamp` | 事件发生的时间(UTC |
<Note>
请返回 **2xx HTTP 状态**200-299)以确认已接收。 非 2xx 的响应将被记录为投递失败。
</Note>
## Webhook 验证
为了安全起见,Twenty 会对每个 Webhook 请求进行签名。 请验证签名以确保请求真实有效。
### 请求头
| 请求头 | 描述 |
| ---------------------------- | -------------- |
| `X-Twenty-Webhook-Signature` | HMAC SHA256 签名 |
| `X-Twenty-Webhook-Timestamp` | 请求时间戳 |
### 验证步骤
1. 从 `X-Twenty-Webhook-Timestamp` 获取时间戳
2. 构造字符串:`{timestamp}:{JSON payload}`
3. 使用您的 Webhook 密钥计算 HMAC SHA256
4. 与 `X-Twenty-Webhook-Signature` 进行比较
### 示例(Node.js
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const receivedSignature = req.headers["x-twenty-webhook-signature"];
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature, "hex"),
Buffer.from(receivedSignature, "hex")
);
```
## Webhooks 与工作流
| 方法 | 方向 | 用例 |
| ------------------- | --- | -------------------- |
| **Webhooks** | OUT | 自动通知外部系统任何记录变更 |
| **工作流 + HTTP 请求** | OUT | 使用自定义逻辑(过滤、转换)向外发送数据 |
| **工作流 Webhook 触发器** | IN | 从外部系统接收数据到 Twenty |
如需接收外部数据,请参见 [设置 Webhook 触发器](/l/zh/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)。
@@ -5,28 +5,18 @@ description: 欢迎来到 Twenty 开发者文档,这是您用于扩展、自
import { CardTitle } from "/snippets/card-title.mdx"
<CardGroup cols={2}>
<Card href="/l/zh/developers/api" icon="code">
<CardTitle>API</CardTitle>
使用 REST 或 GraphQL 查询和修改您的 CRM 数据
<CardGroup cols={3}>
<Card href="/l/zh/developers/extend/extend" img="/images/user-guide/integrations/plug.png">
<CardTitle>扩展</CardTitle>
使用 API、网络钩子和自定义应用构建集成
</Card>
<Card href="/l/zh/developers/webhooks" icon="bell">
<CardTitle>Webhooks</CardTitle>
当事件发生时接收实时通知。
</Card>
<Card href="/l/zh/developers/apps/apps" icon="puzzle-piece">
<CardTitle>Apps</CardTitle>
构建扩展 Twenty 功能的定制应用程序。
</Card>
<Card href="/l/zh/developers/self-host/self-host" icon="desktop">
<Card href="/l/zh/developers/self-host/self-host" img="/images/user-guide/what-is-twenty/20.png">
<CardTitle>自托管</CardTitle>
在您自己的基础设施上部署并管理 Twenty。
</Card>
<Card href="/l/zh/developers/contribute/contribute" icon="github">
<Card href="/l/zh/developers/contribute/contribute" img="/images/user-guide/github/github-header.png">
<CardTitle>贡献</CardTitle>
加入我们的开源社区并为 Twenty 做出贡献。
</Card>
@@ -24,7 +24,7 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
* 仅导出**可见列**
* 仅导出**筛选后的记录**(基于您当前视图)
<Note>对于更大的导出(20,000+ 条记录),请使用筛选器分批导出,或使用 [API](/l/zh/developers/api)。</Note>
<Note>对于更大的导出(20,000+ 条记录),请使用筛选器分批导出,或使用 [API](/l/zh/developers/extend/api)。</Note>
### 权限
@@ -148,7 +148,7 @@ API 没有记录数量限制:
2. 使用 GraphQL API 查询记录
3. 在您的应用程序中处理结果
参见:[API 文档](/l/zh/developers/api)
参见:[API 文档](/l/zh/developers/extend/api)
## 技巧与最佳实践
@@ -206,4 +206,4 @@ API 没有记录数量限制:
* [如何更新现有记录](/l/zh/user-guide/data-migration/how-tos/update-existing-records-via-import) — 编辑并重新导入您的导出文件
* [如何通过 API 导入数据](/l/zh/user-guide/data-migration/how-tos/import-data-via-api) — 适用于大型数据集
* [API 文档](/l/zh/developers/api) — 构建自定义导出工作流
* [API 文档](/l/zh/developers/extend/api) — 构建自定义导出工作流
@@ -57,10 +57,10 @@ Twenty 实施速率限制以确保系统稳定性:
Twenty 支持两种 API 类型:
| 接口 | 最适合 | 文档 |
| ----------- | ----------------------- | ---------------------------------------------- |
| **GraphQL** | 灵活查询、获取关联数据、复杂操作 | [API 文档](/l/zh/developers/api) |
| **REST** | 简单的 CRUD 操作、熟悉的 REST 模式 | [API 文档](/l/zh/developers/api) |
| 接口 | 最适合 | 文档 |
| ----------- | ----------------------- | -------------------------------- |
| **GraphQL** | 灵活查询、获取关联数据、复杂操作 | [API 文档](/l/zh/developers/extend/api) |
| **REST** | 简单的 CRUD 操作、熟悉的 REST 模式 | [API 文档](/l/zh/developers/extend/api) |
两种 API 都支持:
@@ -173,4 +173,4 @@ GraphQL API 支持**批量合并插入** — 若记录已存在则更新,不
有关完整的实现细节、代码示例和架构参考:
* [API 文档](/l/zh/developers/api)
* [API 文档](/l/zh/developers/extend/api)
@@ -35,7 +35,7 @@ description: Twenty 是一款开源 CRM,为您提供构建模块,助您精
* **仪表板:** 通过自定义报表和可视化跟踪绩效。 [查看仪表板](/l/zh/user-guide/dashboards/overview)。
* **权限与访问:** 通过基于角色的权限控制谁可以查看、编辑和管理您的数据。 [配置访问](/l/zh/user-guide/permissions-access/overview)。
* **笔记与任务:** 创建与您的记录关联的笔记和任务,以便更好地协作。
* **API 与 Webhook:** 连接到其他应用,并构建自定义集成。 [开始集成](/l/zh/developers/api)。
* **API 与 Webhook:** 连接到其他应用,并构建自定义集成。 [开始集成](/l/zh/developers/extend/api)。
## 立即加入
@@ -95,7 +95,7 @@ description: 使用工作流显示关联记录中的数据(例如,在机会
* 公司规模:`{{searchRecords[0].employees}}`
<Note>
**任务和备注限制**:任务和备注上的关系被硬编码为多对多,目前尚不可用于工作流触发器或操作。 要访问这些关系,请改用 [API](/l/zh/developers/api)。
**任务和备注限制**:任务和备注上的关系被硬编码为多对多,目前尚不可用于工作流触发器或操作。 要访问这些关系,请改用 [API](/l/zh/developers/extend/api)。
</Note>
## 双向同步