---
title: Приложения Twenty
description: Создавайте и управляйте настройками Twenty в виде кода.
---

<Warning>
Приложения сейчас проходят альфа-тестирование. Функциональность работает, но продолжает развиваться.
</Warning>

## Что такое приложения?

Приложения позволяют создавать и управлять настройками Twenty **в виде кода**. Вместо настройки всего через интерфейс вы определяете модель данных и логические функции в коде — так быстрее создавать, поддерживать и развёртывать в нескольких рабочих пространствах.

**Что вы можете делать уже сегодня:**

* Определяйте пользовательские объекты и поля в виде кода (управляемая модель данных)
* Создавайте логические функции с пользовательскими триггерами
* Определите навыки и агентов для ИИ
* Развёртывайте одно и то же приложение в нескольких рабочих пространствах

## Требования

* Node.js 24+ и Yarn 4
* Рабочее пространство Twenty и ключ API (создайте его на https://app.twenty.com/settings/api-webhooks)

## Начало работы

Создайте новое приложение с помощью официального генератора, затем выполните аутентификацию и начните разработку:

```bash filename="Terminal"
# Создать каркас нового приложения (по умолчанию включает все примеры)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app

# Запустить режим разработки: автоматически синхронизирует локальные изменения с вашим рабочим пространством
yarn twenty app:dev
```

Генератор каркаса поддерживает два режима для управления тем, какие файлы-примеры включаются:

```bash filename="Terminal"
# По умолчанию (полный набор): все примеры (объект, поле, логическая функция, фронтенд-компонент, представление, пункт меню навигации, навык, агент)
npx create-twenty-app@latest my-app

# Минимальный: только основные файлы (application-config.ts и default-role.ts)
npx create-twenty-app@latest my-app --minimal
```

Отсюда вы можете:

```bash filename="Terminal"
# Добавить новую сущность в ваше приложение (с мастером)
yarn twenty entity:add

# Просматривать логи функций вашего приложения
yarn twenty function:logs

# Выполнить функцию по имени
yarn twenty function:execute -n my-function -p '{"name": "test"}'

# Выполнить предустановочную функцию
yarn twenty function:execute --preInstall

# Выполнить послеустановочную функцию
yarn twenty function:execute --postInstall

# Собрать приложение для распространения
yarn twenty app:build

# Опубликовать приложение в npm или на сервер Twenty
yarn twenty app:publish

# Удалить приложение из текущего рабочего пространства
yarn twenty app:uninstall

# Показать справку по командам
yarn twenty help
```

Смотрите также: страницы справки CLI для [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) и [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).

## Структура проекта (сгенерированного)

Когда вы запускаете `npx create-twenty-app@latest my-twenty-app`, генератор:

* Копирует минимальное базовое приложение в `my-twenty-app/`
* Добавляет локальную зависимость `twenty-sdk` и конфигурацию Yarn 4
* Создаёт файлы конфигурации и скрипты, подключённые к CLI `twenty`
* Генерирует основные файлы (конфигурацию приложения, роль функций по умолчанию, предустановочную и послеустановочную функции), а также примерные файлы в зависимости от выбранного режима создания каркаса

Сгенерированное с помощью каркаса приложение с режимом по умолчанию `--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/                           # Папка публичных ресурсов (изображения, шрифты и т. д.)
  src/
  ├── application-config.ts           # Обязательный — основная конфигурация приложения
  ├── roles/
  │   └── default-role.ts               # Роль по умолчанию для логических функций
  ├── objects/
  │   └── example-object.ts             # Пример определения пользовательского объекта
  ├── fields/
  │   └── example-field.ts              # Пример определения отдельного поля
  ├── logic-functions/
  │   ├── hello-world.ts                # Пример логической функции
  │   ├── pre-install.ts                # Предустановочная логическая функция
  │   └── post-install.ts               # Послеустановочная логическая функция
  ├── front-components/
  │   └── hello-world.tsx               # Пример фронтенд-компонента
  ├── views/
  │   └── example-view.ts                # Пример определения сохранённого представления
  ├── navigation-menu-items/
  │   └── example-navigation-menu-item.ts # Пример ссылки боковой панели навигации
  ├── skills/
  │   └── example-skill.ts                # Пример определения навыка агента ИИ
  └── agents/
      └── example-agent.ts                # Пример определения агента ИИ
```

С `--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`, который делегирует выполнение локальному CLI `twenty`. Выполните `yarn twenty help`, чтобы вывести список всех доступных команд.
* **.gitignore**: Игнорирует распространённые артефакты, такие как `node_modules`, `.yarn`, `generated/` (типизированный клиент), `dist/`, `build/`, каталоги coverage, файлы журналов и файлы `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Фиксируют и настраивают используемый в проекте инструментарий Yarn 4.
* **.nvmrc**: Фиксирует версию Node.js, ожидаемую проектом.
* **.oxlintrc.json** and **tsconfig.json**: Provide linting and TypeScript configuration for your app's TypeScript sources.
* **README.md**: Короткий README в корне приложения с базовыми инструкциями.
* **public/**: Папка для хранения общедоступных ресурсов (изображений, шрифтов, статических файлов), которые будут отдаваться вашим приложением. Файлы, размещённые здесь, загружаются во время синхронизации и доступны во время выполнения.
* **src/**: Основное место, где вы определяете приложение как код

### Обнаружение сущностей

SDK обнаруживает сущности, разбирая ваши файлы TypeScript в поисках вызовов **`export default define<Entity>({...})`**. Для каждого типа сущности существует соответствующая вспомогательная функция, экспортируемая из `twenty-sdk`:

| Вспомогательная функция            | Тип сущности                                                       |
| ---------------------------------- | ------------------------------------------------------------------ |
| `defineObject()`                   | Определения пользовательских объектов                              |
| `defineLogicFunction()`            | Определения логических функций                                     |
| `definePreInstallLogicFunction()`  | Предустановочная логическая функция (запускается до установки)     |
| `definePostInstallLogicFunction()` | Послеустановочная логическая функция (запускается после установки) |
| `defineFrontComponent()`           | Определения компонентов фронтенда                                  |
| `defineRole()`                     | Определения ролей                                                  |
| `defineField()`                    | Расширения полей для существующих объектов                         |
| `defineView()`                     | Определения сохранённых представлений                              |
| `defineNavigationMenuItem()`       | Определения пунктов меню навигации                                 |
| `defineSkill()`                    | Определения навыков агента ИИ                                      |
| `defineAgent()`                    | Определения агентов ИИ                                             |

<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` автоматически сгенерирует два типизированных клиента API в `node_modules/twenty-sdk/clients`: `CoreApiClient` (для данных рабочего пространства через `/graphql`) и `MetadataApiClient` (для конфигурации рабочего пространства и загрузки файлов через `/metadata`).
* `yarn twenty entity:add` добавит файлы определений сущностей в `src/` для ваших пользовательских объектов, функций, фронтенд-компонентов, ролей, навыков и многого другого.

## Аутентификация

При первом запуске `yarn twenty auth:login` вам будет предложено указать:

* URL API (по умолчанию http://localhost:3000 или текущий профиль рабочего пространства)
* Ключ API

Ваши учётные данные хранятся для каждого пользователя в `~/.twenty/config.json`. Вы можете хранить несколько профилей и переключаться между ними.

### Управление рабочими пространствами

```bash filename="Terminal"
# Войти в интерактивном режиме (рекомендуется)
yarn twenty auth:login

# Войти в профиль конкретного рабочего пространства
yarn twenty auth:login --workspace my-custom-workspace

# Показать список всех настроенных рабочих пространств
yarn twenty auth:list

# Переключить рабочее пространство по умолчанию (в интерактивном режиме)
yarn twenty auth:switch

# Переключиться на определённое рабочее пространство
yarn twenty auth:switch production

# Проверить текущий статус аутентификации
yarn twenty auth:status
```

После переключения рабочего пространства с помощью `yarn twenty auth:switch` все последующие команды по умолчанию будут использовать это рабочее пространство. Вы по-прежнему можете временно переопределить это с помощью `--workspace <name>`.

## Используйте ресурсы SDK (типы и конфигурация)

Пакет twenty-sdk предоставляет типизированные строительные блоки и вспомогательные функции, которые вы используете внутри своего приложения. Ниже — ключевые части, с которыми вы будете работать чаще всего.

### Вспомогательные функции

SDK предоставляет вспомогательные функции для определения сущностей вашего приложения. Как описано в [Обнаружение сущностей](#entity-detection), вы должны использовать `export default define<Entity>({...})`, чтобы ваши сущности были обнаружены:

| Функция                            | Назначение                                                               |
| ---------------------------------- | ------------------------------------------------------------------------ |
| `defineApplication()`              | Настройка метаданных приложения (обязательно, по одному на приложение)   |
| `defineObject()`                   | Определяет пользовательские объекты с полями                             |
| `defineLogicFunction()`            | Определение логических функций с обработчиками                           |
| `definePreInstallLogicFunction()`  | Определяет предустановочную логическую функцию (по одной на приложение)  |
| `definePostInstallLogicFunction()` | Определяет послеустановочную логическую функцию (по одной на приложение) |
| `defineFrontComponent()`           | Определение фронт-компонентов для настраиваемого интерфейса              |
| `defineRole()`                     | Настраивает права роли и доступ к объектам                               |
| `defineField()`                    | Расширение существующих объектов дополнительными полями                  |
| `defineView()`                     | Определяйте сохранённые представления для объектов                       |
| `defineNavigationMenuItem()`       | Определяйте ссылки боковой панели навигации                              |
| `defineSkill()`                    | Определение навыков агента ИИ                                            |
| `defineAgent()`                    | Определяйте ИИ-агентов с системными промптами                            |

Эти функции проверяют вашу конфигурацию на этапе сборки и обеспечивают автодополнение в 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>

### Определение полей для существующих объектов

Используйте `defineField()` для добавления настраиваемых полей к существующим объектам — как стандартным объектам (например, `company`, `person`, `opportunity`), так и пользовательским объектам, определённым другими приложениями. Каждое поле находится в собственном файле и ссылается на целевой объект по его `universalIdentifier`.

Чтобы ссылаться на стандартные объекты, импортируйте `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` из `twenty-sdk`. Эта константа предоставляет стабильные идентификаторы для всех встроенных объектов и их полей:

```typescript
// src/fields/apollo-total-funding.field.ts
import {
  defineField,
  FieldType,
  STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';

export default defineField({
  universalIdentifier: 'c90ae72d-4ddf-4f22-882f-eef98c91e40e',
  objectUniversalIdentifier:
    STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
  type: FieldType.CURRENCY,
  name: 'apolloTotalFunding',
  label: 'Total Funding',
  description: 'Total funding raised by the company',
  icon: 'IconCash',
});
```

Основные моменты:

* `objectUniversalIdentifier` сообщает Twenty, к какому объекту прикрепить поле. Используйте `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<objectName>.universalIdentifier` для стандартных объектов.
* Каждому полю требуется собственный стабильный `universalIdentifier`, а также `name`, `type`, `label` и целевой `objectUniversalIdentifier`.
* Вы можете сгенерировать новые поля с помощью `yarn twenty entity:add`, выбрав опцию поля.
* Для удобства `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` также экспортируется как `STANDARD_OBJECT` — обе ссылки указывают на одну и ту же константу.

Доступные стандартные объекты включают: `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`.

Каждый стандартный объект также предоставляет идентификаторы своих полей. Например, чтобы сослаться на конкретное поле стандартного объекта в разрешениях роли:

```typescript
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier
```

#### Поля связей в существующих объектах

Вы также можете определить поля связей, которые связывают существующие объекты с вашими пользовательскими объектами:

```typescript
// src/fields/people-on-call-recording.field.ts
import { defineField, FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk';
import { CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/call-recording';
import { CALL_RECORDING_ON_PERSON_ID } from 'src/fields/call-recording-on-person.field';

export default defineField({
  universalIdentifier: '4a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d',
  objectUniversalIdentifier:
    CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER,
  type: FieldType.RELATION,
  name: 'person',
  label: 'Person',
  relationTargetObjectMetadataUniversalIdentifier:
    STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
  relationTargetFieldMetadataUniversalIdentifier:
    CALL_RECORDING_ON_PERSON_ID,
  relationType: RelationType.MANY_TO_ONE,
});
```

### Конфигурация приложения (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` — это детерминированные идентификаторы, которыми вы управляете; сгенерируйте их один раз и сохраняйте стабильными между синхронизациями.
* `applicationVariables` становятся переменными окружения для ваших функций (например, `DEFAULT_RECIPIENT_NAME` доступна как `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` должен соответствовать файлу роли (см. ниже).
* Предустановочные и послеустановочные функции автоматически обнаруживаются во время сборки манифеста. См. [Предустановочные функции](#pre-install-functions) и [Послеустановочные функции](#post-install-functions).

#### Роли и разрешения

Приложения могут определять роли, инкапсулирующие права на объекты и действия в вашем рабочем пространстве. Поле `defaultRoleUniversalIdentifier` в `application-config.ts` обозначает роль по умолчанию, используемую логическими функциями вашего приложения.

* Ключ API во время выполнения, подставляемый как `TWENTY_API_KEY`, получается из этой роли функции по умолчанию.
* Типизированный клиент будет ограничен правами, предоставленными этой ролью.
* Следуйте принципу наименьших привилегий: создайте отдельную роль только с теми правами, которые нужны вашим функциям, и укажите её универсальный идентификатор.

##### Роль функции по умолчанию (\*.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/clients';

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**: Публикует вашу функцию по HTTP-пути и методу **под конечной точкой `/s/`**:

> например, `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):** Формат полезной нагрузки триггера маршрута изменился. До 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` вместо прямого доступа к объекту params.
</Warning>

Когда триггер маршрута вызывает вашу логическую функцию, она получает объект `RoutePayload`, соответствующий формату AWS HTTP API v2. Импортируйте тип из `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' }`) |
| `текст`                      | `object \| null`                      | Разобранное тело запроса (JSON)                                                            |
| `isBase64Encoded`            | `логический тип`                      | Является ли тело закодированным в base64                                                   |
| `requestContext.http.method` | `строка`                              | Метод HTTP (GET, POST, PUT, PATCH, DELETE)                                                 |
| `requestContext.http.path`   | `строка`                              | Необработанный путь запроса                                                                |

### Проброс 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()`, следуя тому же шаблону.

### Пометка логической функции как инструмента

Логические функции можно предоставлять как **инструменты** для ИИ-агентов и рабочих процессов. Когда функция помечена как инструмент, она становится доступной для ИИ Twenty и может быть выбрана в качестве шага в автоматизациях рабочих процессов.

Чтобы пометить логическую функцию как инструмент, установите `isTool: true` и укажите `toolInputSchema` для описания ожидаемых входных параметров с помощью [схемы JSON](https://json-schema.org/):

```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';

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`, функция регистрируется как инструмент и становится доступной агентам ИИ и автоматизациям рабочих процессов.
* **`toolInputSchema`** (`object`, необязательно): Объект JSON Schema, который описывает параметры, которые принимает ваша функция. Агенты ИИ используют эту схему, чтобы понять, какие входные данные ожидает инструмент, и проверять корректность вызовов. Если опущено, по умолчанию используется схема `{ type: 'object', properties: {} }` (без параметров).
* Функции с `isTool: false` (или без указания) **не** выставляются как инструменты. Их по-прежнему можно выполнять напрямую или вызывать из других функций, но они не будут отображаться при обнаружении инструментов.
* **Именование инструмента**: При публикации как инструмента имя функции автоматически нормализуется до `logic_function_<name>` (в нижнем регистре, небуквенно-цифровые символы заменяются на подчёркивания). Например, `enrich-company` становится `logic_function_enrich_company`.
* Вы можете комбинировать `isTool` с триггерами — функция может одновременно быть инструментом (вызываемым агентами ИИ) и запускаться событиями (cron, события базы данных, маршруты).

<Note>
**Напишите хорошее описание в поле `description`.** Агенты ИИ опираются на поле `description` функции, чтобы решить, когда использовать инструмент. Чётко опишите, что делает инструмент и когда его следует вызывать.
</Note>

### Фронт-компоненты

Фронт-компоненты позволяют создавать пользовательские компоненты React, которые рендерятся внутри интерфейса Twenty. Используйте `defineFrontComponent()` для определения компонентов со встроенной валидацией:

```typescript
// src/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,
});
```

Основные моменты:

* Фронт-компоненты — это компоненты React, которые рендерятся в изолированных контекстах внутри Twenty.
* Поле `component` ссылается на ваш компонент React.
* Компоненты автоматически собираются и синхронизируются во время `yarn twenty app:dev`.

Вы можете создать новые фронт-компоненты двумя способами:

* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового фронтенд-компонента.
* **Вручную**: Создайте новый файл `.tsx` и используйте `defineFrontComponent()`, следуя тому же шаблону.

#### Где можно использовать фронт-компоненты

Фронт-компоненты могут отображаться в двух местах внутри Twenty:

* **Боковая панель** — фронт-компоненты с интерфейсом открываются в правой боковой панели. Это поведение по умолчанию, когда фронт-компонент запускается из меню команд.
* **Виджеты (дашборды и страницы записей)** — фронт-компоненты можно встраивать как виджеты в макеты страниц. При настройке дашборда или макета страницы записи пользователи могут добавить виджет фронт-компонента.

#### Headless и non-headless

Фронт-компоненты поддерживают два режима отображения, управляемых опцией `isHeadless`:

**Non-headless (по умолчанию)** — компонент отображает видимый интерфейс. При запуске из меню команд он открывается в боковой панели. Это поведение по умолчанию, когда `isHeadless` имеет значение `false` или опущен.

**Headless** — компонент монтируется невидимо в фоновом режиме. Он не открывает боковую панель. Компоненты headless предназначены для действий, которые выполняют логику и затем размонтируются — например, запуск асинхронной задачи, переход на страницу или показ модального окна подтверждения. Они естественно сочетаются с компонентами SDK Command, описанными ниже.

```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',
  },
});
```

#### Добавление элементов меню команд

Чтобы фронт-компонент отображался как элемент в меню команд Twenty, добавьте свойство `command` в `defineFrontComponent()`. Когда пользователи открывают меню команд (Cmd+K / Ctrl+K), элемент появляется и при клике запускает фронт-компонент.

Объект `command` принимает следующие поля:

| Поле                                    | Тип                                              | Описание                                                                                      |
| --------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| `universalIdentifier`                   | `string` (обязательно)                           | Уникальный ID для элемента меню команд                                                        |
| `метка`                                 | `string` (обязательно)                           | Отображаемая метка в меню команд                                                              |
| `иконка`                                | `string` (необязательно)                         | Имя иконки (например, `'IconSparkles'`)                                                       |
| `isPinned`                              | `boolean` (необязательно)                        | Закреплена ли команда в верхней части меню                                                    |
| `availabilityType`                      | `'GLOBAL' \| 'RECORD_SELECTION'` (необязательно) | `GLOBAL` показывает команду везде; `RECORD_SELECTION` показывает её только в контексте записи |
| `availabilityObjectUniversalIdentifier` | `string` (необязательно)                         | Ограничивает команду конкретным типом объекта (например, Person)                              |

Вот пример из приложения записи звонков, которое добавляет команду, ограниченную записями Person:

```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',
  },
});
```

Когда команда синхронизируется, она появляется в меню команд. Если фронт-компонент с интерфейсом, открывается боковая панель с отображаемым внутри компонентом. Если он headless, компонент монтируется в фоновом режиме и выполняет свою логику.

#### Компоненты SDK Command

Пакет `twenty-sdk` предоставляет четыре вспомогательных компонента Command, предназначенных для headless фронт-компонентов. Каждый компонент выполняет действие при монтировании, обрабатывает ошибки, показывая уведомление snackbar, и автоматически размонтирует фронт-компонент по завершении.

Импортируйте их из `twenty-sdk/command`:

* **`Command`** — запускает асинхронный колбэк через проп `execute`.
* **`CommandLink`** — переходит по пути внутри приложения. Пропы: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — открывает модальное окно подтверждения. Если пользователь подтвердит, выполняет колбэк `execute`. Пропы: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — открывает конкретную страницу боковой панели. Пропы: `page`, `pageTitle`, `pageIcon`.

Полный пример headless фронт-компонента, использующего `Command` для запуска действия из меню команд:

```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',
  },
});
```

А также пример с использованием `CommandModal` для запроса подтверждения перед выполнением:

```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',
  },
});
```

#### Контекст выполнения

Каждый фронт-компонент получает контекст выполнения, который предоставляет информацию о том, где и как он запущен. Получайте значения контекста с помощью хуков из `twenty-sdk`:

| Хук                     | Тип возвращаемого значения | Описание                                                                                                                                                                              |
| ----------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useFrontComponentId()` | `строка`                   | Уникальный ID текущего экземпляра фронт-компонента                                                                                                                                    |
| `useRecordId()`         | `string \| null`           | ID текущей записи, когда компонент запускается в контексте записи (например, виджет на странице записи или команда с областью действия записи). В противном случае возвращает `null`. |
| `useUserId()`           | `string \| null`           | ID текущего пользователя                                                                                                                                                              |

```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>
  );
};
```

Контекст реактивный: если окружающая запись изменяется, хуки автоматически возвращают обновлённые значения.

#### Функции API хоста

Фронт-компоненты выполняются в изолированной песочнице, но могут взаимодействовать с интерфейсом Twenty через набор функций, предоставляемых хостом. Импортируйте их напрямую из `twenty-sdk`:

```typescript
import {
  navigate,
  closeSidePanel,
  enqueueSnackbar,
  unmountFrontComponent,
  openSidePanelPage,
  openCommandConfirmationModal,
} from 'twenty-sdk';
```

| Функция                        | Сигнатура                                                | Описание                                                                                                                                                                              |
| ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `навигация`                    | `(to, params?, queryParams?, options?) => Promise<void>` | Переход по типизированному пути приложения внутри Twenty                                                                                                                              |
| `closeSidePanel`               | `() => Promise<void>`                                    | Закрыть боковую панель                                                                                                                                                                |
| `enqueueSnackbar`              | `(params) => Promise<void>`                              | Показать уведомление snackbar. Параметры: `message`, `variant` (`'error'`, `'success'`, `'info'`, `'warning'`), необязательно `duration`, `detailedMessage`, `dedupeKey`              |
| `unmountFrontComponent`        | `() => Promise<void>`                                    | Размонтировать текущий фронт-компонент (используется headless-компонентами для очистки после выполнения)                                                                              |
| `openSidePanelPage`            | `(params) => Promise<void>`                              | Открыть страницу в боковой панели. Параметры: `page`, `pageTitle`, `pageIcon`, `shouldResetSearchState`                                                                               |
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>`             | Показать модальное окно подтверждения и дождаться ответа пользователя. Параметры: `title`, `subtitle`, `confirmButtonText`, `confirmButtonAccent` (`'default'`, `'blue'`, `'danger'`) |

Пример, который использует API хоста для показа snackbar и закрытия боковой панели после завершения действия:

```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()` для определения навыков со встроенной валидацией:

```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` содержит инструкции навыка — это текст, который использует агент ИИ.
* `icon` (необязательно) задаёт значок, отображаемый в UI.
* `description` (необязательно) предоставляет дополнительный контекст о назначении навыка.

Вы можете создать новые навыки двумя способами:

* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового навыка.
* **Вручную**: Создайте новый файл и используйте `defineSkill()`, следуя тому же шаблону.

### Агенты

Agents позволяют определять ИИ-агентов с системными промптами, которые могут работать в вашем рабочем пространстве. Используйте `defineAgent()` для определения агентов со встроенной валидацией:

```typescript
// src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk';

export default defineAgent({
  universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  name: 'sales-assistant',
  label: 'Sales Assistant',
  description: 'An AI agent that helps with sales tasks',
  icon: 'IconRobot',
  prompt: `You are a sales assistant. Help users with:
1. Researching prospects and companies
2. Drafting personalized outreach messages
3. Tracking follow-ups and next steps
4. Analyzing deal pipeline and suggesting actions`,
});
```

Основные моменты:

* `name` — уникальная строка-идентификатор агента (рекомендуется kebab-case).
* `label` — читаемое человеком отображаемое имя, показываемое в UI.
* `prompt` содержит системный промпт — это текст инструкции, который определяет поведение агента.
* `icon` (необязательно) задаёт значок, отображаемый в UI.
* `description` (необязательно) предоставляет дополнительный контекст о назначении агента.

Вы можете создать новых агентов двумя способами:

* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового агента.
* **Вручную**: Создайте новый файл и используйте `defineAgent()`, следуя тому же шаблону.

### Сгенерированные типизированные клиенты

Два типизированных клиента автоматически генерируются с помощью `yarn twenty app:dev` и сохраняются в `node_modules/twenty-sdk/clients` на основе схемы вашего рабочего пространства:

* **`CoreApiClient`** — выполняет запросы к конечной точке `/graphql` для получения данных рабочего пространства
* **`MetadataApiClient`** — выполняет запросы к эндпоинту `/metadata` для получения конфигурации рабочего пространства и загрузки файлов.

```typescript
import { CoreApiClient } from 'twenty-sdk/clients';
import { MetadataApiClient } from 'twenty-sdk/clients';

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 } });
```

`CoreApiClient` автоматически перегенерируется с помощью `yarn twenty app:dev` при изменении ваших объектов или полей. `MetadataApiClient` поставляется готовым в составе SDK.

#### Учётные данные времени выполнения в логических функциях

Когда ваша функция запускается на Twenty, платформа подставляет учётные данные как переменные окружения перед выполнением вашего кода:

* `TWENTY_API_URL`: Базовый URL API Twenty, на который нацелено ваше приложение.
* `TWENTY_API_KEY`: Краткоживущий ключ, ограниченный ролью функции по умолчанию вашего приложения.

Заметки:

* Вам не нужно передавать URL или ключ API сгенерированному клиенту. Он читает `TWENTY_API_URL` и `TWENTY_API_KEY` из process.env во время выполнения.
* Права ключа API определяются ролью, на которую ссылается ваш `application-config.ts` через `defaultRoleUniversalIdentifier`. Это роль по умолчанию, используемая логическими функциями вашего приложения.
* Приложения могут определять роли, чтобы следовать принципу наименьших привилегий. Предоставляйте только те права, которые нужны вашим функциям, затем укажите в `defaultRoleUniversalIdentifier` универсальный идентификатор этой роли.

#### Загрузка файлов

`MetadataApiClient` включает метод `uploadFile` для прикрепления файлов к полям типа «файл» в объектах вашего рабочего пространства. Поскольку стандартные клиенты GraphQL изначально не поддерживают многочастовую загрузку файлов, клиент предоставляет специальный метод, который под капотом реализует [спецификацию многочастных запросов GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).

```typescript
import { MetadataApiClient } from 'twenty-sdk/clients';
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`                         | `строка` | Имя файла (используется для хранения и отображения)                      |
| `contentType`                      | `строка` | Тип MIME файла (по умолчанию `application/octet-stream`, если не указан) |
| `fieldMetadataUniversalIdentifier` | `строка` | Значение `universalIdentifier` для поля типа файла в вашем объекте       |

Основные моменты:

* Метод `uploadFile` доступен в `MetadataApiClient`, потому что мутация загрузки обрабатывается эндпоинтом `/metadata`.
* Он использует `universalIdentifier` поля (а не его идентификатор, специфичный для рабочего пространства), поэтому ваш код загрузки будет работать в любом рабочем пространстве, где установлено ваше приложение — в соответствии с тем, как приложения ссылаются на поля повсюду.
* Возвращаемый `url` — это подписанный URL, который можно использовать для доступа к загруженному файлу.

### Пример Hello World

Ознакомьтесь с минимальным сквозным примером, демонстрирующим объекты, логические функции, фронт-компоненты и несколько триггеров, [здесь](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):

## Сборка вашего приложения

После того как вы разработали приложение с помощью `app:dev`, используйте `app:build`, чтобы скомпилировать его в распространяемый пакет.

```bash filename="Terminal"
# Собрать приложение (результат сохраняется в .twenty/output/)
yarn twenty app:build

# Собрать и создать tarball (.tgz) для распространения
yarn twenty app:build --tarball
```

Процесс сборки:

1. **Разбирает и проверяет манифест** — читает все сущности `defineX()` из ваших исходных файлов и проверяет структуру манифеста.
2. **Компилирует логические функции и фронтенд-компоненты** — упаковывает исходники TypeScript в ESM-файлы `.mjs` с помощью esbuild.
3. **Генерирует контрольные суммы** — вычисляет хэши MD5 для каждого собранного файла, сохраняемые в манифесте как `builtHandlerChecksum` / `builtComponentChecksum`.
4. **Generates the typed API client** — introspects the GraphQL schema and generates typed `CoreApiClient` and `MetadataApiClient` clients.
5. **Runs a TypeScript type check** — runs `tsc --noEmit` to catch type errors before publishing.
6. **Rebuilds with the generated client** — performs a second compilation pass so the generated client types are included.
7. **Optionally creates a tarball** — if `--tarball` is passed, runs `npm pack` to create a `.tgz` file ready for distribution.

The build output in `.twenty/output/` contains:

```text
.twenty/output/
├── manifest.json              # Manifest with checksums for all built files
├── package.json               # Copied from app root
├── yarn.lock                  # Copied from app root
├── src/
│   ├── logic-functions/       # Compiled .mjs logic function files
│   └── front-components/      # Compiled .mjs front component files
├── public/                    # Static assets (if any)
└── my-app-1.0.0.tgz          # Only with --tarball flag
```

| Вариант     | Описание                                                  |
| ----------- | --------------------------------------------------------- |
| `[appPath]` | Path to the app directory (defaults to current directory) |
| `--tarball` | Also pack the output into a `.tgz` tarball                |

## Publishing your app

Use `app:publish` to distribute your app — either to the npm registry or directly to a Twenty server.

### Publish to npm (default)

```bash filename="Terminal"
# Publish to npm (requires npm login)
yarn twenty app:publish

# Publish with a dist-tag (e.g. beta, next)
yarn twenty app:publish --tag beta
```

This builds the app and runs `npm publish` from the `.twenty/output/` directory. The published package can then be installed from the Twenty marketplace by any workspace.

### Publish to a Twenty server

```bash filename="Terminal"
# Publish directly to a Twenty server
yarn twenty app:publish --server https://app.twenty.com
```

This builds the app with a tarball, uploads it to the server via the `uploadAppTarball` GraphQL mutation, and triggers installation in one step. This is useful for private deployments or testing against a specific server.

| Вариант           | Описание                                                  |
| ----------------- | --------------------------------------------------------- |
| `[appPath]`       | Path to the app directory (defaults to current directory) |
| `--server <url>`  | Publish to a Twenty server instead of npm                 |
| `--token <token>` | Authentication token for the target server                |
| `--tag <tag>`     | npm dist-tag (e.g. `beta`, `next`) — only for npm publish |

## Application registration

Before an app can be installed in a workspace, it must be **registered**. A registration is a metadata record that describes where the app comes from and how to authenticate it. This is handled automatically by the CLI in most cases.

### Source types

Each registration has a **source type** that determines how the app's files are resolved during installation:

| Source type | How files are resolved                                                     | Typical use case                       |
| ----------- | -------------------------------------------------------------------------- | -------------------------------------- |
| `LOCAL`     | Files are synced in real-time by the CLI watcher — installation is skipped | Development with `app:dev`             |
| `NPM`       | Fetched from the npm registry via the `sourcePackage` field                | Published apps on npm                  |
| `TARBALL`   | Extracted from an uploaded `.tgz` file stored on the server                | Private apps published with `--server` |

### How registration happens

* **`app:dev`** — automatically creates a `LOCAL` registration the first time you run dev mode against a workspace.
* **`app:publish --server`** — uploads a tarball and creates (or updates) a `TARBALL` registration, then installs the app.
* **npm marketplace** — `NPM` registrations are created when apps are synced from the npm registry into the Twenty marketplace catalog.
* **GraphQL API** — you can also create registrations programmatically via the `createApplicationRegistration` mutation.

### Registration vs installation

**Registration** and **installation** are separate concepts:

* A **registration** (`ApplicationRegistration`) is a global metadata record describing the app: its name, source type, OAuth credentials, and marketplace listing status. It exists independently of any workspace.
* An **installation** (`Application`) is a per-workspace instance. When a user installs an app, Twenty resolves the package from the registration's source, writes the built files to storage, and synchronizes the manifest (creating objects, fields, logic functions, etc.) in that workspace.

One registration can be installed in many workspaces. Each workspace gets its own copy of the app's files and data model.

### OAuth credentials

Each registration includes OAuth credentials (`oAuthClientId` and `oAuthClientSecret`) generated at creation time. These are used by the app to authenticate API requests on behalf of users. The client secret is returned **once** at creation — store it securely. You can rotate it later via the `rotateApplicationRegistrationClientSecret` mutation.

## Ручная настройка (без генератора)

Хотя мы рекомендуем использовать `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 есть необходимые права.
* Не удаётся подключиться к серверу: проверьте URL API и доступность сервера Twenty.
* Types or client missing/outdated: restart `yarn twenty app:dev` — it auto-generates the typed client.
* Режим разработки не синхронизируется: убедитесь, что запущен `yarn twenty app:dev`, и что ваша среда не игнорирует изменения.

Канал помощи в Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
