Перейти к основному содержанию
Logic functions are server-side TypeScript functions that run on the Twenty platform. They can be triggered by HTTP requests, cron schedules, or database events — and can also be exposed as tools for AI agents.
Каждый файл функции использует defineLogicFunction() для экспорта конфигурации с обработчиком и необязательными триггерами.
src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define';
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';

const handler = async (params: RoutePayload) => {
  const client = new CoreApiClient();
  const name = 'name' in params.queryStringParameters
    ? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
    : 'Hello world';

  const result = await client.mutation({
    createPostCard: {
      __args: { data: { name } },
      id: true,
      name: true,
    },
  });
  return result;
};

export default defineLogicFunction({
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
  name: 'create-new-post-card',
  timeoutSeconds: 2,
  handler,
  httpRouteTriggerSettings: {
    path: '/post-card/create',
    httpMethod: 'GET',
    isAuthRequired: true,
  },
  /*databaseEventTriggerSettings: {
    eventName: 'people.created',
  },*/
  /*cronTriggerSettings: {
    pattern: '0 0 1 1 *',
  },*/
});
Доступные типы триггеров:
  • httpRoute: Публикует вашу функцию по HTTP-пути и методу под конечной точкой /s/:
например, path: '/post-card/create' вызывается по адресу https://your-twenty-server.com/s/post-card/create
  • cron: Запускает вашу функцию по расписанию с использованием выражения CRON.
  • databaseEvent: Запускается при событиях жизненного цикла объектов рабочего пространства. Когда операция события — updated, можно указать конкретные поля для отслеживания в массиве updatedFields. Если оставить не заданным или пустым, любое обновление будет вызывать функцию.
например, person.updated, *.created, company.*
Вы также можете вручную выполнить функцию с помощью CLI:
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
Вы можете просматривать логи с помощью:
yarn twenty logs

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

Когда триггер маршрута вызывает вашу логическую функцию, она получает объект RoutePayload, который соответствует формату AWS HTTP API v2. Импортируйте тип RoutePayload из twenty-sdk:
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';

const handler = async (event: RoutePayload) => {
  const { headers, queryStringParameters, pathParameters, body } = event;
  const { method, path } = event.requestContext.http;

  return { message: 'Success' };
};
Тип RoutePayload имеет следующую структуру:
СвойствоТипОписаниеПример
headersRecord\<string, string | undefined>HTTP-заголовки (только перечисленные в forwardedRequestHeaders)см. раздел ниже
queryStringParametersRecord\<string, string | undefined>Параметры строки запроса (несколько значений объединяются запятыми)/users?ids=1&ids=2&ids=3&name=Alice -> { ids: '1,2,3', name: 'Alice' }
pathParametersRecord\<string, string | undefined>Параметры пути, извлечённые из шаблона маршрута/users/:id, /users/123 -> { id: '123' }
bodyobject | nullРазобранное тело запроса (JSON){ id: 1 } -> { id: 1 }
isBase64EncodedbooleanЯвляется ли тело закодированным в base64
requestContext.http.methodstringМетод HTTP (GET, POST, PUT, PATCH, DELETE)
requestContext.http.pathstringНеобработанный путь запроса

forwardedRequestHeaders

По умолчанию HTTP-заголовки из входящих запросов не передаются в вашу логическую функцию по соображениям безопасности. Чтобы получить доступ к определённым заголовкам, перечислите их в массиве forwardedRequestHeaders:
export default defineLogicFunction({
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
  name: 'webhook-handler',
  handler,
  httpRouteTriggerSettings: {
    path: '/webhook',
    httpMethod: 'POST',
    isAuthRequired: false,
    forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
  },
});
В обработчике обращайтесь к переданным заголовкам следующим образом:
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']).

Предоставление функции как инструмента

Логические функции можно предоставлять как инструменты для ИИ-агентов и рабочих процессов. Когда функция помечена как инструмент, она становится доступной для функций ИИ Twenty и может использоваться в автоматизациях рабочих процессов.Чтобы пометить логическую функцию как инструмент, установите isTool: true:
src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';

const handler = async (params: { companyName: string; domain?: string }) => {
  const client = new CoreApiClient();

  const result = await client.mutation({
    createTask: {
      __args: {
        data: {
          title: `Enrich data for ${params.companyName}`,
          body: `Domain: ${params.domain ?? 'unknown'}`,
        },
      },
      id: true,
    },
  });

  return { taskId: result.createTask.id };
};

export default defineLogicFunction({
  universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
  name: 'enrich-company',
  description: 'Enrich a company record with external data',
  timeoutSeconds: 10,
  handler,
  isTool: true,
});
Основные моменты:
  • Вы можете комбинировать isTool с триггерами — функция может одновременно быть инструментом (вызываемым агентами ИИ) и запускаться событиями.
  • toolInputSchema (необязательно): объект JSON Schema, описывающий параметры, которые принимает ваша функция. Схема вычисляется автоматически на основе статического анализа исходного кода, но вы можете задать её явно:
export default defineLogicFunction({
  ...,
  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'],
  },
});
Напишите хорошее описание в поле description. Агенты ИИ опираются на поле description функции, чтобы решить, когда использовать инструмент. Чётко опишите, что делает инструмент и когда его следует вызывать.
Послеустановочная функция — это функция логики, которая автоматически выполняется после завершения установки вашего приложения в рабочем пространстве. Сервер выполняет её после того, как метаданные приложения синхронизированы и клиент SDK сгенерирован, так что рабочее пространство полностью готово к использованию, а новая схема уже применена. Типичные сценарии использования включают предзаполнение данных по умолчанию, создание начальных записей, настройку параметров рабочего пространства или выделение ресурсов в сторонних сервисах.
src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';

const handler = async (payload: InstallPayload): Promise<void> => {
  console.log('Post install logic function executed successfully!', payload.previousVersion);
};

export default definePostInstallLogicFunction({
  universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
  name: 'post-install',
  description: 'Runs after installation to set up the application.',
  timeoutSeconds: 300,
  shouldRunOnVersionUpgrade: false,
  shouldRunSynchronously: false,
  handler,
});
Вы также можете вручную выполнить постустановочную функцию в любое время с помощью CLI:
yarn twenty exec --postInstall
Основные моменты:
  • Послеустановочные функции используют definePostInstallLogicFunction() — специализированный вариант, который опускает настройки триггеров (cronTriggerSettings, databaseEventTriggerSettings, httpRouteTriggerSettings, isTool).
  • Обработчик получает InstallPayload с { previousVersion?: string; newVersion: string }newVersion — это устанавливаемая версия, а previousVersion — версия, установленная ранее (или undefined при чистой установке). Используйте эти значения, чтобы отличать чистые установки от обновлений и запускать логику миграции, зависящую от версии.
  • Когда запускается хук: по умолчанию только при чистой установке. Передайте shouldRunOnVersionUpgrade: true, если хотите, чтобы он также выполнялся при обновлении приложения с предыдущей версии. Если флаг опущен, по умолчанию он равен false, и при обновлении хук пропускается.
  • Модель выполнения — по умолчанию асинхронно, синхронный режим по выбору: флаг shouldRunSynchronously определяет, как выполняется post-install.
    • shouldRunSynchronously: false (по умолчанию) — хук помещается в очередь сообщений с retryLimit: 3 и выполняется асинхронно в воркере. Ответ на установку возвращается сразу после постановки задания в очередь, поэтому медленный или дающий сбой обработчик не блокирует вызывающую сторону. Воркер выполнит до трёх повторных попыток. Используйте это для длительных задач — наполнение большими наборами данных, вызовы медленных сторонних API, подготовка внешних ресурсов — всего, что может выйти за разумное окно ответа HTTP.
    • shouldRunSynchronously: true — хук выполняется непосредственно в процессе установки (тот же исполнитель, что и для pre-install). Запрос установки блокируется, пока обработчик не завершится, и если он генерирует исключение, вызывающая сторона установки получает POST_INSTALL_ERROR. Автоматических повторов нет. Используйте это для быстрых задач, которые должны завершиться до отправки ответа — например, выдача ошибки валидации пользователю или быстрая настройка, на которую клиент будет полагаться сразу после возврата вызова установки. Имейте в виду, что к моменту запуска post-install миграция метаданных уже применена, поэтому сбой в синхронном режиме не откатывает изменения схемы — он лишь выявляет ошибку.
  • Убедитесь, что ваш обработчик идемпотентен. В асинхронном режиме очередь может выполнить до трёх повторных попыток; в любом режиме хук может запускаться снова при обновлениях, когда shouldRunOnVersionUpgrade: true.
  • Переменные окружения APPLICATION_ID, APP_ACCESS_TOKEN и API_URL доступны внутри обработчика (как и в любой другой логической функции), поэтому вы можете вызывать API Twenty с токеном доступа приложения, ограниченным вашим приложением.
  • Для каждого приложения допускается только одна послеустановочная функция. Сборка манифеста завершится ошибкой, если будет обнаружено более одной такой функции.
  • Параметры функции universalIdentifier, shouldRunOnVersionUpgrade и shouldRunSynchronously автоматически добавляются в манифест приложения в поле postInstallLogicFunction во время сборки — вам не нужно указывать их в defineApplication().
  • Тайм-аут по умолчанию установлен на 300 секунд (5 минут), чтобы позволить выполнять более длительные задачи настройки, такие как инициализация данных.
  • Не выполняется в режиме разработки: когда приложение зарегистрировано локально (через yarn twenty dev), сервер полностью пропускает процесс установки и синхронизирует файлы напрямую через наблюдатель CLI — поэтому post-install никогда не запускается в режиме разработки, независимо от shouldRunSynchronously. Используйте yarn twenty exec --postInstall, чтобы запустить это вручную для запущенного рабочего пространства.
Функция pre-install — это логическая функция, которая автоматически выполняется во время установки, до применения миграции метаданных рабочего пространства. Она использует ту же структуру полезной нагрузки, что и post-install (InstallPayload), но находится раньше в процессе установки, чтобы подготовить состояние, от которого зависит предстоящая миграция, — типичные сценарии включают резервное копирование данных, проверку совместимости с новой схемой или архивирование записей, которые будут реструктурированы или удалены.
src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';

const handler = async (payload: InstallPayload): Promise<void> => {
  console.log('Pre install logic function executed successfully!', payload.previousVersion);
};

export default definePreInstallLogicFunction({
  universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
  name: 'pre-install',
  description: 'Runs before installation to prepare the application.',
  timeoutSeconds: 300,
  shouldRunOnVersionUpgrade: true,
  handler,
});
Вы также можете вручную выполнить предустановочную функцию в любое время с помощью CLI:
yarn twenty exec --preInstall
Основные моменты:
  • Функции pre-install используют definePreInstallLogicFunction() — та же специализированная конфигурация, что и у post-install, только привязанная к другому этапу жизненного цикла.
  • И обработчики pre-, и post-install получают один и тот же тип InstallPayload: { previousVersion?: string; newVersion: string }. Импортируйте его один раз и используйте повторно в обоих хуках.
  • Когда запускается хук: выполняется непосредственно перед миграцией метаданных рабочего пространства (synchronizeFromManifest). Перед выполнением сервер запускает чисто добавочную «урезанную синхронизацию», которая регистрирует в метаданных рабочего пространства pre-install функцию новой версии — ничего больше не затрагивается — а затем выполняет её. Поскольку эта синхронизация только добавляет, объекты, поля и данные предыдущей версии остаются нетронутыми к моменту запуска вашего обработчика: вы можете безопасно читать и сохранять состояние до миграции.
  • Модель выполнения: pre-install выполняется синхронно и блокирует установку. Если обработчик генерирует исключение, установка прерывается до применения каких-либо изменений схемы — рабочее пространство остаётся на предыдущей версии в согласованном состоянии. Это сделано намеренно: pre-install — ваш последний шанс отказать в рискованном обновлении.
  • Как и в случае с post-install, для каждого приложения допускается только одна предустановочная функция. Она автоматически добавляется в манифест приложения в поле preInstallLogicFunction во время сборки.
  • Не выполняется в режиме разработки: как и post-install, процесс установки полностью пропускается для локально зарегистрированных приложений, поэтому pre-install никогда не запускается при yarn twenty dev. Используйте yarn twenty exec --preInstall, чтобы запустить это вручную.
Оба хука являются частью одного и того же процесса установки и получают один и тот же InstallPayload. Разница в том, когда они запускаются относительно миграции метаданных рабочего пространства, и это определяет, к каким данным можно безопасно обращаться.
┌─────────────────────────────────────────────────────────────┐
│ install flow                                                │
│                                                             │
│   upload package → [pre-install] → metadata migration →     │
│   generate SDK → [post-install]                             │
│                                                             │
│                  old schema visible    new schema visible   │
└─────────────────────────────────────────────────────────────┘
Pre-install всегда синхронный (он блокирует установку и может её прервать). Post-install по умолчанию асинхронный — ставится в очередь воркера с автоматическими повторами — но может перейти к синхронному выполнению с shouldRunSynchronously: true. См. аккордеон definePostInstallLogicFunction выше о том, когда использовать каждый режим.Используйте post-install для всего, что требует наличия новой схемы. Это распространённый случай:
  • Наполнение данными по умолчанию (создание начальных записей, стандартных представлений, демонстрационного контента) для недавно добавленных объектов и полей.
  • Регистрация вебхуков в сторонних сервисах теперь, когда у приложения уже есть учётные данные.
  • Вызов вашего собственного API для завершения настройки, зависящей от синхронизированных метаданных.
  • Идемпотентная логика «убедиться, что это существует», которая должна приводить состояние в соответствие при каждом обновлении — совместите с shouldRunOnVersionUpgrade: true.
Пример — создать запись PostCard по умолчанию после установки:
src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';

const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
  if (previousVersion) return; // fresh installs only

  const client = createClient();
  await client.postCard.create({
    data: { title: 'Welcome to Postcard', content: 'Your first card!' },
  });
};

export default definePostInstallLogicFunction({
  universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
  name: 'post-install',
  description: 'Seeds a welcome post card after install.',
  timeoutSeconds: 300,
  shouldRunOnVersionUpgrade: false,
  handler,
});
Используйте pre-install, когда миграция в противном случае уничтожит или повредит существующие данные. Поскольку pre-install работает с предыдущей схемой и при сбое откатывает обновление, это правильное место для всего рискованного:
  • Резервное копирование данных, которые будут удалены или реструктурированы — например, вы удаляете поле в v2 и вам нужно скопировать его значения в другое поле или экспортировать их в хранилище до запуска миграции.
  • Архивирование записей, которые новое ограничение сделает недопустимыми — например, поле становится NOT NULL, и вам сначала нужно удалить или исправить строки со значениями null.
  • Проверка совместимости и отказ от обновления, если текущие данные нельзя корректно мигрировать — выбросьте исключение из обработчика, и установка прервётся без внесения изменений. Это безопаснее, чем обнаружить несовместимость в середине миграции.
  • Переименование или изменение ключей данных перед изменением схемы, которое привело бы к потере связи.
Пример — архивировать записи перед разрушительной миграцией:
src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';

const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
  // Only the 1.x → 2.x upgrade drops the legacy `notes` field.
  if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
    return;
  }

  const client = createClient();
  const legacyRecords = await client.postCard.findMany({
    where: { notes: { isNotNull: true } },
  });

  if (legacyRecords.length === 0) return;

  // Copy legacy `notes` into the new `description` field before the migration
  // drops the `notes` column. If this fails, the upgrade is aborted and the
  // workspace stays on v1 with all data intact.
  await Promise.all(
    legacyRecords.map((record) =>
      client.postCard.update({
        where: { id: record.id },
        data: { description: record.notes },
      }),
    ),
  );
};

export default definePreInstallLogicFunction({
  universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
  name: 'pre-install',
  description: 'Backs up legacy notes into description before the v2 migration.',
  timeoutSeconds: 300,
  shouldRunOnVersionUpgrade: true,
  handler,
});
Общее правило:
You want to…Использовать
Наполнить данными по умолчанию, настроить рабочее пространство, зарегистрировать внешние ресурсыpost-install
Выполнить длительное наполнение или сторонние вызовы, которые не должны блокировать ответ установкиpost-install (по умолчанию — shouldRunSynchronously: false, с повторами воркера)
Выполнить быструю настройку, на которую вызывающая сторона будет полагаться сразу после возврата вызова установкиpost-install с shouldRunSynchronously: true
Прочитать или сохранить данные, которые предстоящая миграция может потерятьpre-install
Отклонить обновление, которое повредит существующие данныеpre-install (бросьте исключение из обработчика)
Выполнять согласование при каждом обновленииpost-install с shouldRunOnVersionUpgrade: true
Сделать одноразовую настройку только при первой установкеpost-install с shouldRunOnVersionUpgrade: false (по умолчанию)
Если сомневаетесь, выбирайте по умолчанию post-install. Обращайтесь к pre-install только тогда, когда сама миграция разрушительна и вам нужно перехватить предыдущее состояние, прежде чем оно исчезнет.

Типизированные клиенты API (twenty-client-sdk)

Пакет twenty-client-sdk предоставляет два типизированных клиента GraphQL для взаимодействия с API Twenty из ваших логических функций и фронт-компонентов.
КлиентИмпортКонечная точкаГенерируется?
CoreApiClienttwenty-client-sdk/core/graphql — данные рабочего пространства (записи, объекты)Да, на этапе dev/build
MetadataApiClienttwenty-client-sdk/metadata/metadata — конфигурация рабочего пространства, загрузка файловНет, поставляется в готовом виде
CoreApiClient — основной клиент для запросов и изменений данных рабочего пространства. Он генерируется из схемы вашего рабочего пространства во время yarn twenty dev или yarn twenty build, поэтому полностью типизирован в соответствии с вашими объектами и полями.
import { CoreApiClient } from 'twenty-client-sdk/core';

const client = new CoreApiClient();

// Query records
const { companies } = await client.query({
  companies: {
    edges: {
      node: {
        id: true,
        name: true,
        domainName: {
          primaryLinkLabel: true,
          primaryLinkUrl: true,
        },
      },
    },
  },
});

// Create a record
const { createCompany } = await client.mutation({
  createCompany: {
    __args: {
      data: {
        name: 'Acme Corp',
      },
    },
    id: true,
    name: true,
  },
});
Клиент использует синтаксис selection-set: передайте true, чтобы включить поле, используйте __args для аргументов и вкладывайте объекты для отношений. Вы получаете полное автодополнение и проверку типов на основе схемы вашего рабочего пространства.
CoreApiClient генерируется на этапе dev/build. Если вы используете его, не запустив сначала yarn twenty dev или yarn twenty build, он выбросит ошибку. Генерация происходит автоматически — CLI анализирует GraphQL-схему вашего рабочего пространства и создает типизированный клиент с помощью @genql/cli.

Использование CoreSchema для аннотаций типов

CoreSchema предоставляет типы TypeScript, соответствующие объектам вашего рабочего пространства — это полезно для типизации состояния компонентов или параметров функций:
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';

const [company, setCompany] = useState<
  Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);

const client = new CoreApiClient();
const result = await client.query({
  company: {
    __args: { filter: { position: { eq: 1 } } },
    id: true,
    name: true,
  },
});
setCompany(result.company);
MetadataApiClient поставляется в готовом виде вместе с SDK (генерация не требуется). Он выполняет запросы к эндпоинту /metadata для получения конфигурации рабочего пространства, приложений и загрузки файлов.
import { MetadataApiClient } from 'twenty-client-sdk/metadata';

const metadataClient = new MetadataApiClient();

// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
  objects: {
    edges: {
      node: {
        id: true,
        nameSingular: true,
        namePlural: true,
        labelSingular: true,
        isCustom: true,
      },
    },
    __args: {
      filter: {},
      paging: { first: 10 },
    },
  },
});

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

MetadataApiClient включает метод uploadFile для прикрепления файлов к полям типа файла:
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';

const metadataClient = new MetadataApiClient();

const fileBuffer = fs.readFileSync('./invoice.pdf');

const uploadedFile = await metadataClient.uploadFile(
  fileBuffer,                                         // file contents as a Buffer
  'invoice.pdf',                                      // filename
  'application/pdf',                                  // MIME type
  '58a0a314-d7ea-4865-9850-7fb84e72f30b',            // field universalIdentifier
);

console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
ПараметрТипОписание
fileBufferBufferНеобработанное содержимое файла
filenamestringИмя файла (используется для хранения и отображения)
contentTypestringТип MIME (по умолчанию application/octet-stream, если не указан)
fieldMetadataUniversalIdentifierstringЗначение universalIdentifier для поля типа файла в вашем объекте
Основные моменты:
  • Он использует universalIdentifier поля (а не его идентификатор, специфичный для рабочего пространства), поэтому ваш код загрузки будет работать в любом рабочем пространстве, где установлено ваше приложение.
  • Возвращаемый url — это подписанный URL, который можно использовать для доступа к загруженному файлу.
Когда ваш код выполняется на Twenty (логические функции или фронт-компоненты), платформа предоставляет учётные данные в виде переменных окружения:
  • TWENTY_API_URL — базовый URL API Twenty
  • TWENTY_APP_ACCESS_TOKEN — краткоживущий ключ, ограниченный ролью функции по умолчанию вашего приложения
Вам не нужно передавать их клиентам — они автоматически читаются из process.env. Права ключа API определяются ролью, указанной в defaultRoleUniversalIdentifier в вашем application-config.ts.