Skip to main content
Функции логики — это серверные функции на TypeScript, которые выполняются на платформе Twenty. Их можно запускать HTTP-запросами, расписаниями cron или событиями базы данных — а также предоставлять как инструменты для ИИ-агентов.
Каждый файл функции использует defineLogicFunction() для экспорта конфигурации с обработчиком и необязательными триггерами.
src/logic-functions/createPostCard.logic-function.ts
Доступные типы триггеров:
  • httpRoute: Публикует вашу функцию по HTTP-пути и методу. В коде приложения добавляйте префикс /s/ к пути маршрута при использовании RestApiClient; развернутый URL использует внедренную базовую часть TWENTY_FUNCTIONS_URL (или \<server-url>/s, если она не задана).
Чтобы вызвать логическую функцию, запускаемую маршрутом, из фронтенд-компонента (без интерфейса), см. раздел Вызов логической функции.
  • cron: Запускает вашу функцию по расписанию с использованием выражения CRON.
  • databaseEvent: Запускается при событиях жизненного цикла объектов рабочего пространства. Когда операция события — updated, можно указать конкретные поля для отслеживания в массиве updatedFields. Если оставить не заданным или пустым, любое обновление будет вызывать функцию.
например, person.updated, *.created, company.*
  • serverRoute: открывает один HTTP-маршрут в области регистрации. Функция-резолвер (объявленная с помощью serverRouteTriggerSettings) выполняется в рабочем пространстве-владельце и либо возвращает синхронный Response, либо целевое рабочее пространство И логическую функцию для постановки в очередь; в случае постановки в очередь платформа отправляет подтверждение с кодом 202 и запускает эту целевую функцию в очереди worker. См. триггер серверного маршрута.
Вы также можете вручную выполнить функцию с помощью CLI:
Вы можете просматривать логи с помощью:

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

Когда триггер маршрута вызывает вашу логическую функцию, она получает объект RoutePayload, который соответствует формату AWS HTTP API v2. Импортируйте тип RoutePayload из twenty-sdk/logic-function:
Тип RoutePayload имеет следующую структуру:

forwardedRequestHeaders

По умолчанию HTTP-заголовки из входящих запросов не передаются в вашу логическую функцию по соображениям безопасности. Чтобы получить доступ к определённым заголовкам, перечислите их в массиве forwardedRequestHeaders:
В обработчике обращайтесь к переданным заголовкам следующим образом:
Имена заголовков приводятся к нижнему регистру. Обращайтесь к ним, используя ключи в нижнем регистре (например, event.headers['content-type']).

Пользовательский HTTP-ответ

По умолчанию возврат простого значения из обработчика отправляет его обратно как ответ 200 (JSON для объектов, text/plain для строк). Чтобы управлять статус-кодом и заголовками ответа, верните Response из twenty-sdk/logic-function:
По соображениям безопасности заголовки ответа ограничены списком разрешенных заголовков. Любой заголовок, которого нет в этом списке (например, Set-Cookie, CORS-заголовки, такие как Access-Control-Allow-Origin, или пользовательские заголовки X-*), молчаливо удаляется перед отправкой ответа. Разрешенные заголовки ответа:
  • content-type
  • content-language
  • content-disposition
  • cache-control
  • retry-after
Код состояния должен быть допустимым кодом состояния HTTP (в диапазоне от 100 до 599). Имена заголовков ответа сравниваются без учета регистра.

Ошибочные ответы платформы

Помимо собственных ответов вашего обработчика, платформа в некоторых ситуациях отвечает на запросы к маршрутам напрямую: 404, когда маршрут или функция не существует, 403, когда приложение остановлено, 429, когда достигнут предел частоты выполнения, и 422, когда dependencies приложения для продакшена слишком велики для установки — см. раздел ограничения на размер зависимостей.

Триггер серверного маршрута

httpRouteTriggerSettings предоставляет функцию по пути /s/ и определяет рабочее пространство из хоста запроса — это работает, когда у каждого рабочего пространства свой домен. Поставщики сторонних сервисов, однако, отправляют события всех арендаторов на один URL. В этом случае используйте serverRouteTriggerSettings.Триггер состоит из двух частей:
  1. Логическая функция-резолвер — объявляется с помощью serverRouteTriggerSettings — выполняется в вашем рабочем пространстве-владельце (рабочем пространстве, которому принадлежит регистрация приложения). Она анализирует входящий запрос и возвращает либо:
    • { workspaceId, targetLogicFunctionUniversalIdentifier, payload? } — платформа ставит эту целевую функцию в очередь в определённом рабочем пространстве и отправляет подтверждение с кодом 202 { queued: true }, или
    • Response из twenty-sdk/logic-function — платформа возвращает этот HTTP‑ответ синхронно и не ставит целевую функцию в очередь (используйте это для challenge‑рукопожатий, таких как Slack url_verification).
    Резолвер является единой точкой авторизации — URL содержит только идентификатор резолвера. Это предпочтительное место для проверки подписей запросов: резолвер выполняется до любых побочных эффектов, имеет доступ к исходным rawBody и переадресованным заголовкам и может отклонить запрос, не обращаясь к целевой функции.
  2. Целевая логическая функция — обычная логическая функция на рабочее пространство — затем выполняется в определенном рабочем пространстве с полезной нагрузкой, возвращенной резолвером (или с исходной полезной нагрузкой запроса, если резолвер ее не преобразовал). Его возвращаемое значение не видит HTTP‑клиент, когда резолвер выбрал путь постановки в очередь.
src/logic-functions/resolve-server-route.logic-function.ts
src/logic-functions/handle-invoice-paid.logic-function.ts
Конечная точка доступна по адресу:
Идентификатор — это universalIdentifier резолвера из вашего манифеста. Зарегистрируйте этот URL у поставщика.Ответ на проверочный GET. Некоторые поставщики проверяют конечную точку перед отправкой данных на неё: они отправляют GET с проверочным запросом на тот же URL, на который позднее будут POST события. Meta’s WhatsApp Cloud API — один из них. Маршрут сервера отвечает только на POST, если не указано иное, поэтому объявите оба метода:
Проверочный запрос поступает в event.queryStringParameters, а возврат Response отправляет его обратно поставщику в рамках того же запроса. Строковое тело отправляется как text/plain, что и ожидают эти поставщики:
httpMethods заменяет значение по умолчанию, а не дополняет его, поэтому одного ['GET'] достаточно, чтобы маршрут отклонял POST. Поддерживаются только GET и POST. Не задавайте его, если провайдеру не требуется второй метод: резолвер маршрута, объявляющего GET, будет выполняться при обращении любого неаутентифицированного вызывающего клиента, включая поисковые роботы и средства предварительного просмотра ссылок, которые отправляют GET без запроса. На всё, для чего у платформы нет метода, возвращается 405, при этом резолвер вообще не запускается.
Приложение должно быть закреплено и установлено в рабочем пространстве владельца. Поскольку резолвер выполняется в рабочем пространстве владельца (рабочем пространстве, которому принадлежит регистрация приложения), триггер серверного маршрута будет работать только после того, как приложение будет закреплено — то есть у него появится рабочее пространство владельца — и это приложение будет установлено в рабочем пространстве владельца. Пока оба этих условия не выполнены, резолверу негде выполняться, поэтому маршрут не может быть отправлен на обработку. Приложение, которое предоставляет логическую функцию serverRouteTriggerSettings, соответственно, не может быть размещено в маркетплейсе, пока оно не будет закреплено и установлено в рабочем пространстве владельца.
Контракт резолвера. Тип LogicFunctionConfig в SDK обеспечивает это на этапе компиляции: как только вы задаете serverRouteTriggerSettings, ваш обработчик обязан возвращать либо Response, либо { workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object } (или Promise одного из этих вариантов). На пути диспетчеризации workspaceId должен указывать на рабочее пространство, в котором установлена целевая функция, иначе запрос будет отклонен с кодом 404. Результат, не соответствующий ни одному из этих форматов — включая случай, когда идентификаторы не являются UUID, — отклоняется с кодом 502.
Ответственность за проверку подписи лежит на вас — выполняйте проверку в резолвере. Платформа не проверяет подписи запросов. Резолвер — рекомендуемое место для этого: он выполняется первым, имеет доступ к event.rawBody и заголовкам, которые вы указали в forwardedRequestHeaders, и выброшенная ошибка (или любой workspaceId, не соответствующий ожидаемому) останавливает диспетчеризацию до вызова целевой функции. Если вместо этого вы перенесете проверку в целевую функцию, целевая функция должна позаботиться о том, чтобы не потерять rawBody и заголовки — то есть резолвер не должен возвращать payload. Всегда выполняйте проверку до любых побочных эффектов и используйте сравнение с постоянным временем выполнения.
Для подписей запросов большинство провайдеров используют HMAC-SHA256; различаются имя заголовка, кодировка дайджеста и строка подписываемой полезной нагрузки. Несколько примеров:Приведенный выше пример резолвера уже показывает поток GitHub HMAC-SHA256 — адаптируйте имя заголовка, кодировку дайджеста и строку подписываемой полезной нагрузки в соответствии с провайдером, с которым вы интегрируетесь.
Когда резолвер возвращает объект диспетчеризации, маршрут отвечает 202 { queued: true }, а целевая функция запускается в очереди worker — вызывающая сторона никогда не видит задержку, результат или сбои целевой функции (они записываются в журналах выполнения). Это не позволяет повторным отправкам со стороны отправителя усиливать замедление обработки, что и требуется для приёма вебхуков.Когда вызывающей стороне нужно прочитать тело ответа в рамках того же запроса (challenge‑рукопожатия, интерактивные подтверждения), вместо этого верните Response из резолвера. Платформа синхронно эхо‑возвращает этот ответ и пропускает очередь; его заголовки проходят через тот же allow‑list, что и заголовки ответов HTTP‑маршрутов. Делайте функцию-резолвер быстрой — некоторые провайдеры (например, Slack) прерывают запрос через несколько секунд. Поскольку резолвер доступен как публичная конечная точка, защитите его с помощью ограничения частоты запросов (rate limiting) на вашем периметре (edge).

Полезная нагрузка триггера события базы данных

Когда триггер события базы данных вызывает вашу функцию логики, она получает по одному DatabaseEventPayload на каждую изменённую запись. Полезная нагрузка объединяет метаданные о рабочем пространстве-источнике и объекте с событием на уровне записи.
Полезная нагрузка включает:При логическом удалении .deleted имеет формат обновления, поскольку изменяется поле deletedAt записи. Для окончательного удаления используйте .destroyed.
databaseEventTriggerSettings.updatedFields фильтрует, какие события обновления запускают функцию. event.properties.updatedFields указывает, какие поля фактически изменились в текущем событии.
Пример события создания:
Пример события обновления:
Триггер только при обновлении email:
Пример события уничтожения:

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

Каждый обработчик получает второй аргумент, описывающий сам запуск независимо от того, что его вызвало. Если форма первого аргумента меняется в зависимости от триггера, то форма этого не меняется:
userWorkspaceId и workspaceMemberId имеют значение null, когда запуск никто не вызвал: у cron-расписаний, хуков установки и неаутентифицированных вебхуков нет человека, стоящего за ними. Они также имеют значение null, когда у человека нет записи участника рабочего пространства или когда она была удалена.
Контекст сообщает, кто вызвал запуск. То, что запуск может делать, определяется отдельно, ниже.

Чьи права доступа использует вызов

Каждый клиент — CoreApiClient, MetadataApiClient и RestApiClient — действует от имени человека, вызвавшего запуск: его роль пересекается с ролью вашего приложения, поэтому вызов никогда не может сделать больше, чем позволяет любая из этих ролей. Это поведение по умолчанию, и оно означает, что человек никогда не сможет использовать ваше приложение для превышения собственных разрешений.Когда запуск никто не вызвал, нет человека, от имени которого можно действовать, поэтому тот же клиент использует права доступа самого приложения: cron-расписания, хуки установки и неаутентифицированные вебхуки не требуют специальной обработки.Для некоторых вызовов правомерно требуются права доступа самого приложения, даже когда за запуском стоит человек — например, для чтения записей конфигурации вашего приложения или выполнения работы, которую этот человек не мог бы выполнить сам. Создайте для них второй клиент:
Создайте оба клиента один раз в области видимости модуля, и тогда каждый сайт вызова будет указывать используемые права доступа через вызываемый клиент. RestApiClient принимает тот же параметр:
Запуск, который никто не вызвал, действует как ваше приложение. У cron-расписаний, хуков установки и неаутентифицированных вебхуков нет человека, стоящего за ними, поэтому клиент по умолчанию использует права доступа самого приложения и продолжает работать. runAs: 'application' нужен только тогда, когда вам требуются эти права доступа в запуске, который вызвал человек.Проверяйте context.workspaceMemberId, когда функция ведет себя по-разному в зависимости от того, стоит ли за ней кто-то, например чтобы указать автора записи.
Помощники SDK, обращающиеся к собственным ресурсам вашего приложения, всегда используют его права доступа и игнорируют runAs: хранилище ключей и значений, подключения, runAgent, getPublicAssetUrl и списание кредитов.

Предоставление функции в качестве инструмента ИИ или действия рабочего процесса

Функции логики могут быть представлены в двух интерфейсах, у каждого — свой триггер:
  • toolTriggerSettings — делает функцию обнаруживаемой для возможностей ИИ Twenty (чат, MCP, вызов функций). Использует стандартную JSON Schema — формат, который модели LLM изначально понимают.
  • workflowActionTriggerSettings — делает функцию доступной как шаг в визуальном конструкторе рабочих процессов. Использует расширенную InputSchema от Twenty, чтобы конструктор мог отрисовывать корректные редакторы полей, селекторы переменных и подписи.
Функция может выбрать один, другой или оба варианта. Они идут рядом с cronTriggerSettings, databaseEventTriggerSettings и httpRouteTriggerSettings — тот же шаблон, та же структура.
Связь с действием Code рабочего процесса. Встроенное действие Code в конструкторе рабочих процессов само по себе является логической функцией — Twenty создаёт по одной на каждый шаг Code и отображает его редактор встроенным образом. workflowActionTriggerSettings — это способ превратить разовый встроенный код в повторно используемое действие: определите функцию один раз в своём приложении, и она станет доступной для выбора в любом рабочем процессе, вместо копирования и вставки в каждый шаг Code. См. действие Code в руководстве пользователя, чтобы увидеть, как это выглядит для конечного пользователя.
src/logic-functions/enrich-company.logic-function.ts
Основные моменты:
  • Функция может сочетать интерфейсы — объявите и toolTriggerSettings, и workflowActionTriggerSettings, чтобы сделать её доступной и в чате, и в конструкторе рабочих процессов.
  • toolTriggerSettings.inputSchema и workflowActionTriggerSettings.inputSchema — обе необязательны. Если они опущены, конструктор манифеста выводит их из исходного кода обработчика (JSON Schema — для инструмента ИИ, InputSchema от Twenty — для действия рабочего процесса). Укажите её явно, когда вам нужна более богатая типизация — например, с полями, учитывающими FieldMetadataType, такими как CURRENCY или RELATION, для конструктора рабочих процессов, или с полями description, которые может прочитать ИИ-агент:
Чтобы объявить параметры один раз и использовать их в обоих сценариях, определите одну JSON Schema (InputJsonSchema) и преобразуйте её для действия рабочего процесса с помощью jsonSchemaToInputSchema из twenty-sdk/logic-function. toolTriggerSettings.inputSchema принимает JSON Schema напрямую, в то время как workflowActionTriggerSettings.inputSchema ожидает InputSchema Twenty:
Полный пример действия рабочего процесса
workflowActionTriggerSettings принимает четыре поля:Объединяя всё вместе — функция, представленная как действие рабочего процесса, с объявленным выходом, чтобы последующие шаги могли ссылаться на taskId:
src/logic-functions/enrich-company.logic-function.ts
После установки приложения Enrich Company появляется в селекторе действий конструктора рабочих процессов. Конструктор отображает companyName и domain как поля ввода (каждое может получать значения из предыдущих шагов), а последующие шаги могут ссылаться на выходные значения шага taskId и enriched.
Напишите хорошее описание в поле description. Агенты ИИ опираются на поле description функции, чтобы решить, когда использовать инструмент. Чётко опишите, что делает инструмент и когда его следует вызывать.
Вспомогательные функции времени выполнения. twenty-sdk/utils повторно экспортирует небольшие вспомогательные функции времени выполнения, поэтому обработчики никогда не импортируют напрямую из twenty-shared. Например, isDefined(value) возвращает false как для null, так и для undefined — используйте её, чтобы безопасно сузить необязательные входные данные обработчика, которые могут приходить как null во время выполнения, даже если имеют тип T | undefined:
Хуки установки — обработчики до установки, после установки и при удалении — используют тот же рантайм, но объявляются с помощью собственных функций define и не принимают настройки триггеров. См. раздел Install Hooks для definePreInstallLogicFunction, definePostInstallLogicFunction и defineUninstallLogicFunction.

Создание действия на временной шкале.

Используйте createTimelineActivity() для публикации явного события домена из логической функции. Сначала определите событие как тип действия на временной шкале, затем укажите тип и объекты с помощью их стабильных универсальных идентификаторов:
Twenty преобразует универсальные идентификаторы в идентификаторы метаданных, специфичные для установки, проверяет, что тип действия на временной шкале принадлежит вызывающему приложению, и сохраняет снимок его метаданных представления в новом действии. Обязательные входные параметры: timelineActivityTypeUniversalIdentifier, targetObjectUniversalIdentifier и targetRecordId. Также можно указать happensAt, properties и workspaceMemberId. happensAt управляет положением события и отображаемым временем на временной шкале; по умолчанию используется время создания. Чтобы связать с событием другую запись, укажите вместе linkedRecordId и linkedObjectMetadataUniversalIdentifier. Кроме того, можно указать linkedRecordCachedName в качестве резервного варианта отображения исторического имени:
Роли логической функции требуется разрешение на запись в стандартный объект timelineActivity. Не связывайте явные типы событий с action; тип, связанный с действием, уже получает автоматические события аудита и в противном случае создаст дублирующиеся строки.

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

Пакет twenty-client-sdk предоставляет два типизированных клиента GraphQL для взаимодействия с API Twenty из ваших логических функций и фронт-компонентов.
CoreApiClient — основной клиент для запросов и изменений данных рабочего пространства. Он генерируется из схемы вашего рабочего пространства во время yarn twenty dev или yarn twenty dev:build, поэтому полностью типизирован в соответствии с вашими объектами и полями.
Клиент использует синтаксис selection-set: передайте true, чтобы включить поле, используйте __args для аргументов и вкладывайте объекты для отношений. Вы получаете полное автодополнение и проверку типов на основе схемы вашего рабочего пространства.
CoreApiClient генерируется на этапе dev/build. Если вы используете его, не запустив сначала yarn twenty dev или yarn twenty dev:build, он выбросит ошибку. Генерация происходит автоматически — CLI анализирует GraphQL-схему вашего рабочего пространства и создает типизированный клиент с помощью @genql/cli.

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

CoreSchema предоставляет типы TypeScript, соответствующие объектам вашего рабочего пространства — это полезно для типизации состояния компонентов или параметров функций:
MetadataApiClient поставляется в готовом виде вместе с SDK (генерация не требуется). Он выполняет запросы к эндпоинту /metadata для получения конфигурации рабочего пространства, приложений и загрузки файлов. Он принимает тот же параметр runAs, что и CoreApiClient — см. Чьи права доступа использует вызов.

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

MetadataApiClient включает метод uploadFile для прикрепления файлов к полям типа файла:
Основные моменты:
  • Он использует universalIdentifier поля (а не его идентификатор, специфичный для рабочего пространства), поэтому ваш код загрузки будет работать в любом рабочем пространстве, где установлено ваше приложение.
  • Возвращаемый url — это подписанный URL, который можно использовать для доступа к загруженному файлу.
Когда ваш код выполняется на Twenty (логические функции или фронт-компоненты), платформа предоставляет учётные данные в виде переменных окружения:
  • TWENTY_API_URL — базовый URL API Twenty
  • TWENTY_APP_ACCESS_TOKEN — краткоживущий ключ для прав доступа по умолчанию: роль человека пересекается с ролью вашего приложения, когда за запуском кто-то стоит; если никто не стоит — используется собственная роль вашего приложения. В логической функции человек — это тот, кто вызвал запуск, а во фронтенд-компоненте — тот, кто просматривает страницу.
  • TWENTY_APP_APPLICATION_ACCESS_TOKEN — краткоживущий ключ, ограниченный исключительно собственной ролью вашего приложения. Только для логических функций, всегда внедряется в них и используется runAs: 'application'.
Вам не нужно передавать их клиентам — они автоматически считываются из process.env, а в разделе Чьи права доступа использует вызов описан выбор между ними. Собственные разрешения вашего приложения определяются ролью, объявленной с помощью defineApplicationRole() (или указанной через defaultRoleUniversalIdentifier в application-config.ts); запуск, действующий от имени человека, никогда не может превысить ни эту роль, ни его собственную.