用 Twenty App 示例工程掌握全部实体类型:Postcard App 源码深度解读
Postcard App 是 Twenty 仓库中位于 packages/twenty-apps/examples/postcard 的一个完整示例应用,它在一个可运行的工程内同时演示了 Twenty SDK 提供的全部应用实体类型:从自定义对象、独立字段与关系、逻辑函数(含安装钩子)、前端组件,到角色权限、视图、导航、技能与 AI Agent、页面布局等。读完本文,你将掌握 twenty-sdk/define 声明式 API 的用法、各类实体的文件组织方式与配置参数,并可直接以该工程为模板搭建自己的 Twenty App。
一、示例工程概览:它到底"全"在哪里
Postcard App 的官方定位是"A rich example app showcasing all Twenty app entity types",即作为开发者构建自身应用时的对照参考。目录结构(根目录为仓库根 packages/twenty-apps/examples/postcard)本身就构成了一张"实体类型 ↔ 源码文件"的映射表:
| 实体类型 | 示例文件 | 演示内容 |
|---|---|---|
| Application | src/application.config.ts | 应用元数据、应用级变量(applicationVariables)、服务端变量(serverVariables) |
| Objects | src/objects/ | 带内联字段的自定义对象(postCard),含枚举字段默认值、可空时间字段等 |
| Fields | src/fields/ | 独立字段、关系字段(ONE_TO_MANY / MANY_TO_ONE)、扩展标准对象(Person) |
| Logic Functions | src/logic-functions/ | HTTP 路由、数据库事件触发器、cron 调度、工具函数、安装前后钩子等逻辑载体 |
| Front Components | src/components/ | 渲染在 Twenty 界面内部的 React 组件 |
| Roles | src/roles/ | 具备对象级与字段级访问控制的权限角色 |
| Views | src/views/ | 带列配置的保存型表格视图 |
| Navigation | src/navigation-menu-items/ | 指向视图的侧边栏导航链接 |
| Skills | src/skills/ | 为 AI Agent 提供上下文的技能声明 |
| Agents | src/agents/ | 带系统提示词的 AI Agent |
| Page Layouts | src/page-layouts/ | 嵌入前端组件 Widget 的自定义记录页 |
除上述实体外,工程还包含 src/command-menu-items/(命令菜单项,可挂在页面头部)以及 e2e/ 端到端测试,后者对 front component 与共享依赖打包(shared-dependencies-bundle.spec.ts)进行了 Playwright 验证。
二、环境与启动方式
工程通过 package.json 声明运行约束:Node 需为 ^24.5.0、使用 yarn(>=4.0.2,本工程锁定 yarn@4.13.0)、twenty CLI 版本不低于 2.23.0,并声明 keywords 为 twenty-app 以便生态识别。其核心依赖是 twenty-sdk(声明实体)与 twenty-client-sdk(调用 Twenty GraphQL API),前端组件部分共享 react 与 react-dom/client(通过 frontComponentSharedDependencies 字段声明)。
官方 README 给出的启动步骤十分简单:
# 在示例工程目录内执行
yarn install
yarn twenty dev
结合 package.json 中的 scripts 可以补充完整的开发命令集合:yarn twenty 即 twenty CLI(构建/发布/安装应用);yarn lint 使用 oxlint 静态检查;yarn test 运行 vitest 单元与集成测试;yarn test:e2e 运行基于 playwright.config.ts 的端到端测试。src 下还包含 src/tests/schema.integration-test.ts 与 src/tests/global-setup.ts,从命名可以推断其负责对应用 schema 做集成校验并为测试做全局准备。
三、Application:应用元数据与两级变量
每个 Twenty App 的入口是 application.config.ts,Postcard 的示例完整展示了三类声明:
// packages/twenty-apps/examples/postcard/src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './roles/default-function.role';
export const APPLICATION_UNIVERSAL_IDENTIFIER =
'8b2df3cc-23ad-4e1b-87fd-f880d4cefd58';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Postcard App',
description: 'Send postcards easily with Twenty',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Alex Karp',
isSecret: false,
},
},
serverVariables: {
POSTCARD_API_KEY: {
description: 'API key for the postcard printing service',
isSecret: true,
isRequired: true,
},
POSTCARD_SENDER_NAME: {
description: 'Default sender name on postcards',
isSecret: false,
isRequired: false,
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
可以对照逐项解析其参数含义:
universalIdentifier:应用级稳定 UUID,用于跨环境(本地、生产、不同 workspace)识别同一实体;同一对象被重新安装或迁移时不至于因 ID 漂移而失效。本示例统一集中定义在各文件中并导出(如APPLICATION_UNIVERSAL_IDENTIFIER),供关系字段等跨文件引用。displayName/description:应用在 Twenty 市场与安装界面的展示信息。applicationVariables:应用级变量,随应用包分发、可被前端组件与页面逻辑读取。示例中的DEFAULT_RECIPIENT_NAME给出默认收件人姓名,通过isSecret: false表明该值非敏感、可直接渲染。serverVariables:服务端变量,在安装时由管理员填写或由部署方注入。示例区分了两种形态:POSTCARD_API_KEY是isSecret: true(密文存储,仅服务端逻辑函数可读)且isRequired: true(未配置将阻止安装);POSTCARD_SENDER_NAME是可选非敏感值。defaultRoleUniversalIdentifier:把角色模块定义的默认角色接入应用装配。
四、Objects 与 Fields:自定义对象、独立字段与关系
4.1 自定义对象:内联字段声明
src/objects/post-card.object.ts 定义了一张"明信片"业务表,展示了 defineObject 的完整形态:
import { defineObject, FieldType } from 'twenty-sdk/define';
enum PostCardStatus { DRAFT, SENT, DELIVERED, RETURNED }
export const POST_CARD_UNIVERSAL_IDENTIFIER = '54b589ca-eeed-4950-a176-358418b85c05';
export default defineObject({
universalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post card',
labelPlural: 'Post cards',
description: 'A post card object',
icon: 'IconMail',
labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [ /* ... */ ],
});
关键参数:nameSingular/namePlural 是底层 GraphQL/API 名称(决定如 createPostCards mutation 的命名),labelSingular/labelPlural 是界面文案,icon 取 Twenty 图标库名(如 IconMail),labelIdentifierFieldMetadataUniversalIdentifier 指定用于展示记录的"主标签"字段。
其内联字段覆盖了几种典型类型:
- TEXT 字段:
name与content,均带icon(IconAbc)与描述文案; - SELECT 枚举字段
status:通过options数组声明DRAFT/SENT/DELIVERED/RETURNED四个选项,每项含value、label、position(排序位)、color(gray/orange/green 等界面色)以及稳定的id;defaultValue:'DRAFT'``(字符串包一层引号)让新记录默认落在草稿态; - DATE_TIME 可空字段
deliveredAt:isNullable: true且defaultValue: null,对应"寄达时间未知"的语义。
从源码结构看,该对象中所有 universalIdentifier 都被抽成常量导出,这是为了让字段、角色、页面等模块在引用该对象时不依赖字符串硬编码。
4.2 独立字段:给标准对象"加字段"
src/fields/person-can-receive-postcards.field.ts 演示了如何扩展 Twenty 内置标准对象:它通过 STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier 指向标准 Person 对象,然后以 defineField 追加一个 BOOLEAN 字段 canReceivePostcards(默认 true),图标 IconMailbox。这意味着安装本应用后,Twenty 联系人页会自动多出"是否可接收明信片"的开关——这正是 Twenty 应用生态常见的"给核心对象打补丁"模式。
4.3 关系字段:一对多与多对一
关系在 Postcard 中是一对配对声明的字段,分别落在两个对象上:
recipient-on-post-card.field.ts(PostCard 侧):
type: FieldType.RELATION,
name: 'recipient',
relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: POST_CARDS_ON_PERSON_ID, // 对方字段的 ID
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'recipientId',
}
post-cards-on-person.field.ts(Person 侧):
type: FieldType.RELATION,
name: 'postCards',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: RECIPIENT_ON_POST_CARD_ID, // 对方字段的 ID
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
}
要点归纳:
- 关系的双向字段必须互相指向对方的
universalIdentifier(relationTargetFieldMetadataUniversalIdentifier引用对侧),配对形成闭环; - 从关系的"一"方(Person)看是
ONE_TO_MANY(一个人可拥有多张明信片),从"多"方(PostCard)看是MANY_TO_ONE,并在该侧配置onDelete: OnDeleteAction.SET_NULL(删除联系人后明信片的recipient置空而不级联删除)与joinColumnName: 'recipientId'(外键列名); POST_CARDS_ON_PERSON_ID与RECIPIENT_ON_POST_CARD_ID两个 UUID 常量在字段文件间交叉 import,再次体现"ID 常量集中定义、供关系互相引用"的工程习惯。
五、Logic Functions:逻辑载体与安装钩子
src/logic-functions/ 目录存放应用的服务端逻辑。README 提到该类型可承载 HTTP 路由、数据库事件触发器、cron 调度、工具函数与安装钩子,Postcard 具体演示了安装生命周期钩子这一子类:pre-install(迁移前执行)与 post-install(安装后执行)。
src/logic-functions/post-install.ts 的核心是用客户端 SDK 做"安装即播种":
import { CoreApiClient } from 'twenty-client-sdk/core';
import { definePostInstallLogicFunction } from 'twenty-sdk/define';
const SEED_POST_CARDS = [
{ name: 'Greetings from Paris', content: 'Wish you were here! The Eiffel Tower is breathtaking.' },
{ name: 'Hello from Tokyo', content: 'Cherry blossoms are in full bloom. Sending love!' },
];
const handler = async () => {
const client = new CoreApiClient();
await client.mutation({
createPostCards: {
__args: { data: SEED_POST_CARDS as any },
id: true,
},
} as any);
console.log(`Seeded ${SEED_POST_CARDS.length} post cards on install.`);
return {};
};
export default definePostInstallLogicFunction({
universalIdentifier: '852c6321-1563-4396-b7c5-9d370f3d30a9',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 30,
handler,
});
值得注意的实现细节:
- 通过
twenty-client-sdk的CoreApiClient构造 GraphQL mutation,createPostCards正是由 4.1 节namePlural: 'postCards'生成的 API 名称,印证对象命名与 API 命名的对应关系; - 声明式
definePostInstallLogicFunction包装了universalIdentifier、name、description、**timeoutSeconds(30s 超时上限)**与handler,同时handler必须以对象{}返回; - 它演示了"安装后初始化数据 + 打印日志"的典型用途。
与之对应的 src/logic-functions/pre-install.ts 结构几乎一致,只是使用 definePreInstallLogicFunction、超时更短(timeoutSeconds: 10),并在 handler 中接收 params 打印参数后返回空对象。README 中描述的"HTTP routes、database event triggers、cron schedules、tool functions"属于同一实体类型的其他子类,本示例主要聚焦安装钩子以保持可自动演示。
六、Roles:对象级 + 字段级权限模型
src/roles/default-function.role.ts 定义了一个典型的"功能型最小权限角色"(DEFAULT_ROLE_UNIVERSAL_IDENTIFIER),并作为 defaultRoleUniversalIdentifier 挂到应用上。它展示了权限声明的最小化原则与两级粒度:
import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fieldUniversalIdentifier: CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
canReadFieldValue: true,
canUpdateFieldValue: true,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
解析要点:
- 顶层布尔开关(
canRead/Update/SoftDelete/DestroyAllObjectRecords、canUpdateAllSettings)全部置false,角色不持有任何全局权限,属于最安全默认; - 三个
canBeAssignedTo*均置false,表明该角色不直接分发给人/Agent/API Key,而由逻辑函数以"Function Twenty client"身份运行(见 description); objectPermissions将读/写权限精确授予自定义的postCards对象,但软删除与物理删除仍然关闭;fieldPermissions进一步把content字段的读写显式放开,叠加出"对象只读其他字段、content 可编辑"的字段级控制;permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS]通过系统权限标志位(SDK 内置枚举)授予应用管理相关能力。
七、Views、Navigation 与 Command Menu:让数据可被找到
7.1 保存型视图
src/views/all-post-cards.view.ts 定义"所有明信片"的表格视图:包含列配置(字段选择与展示顺序),供侧边栏导航直接跳转。视图是"可保存的查询 + 布局",让不同角色/页面共享同一份记录列表界面。
7.2 侧边栏导航
src/navigation-menu-items/post-cards.navigation-menu-item.ts 把上述视图挂到 Twenty 左侧导航。这样应用安装后,用户即可从侧边栏进入明信片列表——它是"用户如何发现应用数据"的第一入口。
7.3 命令菜单项
src/command-menu-items/ 目录下有两个命令:generate-post-card(生成明信片)与 send-post-cards(寄出明信片)。从命名与第 9 节的页面布局可以看出,命令菜单项可被页面头部 Widget 引用(headerCommandMenuItemUniversalIdentifiers),实现"在记录页直接触发 AI 生成、一键寄出"的操作闭环。
八、Skills 与 Agents:给 AI 提供上下文与人格
Twenty 应用可以声明面向 Agent 的 AI 能力。Postcard 用两个文件构成一组最小可用的"AI 协作对":
- src/skills/postcard-writing.skill.ts:一项 skill,语义上是"写明信片的准则/背景知识",负责为 Agent 提供领域上下文(如何措辞、关注哪些要素);
- src/agents/postcard-drafter.agent.ts:一个 Agent,通过
defineAgent声明:
export default defineAgent({
universalIdentifier: 'b8d4f2a3-9c5e-4f7b-a012-3e4d5c6b7a8f',
name: 'postcard-drafter',
label: 'Postcard Drafter',
icon: 'IconRobot',
description: 'Helps draft postcard messages',
prompt:
'You are a postcard writing assistant. Help users draft concise, warm ' +
'postcard messages. Follow the postcard writing guidelines. Ask for the ' +
'recipient name and the occasion if not provided.',
});
该示例清晰展示了 Agent 实体的声明式写法:name 供系统识别、label/description 供界面展示、icon 选 IconRobot,而 prompt 即系统提示词——它要求 Agent 扮演明信片写作助手、遵循写作准则并在信息不足时主动询问收件人与场合。结合角色一节中 canBeAssignedToAgents 的开关可以推断,Agent 执行实际写库等操作时仍需受角色权限约束,从而把"模型行为"与"数据安全"解耦。
九、Page Layouts 与 Front Components:自定义记录页
9.1 前端组件(Front Components)
src/components/card.front-component.tsx 是渲染在 Twenty 记录页内部的 React 组件,作用是"模拟显示明信片正面"。同目录还包含两个 effect 组件(generate-post-card-component-effect.tsx、send-post-cards-component-effect.tsx),从命名看负责承载"生成/寄出"的交互副作用;card-test-ids.ts 则导出稳定 test id 供 e2e 断言。工程通过 frontComponentSharedDependencies(react、react-dom/client)声明与宿主共享的运行库,避免重复打包——这一点在 e2e/shared-dependencies-bundle.spec.ts 中由 Playwright 做了专门验证。
9.2 页面布局:把组件拼进记录页
src/page-layouts/post-card-record-page.page-layout.ts 通过 definePageLayout 为 postCard 对象定制记录页,是"布局即声明"的典型样例:
export default definePageLayout({
universalIdentifier: 'f2bf4b9f-0485-46f0-89bb-9a65d2b939b1',
name: 'Post Card Record Page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
tabs: [
{
title: 'Fields', position: 0, icon: 'IconList',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [{ title: 'Post Card Fields', type: 'FIELDS', configuration: { configurationType: 'FIELDS' } }],
},
{
title: 'Preview', position: 50, icon: 'IconEye',
widgets: [{
title: 'Card Preview', type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier: CARD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
headerCommandMenuItemUniversalIdentifiers: [
GENERATE_POST_CARD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER,
],
},
}],
},
],
});
其建模逻辑可以拆解为三层:
- 页面级:
type: 'RECORD_PAGE'声明这是对象详情页,通过objectUniversalIdentifier绑定到postCard对象; - Tab 级:
position决定 Tab 顺序,layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST指示字段以纵向列表排布; - Widget 级:第一栏
FIELDS展示对象字段;第二栏Preview用FRONT_COMPONENT类型的 Widget 加载明信片正面组件,并通过headerCommandMenuItemUniversalIdentifiers在头部注入"生成明信片"命令入口——至此,第 7.3 节的命令菜单项与本节组件在 UI 上完成装配。
十、从示例到自己的 App:可复用的工程套路
Postcard App 最大的价值不在于功能本身,而在于它揭示了 Twenty App 的标准工程组织方式。对照该工程清单,开发自己的应用时可以遵循以下套路:
- 一切实体都是声明式文件:用
defineApplication/defineObject/defineField/defineRole/defineAgent/definePageLayout等twenty-sdk/define工厂逐个声明,并按application.config.ts聚合; - ID 集中管理:把所有
universalIdentifier抽成文件内导出的常量,关系字段、页面布局、角色通过常量互相引用,杜绝字符串散落; - 最小权限默认:先像默认 function role 那样关掉全部全局权限,再按对象、按字段精确放开,并用
defaultRoleUniversalIdentifier接入应用; - 扩展标准对象优先于新建对象:给 Person 加
canReceivePostcards这类"属性扩展"直接以独立 field 文件声明即可; - 安装即就绪:用
pre/post-install逻辑函数做迁移前检查和数据播种,让用户装完就能看到效果; - 用测试守住边界:集成测试校验 schema(
schema.integration-test.ts),Playwright 覆盖前端组件与共享依赖(e2e/); - 版本纪律:在 package.json 的
engines声明 Node/yarn/twenty 版本范围并维持twenty >= 2.23.0,这是应用能被正确构建的前提。
十一、结语与延伸阅读
Postcard App 是 Twenty 仓库内 fixtures 之外最能完整展示实体类型组合方式的 example(其余示例如 hello-world、document-generator 各有侧重,而 internal/twenty-partners 则是更大型的内部应用)。如果你希望更系统地掌握声明式 API 的全部入参,可以直接阅读 twenty-sdk 的类型定义;若关注运行时行为,可进一步查看 twenty-apps/fixtures 中的各应用配置与对应测试。以本工程为起点,结合 AGENT.md 与 CLAUDE.md 中的约束说明,你便能快速搭出一个结构完整、权限收敛、具备 AI 能力并且可端到端验证的 Twenty App。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
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