跳转到主要内容
逻辑函数是在 Twenty 平台上运行的服务端 TypeScript 函数。 它们可以由 HTTP 请求、cron 调度或数据库事件触发——也可以作为工具暴露给 AI 智能体。
每个函数文件都使用 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:在 /s/ 端点下通过 HTTP 路径和方法公开你的函数:
例如 path: '/post-card/create' 可在 https://your-twenty-server.com/s/post-card/create 调用
要从(无头)前端组件调用由路由触发的逻辑函数,请参见调用逻辑函数
  • cron:使用 CRON 表达式按计划运行你的函数。
  • databaseEvent:在工作区对象生命周期事件上运行。 当事件操作为 updated 时,可以在 updatedFields 数组中指定要监听的特定字段。 如果未定义或为空,任何更新都会触发该函数。
例如 person.updated*.createdcompany.*
你也可以使用 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

路由触发器负载

当路由触发器调用你的逻辑函数时,它会接收一个遵循 AWS HTTP API v2 格式RoutePayload 对象。 从 twenty-sdk/logic-function 导入 RoutePayload 类型:
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在 JSON 解析之前的原始 UTF-8 请求体。 用于验证 HMAC 风格的 Webhook 签名(例如 GitHub 的 X-Hub-Signature-256、Stripe)。 当运行时未保留它时为 undefined
isBase64Encodedboolean请求体是否为 base64 编码
requestContext.http.methodstringHTTP 方法(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)。 要控制状态码和响应头,请从 twenty-sdk/logic-function 返回一个 Response
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 之间)。 响应头名称的匹配不区分大小写。

数据库事件触发器有效负载

当数据库事件触发器调用你的逻辑函数时,每条被更改的记录都会对应一个 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已更改记录的 ID。
userId, userWorkspaceId, workspaceMemberId当事件由工作区用户触发时的操作者字段。
properties事件的记录数据,根据操作不同,包含 beforeafterdiffupdatedFields
事件记录数据
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,
  };
};

将函数公开为 AI 工具或工作流操作

逻辑函数可以在两个入口对外公开,每个入口都有各自的触发器:
  • toolTriggerSettings — 使该函数可被 Twenty 的 AI 功能(chat、MCP、function calling)发现。 使用标准 JSON Schema,LLM 能够原生理解的格式。
  • workflowActionTriggerSettings — 使该函数在可视化工作流构建器中显示为一个步骤。 使用 Twenty 丰富的 InputSchema,以便构建器可以呈现合适的字段编辑器、变量选择器和标签。
函数可以选择加入其中一个、另一个,或两者都加入。 它们与 cronTriggerSettingsdatabaseEventTriggerSettingshttpRouteTriggerSettings 并列 — 相同的模式、相同的结构。
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: {},
});
关键点:
  • 函数可以混用这些入口 — 同时声明 toolTriggerSettingsworkflowActionTriggerSettings,即可在 chat 和工作流构建器中同时公开它。
  • toolTriggerSettings.inputSchemaworkflowActionTriggerSettings.inputSchema 均为可选。 如果省略,清单构建器会根据处理器源代码进行推断(AI 工具使用 JSON Schema,工作流操作使用 Twenty 的 InputSchema)。 当你需要更丰富的类型时,可显式提供一个 — 例如,在工作流构建器中使用对 FieldMetadataType 友好的字段(如 CURRENCYRELATION),或提供 AI 智能体可读取的 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'],
    },
  },
});
写一个好的 description AI 智能体会依赖该函数的 description 字段来决定何时使用该工具。 明确说明该工具的作用以及应在何时调用。
安装 hooks——预安装和后安装处理程序——共享此运行时,但使用它们自己的 define 函数进行声明,并且不接受触发器设置。 有关 definePreInstallLogicFunctiondefinePostInstallLogicFunction,请参阅 Install Hooks

类型化 API 客户端(twenty-client-sdk

twenty-client-sdk 包提供了两个类型化的 GraphQL 客户端,供你的逻辑函数和前端组件与 Twenty API 交互。
客户端导入端点是否生成?
CoreApiClienttwenty-client-sdk/core/graphql——工作区数据(记录、对象)是,在开发/构建时
MetadataApiClienttwenty-client-sdk/metadata/metadata——工作区配置、文件上传否,已预构建提供
CoreApiClient 是用于查询和变更工作区数据的主要客户端。 它会在执行 yarn twenty devyarn 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,
  },
});
该客户端使用选择集语法:传入 true 以包含某字段,使用 __args 传递参数,并通过嵌套对象表示关系。 你将基于工作区架构获得完整的自动补全和类型检查。
CoreApiClient 在开发/构建时生成。 如果在未先运行 yarn twenty devyarn 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文件名称(用于存储和显示)
contentTypestringMIME 类型(如果省略,默认为 application/octet-stream
fieldMetadataUniversalIdentifierstring你的对象上文件类型字段的 universalIdentifier
关键点:
  • 使用字段的 universalIdentifier(而不是其工作区特定的 ID),因此你的上传代码可在安装了你的应用的任何工作区中运行。
  • 返回的 url 是一个签名 URL,你可以用它来访问已上传的文件。
当你的代码在 Twenty 上运行(逻辑函数或前端组件)时,平台会以环境变量的形式注入凭据:
  • TWENTY_API_URL——Twenty API 的基础 URL
  • TWENTY_APP_ACCESS_TOKEN——作用域限定为你的应用默认函数角色的短期密钥
你无需将这些值传递给客户端——它们会自动从 process.env 读取。 API 密钥的权限由使用 defineApplicationRole() 声明的角色(或在 application-config.ts 中通过 defaultRoleUniversalIdentifier 引用的角色)决定。