Twenty App 逻辑开发参考指南:Logic Functions、Bulk 记录动作与安装/卸载钩子实战
本篇指南围绕 Twenty 生态中应用(Twenty App)的逻辑层开发展开,系统讲解逻辑函数(Logic Functions)的文件组织与触发/校验规范、以 records: Array<{id}> 为核心的 Bulk 批量记录动作契约、Skills/Agents 行为约束、第三方连接(Connection Providers)的凭据安全要求,以及 definePostInstallLogicFunction 与 defineUninstallLogicFunction 两类安装生命周期钩子的正确用法与幂等约束。阅读后你将掌握为 Twenty 应用编写可维护、可测试、幂等且符合平台约定的逻辑代码与钩子的完整实践方案。
逻辑函数的职责边界:一个文件只做一件事
在 Twenty 应用中,逻辑函数文件(*.logic-function.ts)应当只承载四件事:触发器注册、输入校验、调用外部能力、写回结果。除此之外的一切都应从文件里剥离出来,放到各自相邻的目录中(对应规范详见 app-structure.md):
- 外部 API 调用 →
src/<service>-client/<name>.ts:以 Twenty 后端*-client惯例(如redis-client/、sdk-client/)按服务建一个文件夹,作为外部 SDK 或 HTTP 客户端的封装层; - 响应解析与映射 →
src/utils/<name>.util.ts:纯函数辅助器,保持目录扁平; - 响应与 DTO 类型 →
src/types/<name>.ts:每个文件只放一个 PascalCase 类型; - 跨对象类型共享的结构 → 以对象 kind 为参数实现单一 mapper 工厂,而不是为每个对象写并行函数。
文件命名统一 kebab-case;响应/DTO 类型与解析/映射工具禁止从同一文件同时导出(仅供本工具使用的局部、不导出的类型可以留在 util 文件内),一个文件也禁止出现多个函数导出。
其余硬性规则:
- 在写回或发起远程调用之前先校验必填字段;
- 记录类动作优先采用 bulk 批量输入;若某逻辑函数可从已选记录触发,其规范输入应为
records: Array<{ id: string; ...fields }>,除非用户明确该函数只服务于单条记录; - 使用
records数组内的id承载 Twenty 记录 ID。不要自行添加recordId、companyId这类对象专属 ID,也不要使用扁平单记录载荷——除非用户明确要求单记录契约; - 多记录动作应返回带逐条结果的 bulk 汇总,包含成功、未匹配、失败三类计数;
- 面向作业(job)与重复调用的行为优先保持幂等;
- 读取密钥必须经由 application-config 辅助器,而不是直接读
process.env; - 不要用仅 UI 层的动作掩盖会影响客户利益的副作用。
经验阈值:单个 *.logic-function.ts 或 *.post-install.ts 文件超过 200 行,即视为需要重构的信号。
Bulk 记录动作:默认契约与推荐输入输出形态
Bulk 是那些可能从前端组件(front component)选中记录后触发的动作的默认逻辑函数契约。正确流程是:前端组件收集被选中的记录,只调用一次函数;不要在无单记录明确需求时,让前端组件循环遍历选中记录并反复执行同一逻辑函数。
推荐输入形态:
type BulkInput<TRecord extends { id: string }> = {
records: TRecord[];
};
推荐输出形态(含逐条结果与汇总计数):
type BulkResult = {
ok: boolean;
enrichedCount: number;
noMatchCount: number;
failedCount: number;
results: Array<{
id: string;
status: 'ENRICHED' | 'NO_MATCH' | 'FAILED';
pdlId?: string;
error?: string;
}>;
};
若要把存量“单记录函数”升级为“选中记录动作”,应把旧的扁平输入替换为 records: Array<{ id: string; ...fields }>,除非用户明确要求保持向后兼容。
每个 Bulk 函数都应该抽出并测试以下纯逻辑片段:
- 规范 bulk 形状的输入归一化;
- 逐条记录校验;
- 外部 API 载荷映射;
- 逐条结果映射;
- 汇总计数聚合;
- 错误消息归一化。
这几点直接对应 app-structure.md 中“单元测试应覆盖 src/utils/ 辅助器与 post-install 钩子幂等性”的约定(完整测试指引见 tests.md):正因为逻辑函数文件本身只保留“注册 + 校验 + 调用 + 写回”,其余映射与聚合逻辑都在 util/type/client 层,才使得这些核心分支可以被无副作用的 *.spec.ts 充分覆盖。
Skills 与 Agents:为 AI 行为设定可判定的边界
在 Twenty 应用中引入 AI 行为时,Skills 与 Agents 应明确描述何时适用、需要哪些上下文、期望产出什么。具体约束如下:
- 让触发规则(trigger rules)足够具体、可判定;
- 指令必须锚定在应用内可得的数据与工具之上,不依赖臆想能力;
- 明确说明 Agent 在何种情况下应向用户询问缺失的 workspace 或记录上下文;
- 当可读答案足以满足用户时,不要把裸 ID、时间戳或嵌套 API 输出直接抛给终端用户。
Connection Providers:第三方连接的凭据纪律
接入第三方连接时需守住三条底线:
- 密钥绝不能进入源码或公开资源(对应 app-structure.md 中“Read secrets through the application-config helper, not raw
process.env”的统一约定); - 必需的 OAuth 或 API 开通步骤应写入应用 README 或应用商店 listing 文档;
- 必须验证“凭据过期/缺失”等失败状态的可观测性与处理路径。
Post-Install 钩子:安装期必需记录的种子来源
凡是应用安装后必须存在的记录——默认工作流、视图(views)、角色(roles)或种子参考数据——都应通过 definePostInstallLogicFunction 在安装期创建,绝不应实现为运行时“首次执行”逻辑。
文件位置与其他逻辑函数相邻(惯例为 src/logic-functions/<name>.post-install.ts),kebab-case 命名、单文件单导出。钩子必须幂等:先按稳定标识查找,存在则更新、绝不去重复创建;单记录查询返回 not-found 一律视为“需要创建”。
注意不要写入 Twenty 会在别处计算出的字段(例如 workflow 的 statuses 由版本状态推导,详见 workflows.md)。
dev sync 会跳过安装钩子,需要在本机手动调用验证:
yarn twenty dev:function:exec
重建(rebuild)后再执行一次,用于验证钩子的幂等性——二次运行不应产生重复记录。
从 SDK 源码看,define-post-install-logic-function.ts 会校验 universalIdentifier 与 handler(handler 必须是函数),并把结果交给 createValidationResult 汇总;其配置类型 post-install-logic-function-config.ts 在通用配置之上追加了一个 shouldRunSynchronously?: boolean 选项,用于指示钩子是否需要同步执行完成。对应测试见 define-post-install-logic-function.spec.ts。
Uninstall 钩子:卸载期的尽力而为清理
当应用被卸载时,应使用 defineUninstallLogicFunction 对外部资源做尽力而为(best-effort)的清理:释放 deprovision API 资源、删除残留机器人、撤销 webhook 等。钩子内失败只会被记录日志,绝不会阻塞卸载流程。
文件位置同样与其他逻辑函数相邻(惯例为 src/logic-functions/uninstall.ts),kebab-case、单导出。关键执行时机:卸载钩子在应用元数据、数据与代码被移除之前运行,因此 handler 仍可查询应用自身的对象与记录。handler 接收的载荷为:
type UninstallPayload = {
version?: string;
};
该类型定义见 uninstall-payload-type.ts;define-uninstall-logic-function.ts 同样强制校验 universalIdentifier 与函数类型的 handler,校验逻辑与 post-install 完全对齐;测试见 define-uninstall-logic-function.spec.ts。
与 SDK 定义的对应关系
逻辑函数、安装/卸载钩子本质上是 Twenty SDK 中的 define 类实体。define/index.ts 集中导出了 defineLogicFunction、definePostInstallLogicFunction 与 defineUninstallLogicFunction。以 define-logic-function.ts 为例,其运行期校验会检查:
- 必须有
universalIdentifier; handler必须存在且为函数;- 若配置了
httpRouteTriggerSettings,path与httpMethod必填; serverRouteTriggerSettings.httpMethods仅支持GET与POST;cronTriggerSettings必须有pattern;databaseEventTriggerSettings必须有eventName。
handler 的函数签名由 logic-function-config.ts 定义:(payload, context: LogicFunctionExecutionContext) => any | Promise<any>;当存在 serverRouteTriggerSettings 时,handler 被限定为 ServerRouteResolverHandler,可返回 ServerRouteDispatchResult 入队目标工作区执行,或返回 LogicFunctionHttpResponse 同步应答调用方(适用于需要握手回复的 provider webhook URL)。上述触发器形态说明,逻辑函数既可以由记录/前端事件驱动,也可以由 HTTP 路由、cron 或数据库事件驱动——但无论哪种触发入口,文件职责边界、Bulk 契约与密钥读取规范都保持一致。
小结
一份合格的 Twenty 应用逻辑层代码应当同时满足:逻辑函数文件职责单一(≤200 行为佳)、与外部世界交互的代码全部下沉到 *-client/utils/types 分层、记录动作遵循带逐条结果汇总的 Bulk 契约、AI 行为指令可判定且不泄露原始内部数据、第三方凭据始终经由 application-config 读取,以及安装/卸载钩子以幂等、尽力而为的方式承担生命周期职责。配合 yarn twenty dev:function:exec 的本地调用验证与重建后的重复执行检查,即可在 yarn twenty apply 同步前发现绝大多数逻辑与幂等问题。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00