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

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

Приложения позволяют создавать и управлять настройками Twenty в виде кода. Вместо настройки всего через интерфейс вы определяете модель данных и логические функции в коде — так быстрее создавать, поддерживать и развёртывать в нескольких рабочих пространствах. Что вы можете делать уже сегодня:
  • Определяйте пользовательские объекты и поля в виде кода (управляемая модель данных)
  • Создавайте логические функции с пользовательскими триггерами
  • Развёртывайте одно и то же приложение в нескольких рабочих пространствах

Требования

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

Создайте новое приложение с помощью официального генератора, затем выполните аутентификацию и начните разработку:
# Scaffold a new app
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app

# If you don't use yarn@4
corepack enable
yarn install

# Authenticate using your API key (you'll be prompted)
yarn twenty auth:login

# Start dev mode: automatically syncs local changes to your workspace
yarn twenty app:dev
Отсюда вы можете:
# Add a new entity to your application (guided)
yarn twenty entity:add

# Generate a typed Twenty client and workspace entity types
yarn twenty app:generate

# 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"}'

# Uninstall the application from the current workspace
yarn twenty app:uninstall

# Display commands' help
yarn twenty help
Смотрите также: страницы справки CLI для create-twenty-app и twenty-sdk CLI.

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

Когда вы запускаете npx create-twenty-app@latest my-twenty-app, генератор:
  • Копирует минимальное базовое приложение в my-twenty-app/
  • Добавляет локальную зависимость twenty-sdk и конфигурацию Yarn 4
  • Создаёт файлы конфигурации и скрипты, подключённые к CLI twenty
  • Генерирует конфигурацию приложения по умолчанию и роль функции по умолчанию
Свежесгенерированное приложение выглядит так:
my-twenty-app/
  package.json
  yarn.lock
  .gitignore
  .nvmrc
  .yarnrc.yml
  .yarn/
    install-state.gz
  eslint.config.mjs
  tsconfig.json
  README.md
  public/                           # Папка общедоступных ресурсов (изображения, шрифты и т. п.)
  src/
  ├── application-config.ts           # Обязательный — основная конфигурация приложения
  ├── roles/
  │   └── default-role.ts               # Роль по умолчанию для логических функций
  ├── logic-functions/
  │   └── hello-world.ts                # Пример логической функции
  └── front-components/
      └── hello-world.tsx               # Пример фронтенд-компонента
В общих чертах:
  • package.json: Declares the app name, version, engines (Node 24+, Yarn 4), and adds twenty-sdk plus a twenty script that delegates to the local twenty CLI. Run yarn twenty help to list all available commands.
  • .gitignore: Игнорирует распространённые артефакты, такие как node_modules, .yarn, generated/ (типизированный клиент), dist/, build/, каталоги coverage, файлы журналов и файлы .env*.
  • yarn.lock, .yarnrc.yml, .yarn/: Фиксируют и настраивают используемый в проекте инструментарий Yarn 4.
  • .nvmrc: Фиксирует версию Node.js, ожидаемую проектом.
  • eslint.config.mjs и tsconfig.json: Обеспечивают линтинг и конфигурацию TypeScript для исходников вашего приложения на TypeScript.
  • README.md: Короткий README в корне приложения с базовыми инструкциями.
  • public/: Папка для хранения общедоступных ресурсов (изображений, шрифтов, статических файлов), которые будут отдаваться вашим приложением. Файлы, размещённые здесь, загружаются во время синхронизации и доступны во время выполнения.
  • src/: Основное место, где вы определяете приложение как код

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

SDK обнаруживает сущности, разбирая ваши файлы TypeScript в поисках вызовов export default define<Entity>({...}). Для каждого типа сущности существует соответствующая вспомогательная функция, экспортируемая из twenty-sdk:
Вспомогательная функцияТип сущности
defineObject()Определения пользовательских объектов
defineLogicFunction()Определения логических функций
defineFrontComponent()Определения компонентов фронтенда
defineRole()Определения ролей
defineField()Расширения полей для существующих объектов
Имена файлов заданы гибко. Обнаружение сущностей основано на AST — SDK сканирует ваши исходные файлы в поисках шаблона export default define<Entity>({...}). Вы можете организовывать файлы и папки как угодно. Группировка по типу сущности (например, logic-functions/, roles/) — это лишь соглашение для организации кода, а не требование.
Пример обнаруженной сущности:
// 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:generate will create a generated/ folder (typed Twenty client + workspace types).
  • yarn twenty entity:add will add entity definition files under src/ for your custom objects, functions, front components, or roles.

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

The first time you run yarn twenty auth:login, you’ll be prompted for:
  • URL API (по умолчанию http://localhost:3000 или текущий профиль рабочего пространства)
  • Ключ API
Ваши учётные данные хранятся для каждого пользователя в ~/.twenty/config.json. Вы можете хранить несколько профилей и переключаться между ними.

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

# 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
Once you’ve switched workspaces with yarn twenty auth:switch, all subsequent commands will use that workspace by default. Вы по-прежнему можете временно переопределить это с помощью --workspace <name>.

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

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

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

SDK предоставляет вспомогательные функции для определения сущностей вашего приложения. Как описано в Обнаружение сущностей, вы должны использовать export default define<Entity>({...}), чтобы ваши сущности были обнаружены:
ФункцияНазначение
defineApplication()Настройка метаданных приложения (обязательно, по одному на приложение)
defineObject()Определяет пользовательские объекты с полями
defineLogicFunction()Определение логических функций с обработчиками
defineFrontComponent()Определение фронт-компонентов для настраиваемого интерфейса
defineRole()Настраивает права роли и доступ к объектам
defineField()Расширение существующих объектов дополнительными полями
Эти функции проверяют вашу конфигурацию на этапе сборки и обеспечивают автодополнение в IDE и безопасность типов.

Определение объектов

Пользовательские объекты описывают как схему, так и поведение записей в вашем рабочем пространстве. Используйте defineObject() для определения объектов со встроенной валидацией:
// 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 необязателен — вы можете определять объекты без пользовательских полей.
  • You can scaffold new objects using yarn twenty entity:add, which guides you through naming, fields, and relationships.
Базовые поля создаются автоматически. Когда вы определяете пользовательский объект, Twenty автоматически добавляет стандартные поля, такие как name, createdAt, updatedAt, createdBy, position и deletedAt. Вам не нужно определять их в массиве fields — добавляйте только свои пользовательские поля.

Конфигурация приложения (application-config.ts)

У каждого приложения есть единственный файл application-config.ts, который описывает:
  • Что это за приложение: идентификаторы, отображаемое имя и описание.
  • Как запускаются его функции: какую роль они используют для прав доступа.
  • (Необязательно) переменные: пары ключ-значение, предоставляемые вашим функциям как переменные окружения.
Используйте defineApplication() для определения конфигурации вашего приложения:
// 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 должен соответствовать файлу роли (см. ниже).

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

Приложения могут определять роли, инкапсулирующие права на объекты и действия в вашем рабочем пространстве. Поле defaultRoleUniversalIdentifier в application-config.ts обозначает роль по умолчанию, используемую логическими функциями вашего приложения.
  • Ключ API во время выполнения, подставляемый как TWENTY_API_KEY, получается из этой роли функции по умолчанию.
  • Типизированный клиент будет ограничен правами, предоставленными этой ролью.
  • Следуйте принципу наименьших привилегий: создайте отдельную роль только с теми правами, которые нужны вашим функциям, и укажите её универсальный идентификатор.
Роль функции по умолчанию (*.role.ts)
Когда вы генерируете новое приложение, CLI также создаёт файл роли по умолчанию. Используйте defineRole() для определения ролей со встроенной валидацией:
// 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.

Конфигурация логической функции и точка входа

Каждый файл функции использует defineLogicFunction() для экспорта конфигурации с обработчиком и необязательными триггерами.
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';

const handler = async (params: RoutePayload) => {
  const client = new Twenty(); // generated typed client
  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 необязателен. Функции без триггеров можно использовать как вспомогательные, вызываемые другими функциями.
  • Вы можете сочетать несколько типов триггеров в одной функции.

Полезная нагрузка триггера маршрута

Нарушающее совместимость изменение (v1.16, январь 2026): Формат полезной нагрузки триггера маршрута изменился. До v1.16 параметры запроса, параметры пути и тело передавались напрямую в качестве полезной нагрузки. Начиная с v1.16 они вложены в структурированный объект RoutePayload.До v1.16:
const handler = async (params) => {
  const { param1, param2 } = params; // Direct access
};
После v1.16:
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.
Когда триггер маршрута вызывает вашу логическую функцию, она получает объект RoutePayload, соответствующий формату AWS HTTP API v2. Импортируйте тип из twenty-sdk:
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 имеет следующую структуру:
СвойствоТипОписание
headersRecord<string, string | undefined>HTTP-заголовки (только перечисленные в forwardedRequestHeaders)
queryStringParametersRecord<string, string | undefined>Параметры строки запроса (несколько значений объединяются запятыми)
pathParametersRecord<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:
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'],
    },
  ],
});
В обработчике вы сможете получить доступ к этим заголовкам:
const handler = async (event: RoutePayload) => {
  const signature = event.headers['x-webhook-signature'];
  const contentType = event.headers['content-type'];

  // Validate webhook signature...
  return { received: true };
};
Имена заголовков приводятся к нижнему регистру. Обращайтесь к ним, используя ключи в нижнем регистре (например, event.headers['content-type']).
Вы можете создать новые функции двумя способами:
  • Scaffolded: Run yarn twenty entity:add and choose the option to add a new logic function. Это создаёт стартовый файл с обработчиком и конфигурацией.
  • Вручную: Создайте новый файл *.logic-function.ts и используйте defineLogicFunction(), следуя тому же шаблону.

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

Фронт-компоненты позволяют создавать пользовательские компоненты React, которые рендерятся внутри интерфейса Twenty. Используйте defineFrontComponent() для определения компонентов со встроенной валидацией:
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';

const MyWidget = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1>My Custom Widget</h1>
      <p>This is a custom front component for Twenty.</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  name: 'my-widget',
  description: 'A custom widget component',
  component: MyWidget,
});
Основные моменты:
  • Фронт-компоненты — это компоненты React, которые рендерятся в изолированных контекстах внутри Twenty.
  • Используйте суффикс файла *.front-component.tsx для автоматического обнаружения.
  • Поле component ссылается на ваш компонент React.
  • Components are built and synced automatically during yarn twenty app:dev.
Вы можете создать новые фронт-компоненты двумя способами:
  • Scaffolded: Run yarn twenty entity:add and choose the option to add a new front component.
  • Вручную: Создайте новый файл *.front-component.tsx и используйте defineFrontComponent().

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

Run yarn twenty app:generate to create a local typed client in generated/ based on your workspace schema. Используйте его в своих функциях:
import Twenty from '~/generated';

const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
The client is re-generated by yarn twenty app:generate. Запускайте повторно после изменения ваших объектов или при подключении к новому рабочему пространству.

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

Когда ваша функция запускается на 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 универсальный идентификатор этой роли.

Пример Hello World

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

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

Хотя мы рекомендуем использовать create-twenty-app для наилучшего старта, вы также можете настроить проект вручную. Не устанавливайте CLI глобально. Instead, add twenty-sdk as a local dependency and wire a single script in your package.json:
yarn add -D twenty-sdk
Then add a twenty script:
{
  "scripts": {
    "twenty": "twenty"
  }
}
Now you can run all commands via yarn twenty <command>, e.g. yarn twenty app:dev, yarn twenty app:generate, yarn twenty help, etc.

Устранение неполадок

  • Authentication errors: run yarn twenty auth:login and ensure your API key has the required permissions.
  • Не удаётся подключиться к серверу: проверьте URL API и доступность сервера Twenty.
  • Types or client missing/outdated: run yarn twenty app:generate.
  • Dev mode not syncing: ensure yarn twenty app:dev is running and that changes are not ignored by your environment.
Канал помощи в Discord: https://discord.com/channels/1130383047699738754/1130386664812982322