Перейти к основному содержанию
Функции логики — это серверные функции на TypeScript, которые выполняются на платформе Twenty. Их можно запускать HTTP-запросами, расписаниями cron или событиями базы данных — а также предоставлять как инструменты для ИИ-агентов.
Каждый файл функции использует defineLogicFunction() для экспорта конфигурации с обработчиком и необязательными триггерами.
src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';

const handler = async (params: RoutePayload) => {
  const client = new CoreApiClient();
  const body = (params.body ?? {}) as { name?: string };
  const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? '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: 'POST',
    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.*
  • serverWebhook: получает входящие вебхуки от стороннего сервиса (Stripe, GitHub, Svix, …) на единственной конечной точке в области регистрации и определяет целевое рабочее пространство из полезной нагрузки. См. триггер серверного вебхука.
Вы также можете вручную выполнить функцию с помощью CLI:
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
Вы можете просматривать логи с помощью:
yarn twenty dev:function:logs

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

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

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 }
rawBodystring | undefinedИсходное тело запроса в кодировке UTF-8, до разбора JSON. Полезно для проверки подписей вебхуков в стиле HMAC (например, X-Hub-Signature-256 от GitHub, Stripe). undefined, если среда выполнения не сохранила его.
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']).

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

По умолчанию возврат простого значения из обработчика отправляет его обратно как ответ 200 (JSON для объектов, text/plain для строк). Чтобы управлять статус-кодом и заголовками ответа, верните Response из twenty-sdk/logic-function:
import { Response } from 'twenty-sdk/logic-function';

const handler = async (event: RoutePayload) => {
  return new Response('<h1>Hello</h1>', {
    status: 201,
    headers: { 'content-type': 'text/html' },
  });
};
По соображениям безопасности заголовки ответа ограничены списком разрешенных заголовков. Любой заголовок, которого нет в этом списке (например, Set-Cookie, CORS-заголовки, такие как Access-Control-Allow-Origin, или пользовательские заголовки X-*), молчаливо удаляется перед отправкой ответа. Разрешенные заголовки ответа:
  • content-type
  • content-language
  • content-disposition
  • cache-control
  • retry-after
Код состояния должен быть допустимым кодом состояния HTTP (в диапазоне от 100 до 599). Имена заголовков ответа сравниваются без учета регистра.

Серверный триггер вебхука

httpRouteTriggerSettings предоставляет функцию по пути /s/ и определяет рабочее пространство из хоста запроса — это работает, когда у каждого рабочего пространства свой домен. Поставщики сторонних сервисов, однако, отправляют события всех арендаторов на один URL вебхука. В этом случае используйте serverWebhookTriggerSettings: функция доступна на конечной точке в области регистрации, а рабочее пространство определяется из полезной нагрузки.
src/logic-functions/handle-provider-webhook.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
import { Response } from 'twenty-sdk/logic-function';

const handler = async (event: RoutePayload) => {
  // Verify the signature yourself before doing anything (see below).
  // Return a non-2xx Response to make the provider retry.
  return { received: true };
};

export default defineLogicFunction({
  universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
  name: 'handle-provider-webhook',
  handler,
  serverWebhookTriggerSettings: {
    workspaceIdResolver: { source: 'body', path: 'metadata.twentyWorkspaceId' },
    forwardedRequestHeaders: ['webhook-id', 'webhook-timestamp', 'webhook-signature'],
  },
});
Функция доступна по адресу:
POST https://your-twenty-server.com/webhooks/server/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier
Оба идентификатора — это universalIdentifier из вашего манифеста: регистрации приложения и этой логической функции. Зарегистрируйте этот URL у поставщика.Определение рабочего пространства. Поскольку одна конечная точка обслуживает все рабочие пространства, ваша интеграция должна поместить целевой workspaceId в передаваемые данные, а workspaceIdResolver.{ source, path } указывает платформе, откуда его прочитать:
ПолеЗначенияЗаметки
sourcebody | query | headerbody читает разобранный JSON. query — самый универсальный вариант: вы обычно контролируете URL обратного вызова, который регистрируете, поэтому добавьте ?twentyWorkspaceId=….
pathточечный путь, например metadata.twentyWorkspaceIdОграничено сегментами из буквенно-цифровых символов / _ / -; ключи прототипа отклоняются.
Определённое значение должно быть действительным UUID рабочего пространства, и ваше приложение должно быть установлено в этом рабочем пространстве, иначе запрос будет отклонён до запуска функции.
Проверка подписи — ваша ответственность. Платформа не проверяет подписи вебхуков для этого триггера — она только определяет рабочее пространство и запускает вашу функцию. Ваш обработчик должен самостоятельно проверить подпись, используя event.rawBody и заголовки, перечисленные в forwardedRequestHeaders, сравнивая с секретом, хранящимся как серверная/приложенческая переменная. Всегда выполняйте проверку до любых побочных эффектов и используйте сравнение с постоянным временем выполнения.
Большинство провайдеров подписывают с помощью HMAC-SHA256; различаются имя заголовка, кодировка дайджеста и строка подписываемой полезной нагрузки. Несколько примеров:
ПровайдерЗаголовки для пересылкиПодписываемая строкаДайджест
Svix (Recall, Resend, Clerk)webhook-id, webhook-timestamp, webhook-signature{id}.{timestamp}.{rawBody}base64 (секрет в формате base64 после удаления префикса whsec_)
Stripestripe-signature{timestamp}.{rawBody}hex
GitHubx-hub-signature-256{rawBody}hex (с префиксом sha256=)
Shopifyx-shopify-hmac-sha256{rawBody}base64
Слэкx-slack-signature, x-slack-request-timestampv0:{timestamp}:{rawBody}hex (с префиксом v0=)
import { createHmac, timingSafeEqual } from 'crypto';

const handler = async (event: RoutePayload) => {
  const signature = event.headers['x-hub-signature-256'] ?? '';
  const expected =
    'sha256=' +
    createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '')
      .update(event.rawBody ?? '')
      .digest('hex');

  const a = Buffer.from(signature);
  const b = Buffer.from(expected);

  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return new Response({ error: 'invalid signature' }, { status: 401 });
  }

  // ...handle the verified event
  return { received: true };
};
Функция выполняется синхронно, и возвращаемое вами значение становится HTTP-ответом, поэтому провайдеры видят ваш статус-код и могут повторить запрос при не-2xx коде. Делайте обработчики быстрыми — некоторые провайдеры (например, Slack) прерывают запрос через несколько секунд. Поскольку функция выполняется до проверки подписи, защитите эту конечную точку ограничением частоты на периметре (edge).

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

Когда триггер события базы данных вызывает вашу функцию логики, она получает по одному DatabaseEventPayload на каждую изменённую запись. Полезная нагрузка объединяет метаданные о рабочем пространстве-источнике и объекте с событием на уровне записи.
import type {
  DatabaseEventPayload,
  ObjectRecordCreateEvent,
  ObjectRecordDestroyEvent,
  ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';

type Person = {
  id: string;
  emails?: { primaryEmail?: string };
};
Полезная нагрузка включает:
СвойствоОписание
nameИмя события, например person.updated.
workspaceIdРабочее пространство, в котором произошло событие.
objectMetadataМетаданные для объекта, который изменился.
recordIdИдентификатор измененной записи.
userId, userWorkspaceId, workspaceMemberIdПоля инициатора, если событие было вызвано пользователем рабочего пространства.
propertiesДанные записи для события с before, after, diff и updatedFields в зависимости от операции.
СобытиеДанные записи
person.createdevent.properties.after
person.updatedevent.properties.before, event.properties.after, event.properties.diff, event.properties.updatedFields
person.destroyedevent.properties.before
При логическом удалении .deleted имеет формат обновления, поскольку изменяется поле deletedAt записи. Для окончательного удаления используйте .destroyed.
databaseEventTriggerSettings.updatedFields фильтрует, какие события обновления запускают функцию. event.properties.updatedFields указывает, какие поля фактически изменились в текущем событии.
Пример события создания:
type PersonCreatedEvent = DatabaseEventPayload<
  ObjectRecordCreateEvent<Person>
>;

const handler = async (event: PersonCreatedEvent) => {
  const person = event.properties.after;

  return {
    personId: event.recordId,
    email: person.emails?.primaryEmail,
  };
};
Пример события обновления:
type PersonUpdatedEvent = DatabaseEventPayload<
  ObjectRecordUpdateEvent<Person>
>;

const handler = async (event: PersonUpdatedEvent) => {
  const { before, after, diff, updatedFields } = event.properties;

  return {
    personId: event.recordId,
    updatedFields,
    previousEmail: before.emails?.primaryEmail,
    currentEmail: after.emails?.primaryEmail,
    emailDiff: diff.emails,
  };
};
Триггер только при обновлении email:
export default defineLogicFunction({
  ...,
  databaseEventTriggerSettings: {
    eventName: 'person.updated',
    updatedFields: ['emails'],
  },
});
Пример события уничтожения:
type PersonDestroyedEvent = DatabaseEventPayload<
  ObjectRecordDestroyEvent<Person>
>;

const handler = async (event: PersonDestroyedEvent) => {
  const personBeforeDestroy = event.properties.before;

  return {
    personId: event.recordId,
    email: personBeforeDestroy.emails?.primaryEmail,
  };
};

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

Функции логики могут быть представлены в двух интерфейсах, у каждого — свой триггер:
  • toolTriggerSettings — делает функцию обнаруживаемой для возможностей ИИ Twenty (чат, MCP, вызов функций). Использует стандартную JSON Schema — формат, который модели LLM изначально понимают.
  • workflowActionTriggerSettings — делает функцию доступной как шаг в визуальном конструкторе рабочих процессов. Использует расширенную InputSchema от Twenty, чтобы конструктор мог отрисовывать корректные редакторы полей, селекторы переменных и подписи.
Функция может выбрать один, другой или оба варианта. Они идут рядом с cronTriggerSettings, databaseEventTriggerSettings и httpRouteTriggerSettings — тот же шаблон, та же структура.
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,
  toolTriggerSettings: {},
});
Основные моменты:
  • Функция может сочетать интерфейсы — объявите и toolTriggerSettings, и workflowActionTriggerSettings, чтобы сделать её доступной и в чате, и в конструкторе рабочих процессов.
  • toolTriggerSettings.inputSchema и workflowActionTriggerSettings.inputSchema — обе необязательны. Если они опущены, конструктор манифеста выводит их из исходного кода обработчика (JSON Schema — для инструмента ИИ, InputSchema от Twenty — для действия рабочего процесса). Укажите её явно, когда вам нужна более богатая типизация — например, с полями, учитывающими FieldMetadataType, такими как CURRENCY или RELATION, для конструктора рабочих процессов, или с полями description, которые может прочитать ИИ-агент:
export default defineLogicFunction({
  ...,
  toolTriggerSettings: {
    inputSchema: {
      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'],
    },
  },
});
Чтобы объявить параметры один раз и использовать их в обоих сценариях, определите одну JSON Schema (InputJsonSchema) и преобразуйте её для действия рабочего процесса с помощью jsonSchemaToInputSchema из twenty-sdk/logic-function. toolTriggerSettings.inputSchema принимает JSON Schema напрямую, в то время как workflowActionTriggerSettings.inputSchema ожидает InputSchema Twenty:
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';

const inputSchema: InputJsonSchema = {
  type: 'object',
  properties: {
    companyName: { type: 'string', label: 'Company name' },
    domain: { type: 'string', label: 'Domain' },
  },
  required: ['companyName'],
};

export default defineLogicFunction({
  ...,
  toolTriggerSettings: { inputSchema },
  workflowActionTriggerSettings: {
    label: 'Enrich Company',
    icon: 'IconBuilding',
    inputSchema: jsonSchemaToInputSchema(inputSchema),
  },
});
Напишите хорошее описание в поле description. Агенты ИИ опираются на поле description функции, чтобы решить, когда использовать инструмент. Чётко опишите, что делает инструмент и когда его следует вызывать.
Вспомогательные функции времени выполнения. twenty-sdk/utils повторно экспортирует небольшие вспомогательные функции времени выполнения, поэтому обработчики никогда не импортируют напрямую из twenty-shared. Например, isDefined(value) возвращает false как для null, так и для undefined — используйте её, чтобы безопасно сузить необязательные входные данные обработчика, которые могут приходить как null во время выполнения, даже если имеют тип T | undefined:
import { isDefined } from 'twenty-sdk/utils';

const handler = async (params: { parentMessageId?: string }) => {
  if (isDefined(params.parentMessageId)) {
    // params.parentMessageId is narrowed to string here
  }
};
Хуки установки — обработчики до установки и после установки — используют тот же рантайм, но объявляются с помощью собственных функций define и не принимают настройки триггеров. См. раздел Install Hooks для definePreInstallLogicFunction и definePostInstallLogicFunction.

Типизированные клиенты 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 dev: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 dev: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 определяются ролью, объявленной с помощью defineApplicationRole() (или указанной через defaultRoleUniversalIdentifier в application-config.ts).