packages/twenty-docs/developers/extend/apps/data-model.mdx
The twenty-sdk package provides defineEntity functions to declare your app's data model. You must use export default defineEntity({...}) for the SDK to detect your entities. These functions validate your configuration at build time and provide IDE autocompletion and type safety.
Roles encapsulate permissions on your workspace's objects and actions.
import {
defineRole,
PermissionFlag,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
label: 'My new role',
description: 'A role that can be used in your workspace',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
});
Every app must have exactly one defineApplication call that describes:
import { defineApplication } from 'twenty-sdk/define';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
Notes:
universalIdentifier fields are deterministic IDs you own. Generate them once and keep them stable across syncs.applicationVariables become environment variables for your functions and front components (e.g., DEFAULT_RECIPIENT_NAME is available as process.env.DEFAULT_RECIPIENT_NAME).defaultRoleUniversalIdentifier must reference a role defined with defineRole() (see above).defineApplication().If you plan to publish your app, these optional fields control how it appears in the marketplace:
| Field | Description |
|---|---|
author | Author or company name |
category | App category for marketplace filtering |
logoUrl | Path to your app logo (e.g., public/logo.png) |
screenshots | Array of screenshot paths (e.g., public/screenshot-1.png) |
aboutDescription | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's README.md from npm |
websiteUrl | Link to your website |
termsUrl | Link to terms of service |
emailSupport | Support email address |
issueReportUrl | Link to issue tracker |
The defaultRoleUniversalIdentifier in application-config.ts designates the default role used by your app's logic functions and front components. See defineRole above for details.
TWENTY_APP_ACCESS_TOKEN is derived from this role.When you scaffold a new app, the CLI creates a default role file:
import { defineRole, PermissionFlag } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlags: [],
});
This role's universalIdentifier is referenced in application-config.ts as defaultRoleUniversalIdentifier:
Notes:
objectPermissions and fieldPermissions with the objects and fields your functions actually need.permissionFlags control access to platform-level capabilities. Keep them minimal.hello-world/src/roles/function-role.ts.Custom objects describe both schema and behavior for records in your workspace. Use defineObject() to define objects with built-in validation:
import { defineObject, FieldType } from 'twenty-sdk/define';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
name: 'content',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
name: 'recipientName',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
name: 'recipientAddress',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
name: 'status',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
Key points:
defineObject() for built-in validation and better IDE support.universalIdentifier must be unique and stable across deployments.name, type, label, and its own stable universalIdentifier.fields array is optional — you can define objects without custom fields.yarn twenty add, which guides you through naming, fields, and relationships.Use defineField() to add fields to objects you don't own — such as standard Twenty objects (Person, Company, etc.) or objects from other apps. Unlike inline fields in defineObject(), standalone fields require an objectUniversalIdentifier to specify which object they extend:
import { defineField, FieldType } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
Key points:
objectUniversalIdentifier identifies the target object. For standard objects, use STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS exported from twenty-sdk.defineObject(), you do not need objectUniversalIdentifier — it's inherited from the parent object.defineField() is the only way to add fields to objects you didn't create with defineObject().Relations connect objects together. In Twenty, relations are always bidirectional — you define both sides, and each side references the other.
There are two relation types:
| Relation type | Description | Has foreign key? |
|---|---|---|
MANY_TO_ONE | Many records of this object point to one record of the target | Yes (joinColumnName) |
ONE_TO_MANY | One record of this object has many records of the target | No (inverse side) |
Every relation requires two fields that reference each other:
Both fields use FieldType.RELATION and cross-reference each other via relationTargetFieldMetadataUniversalIdentifier.
Suppose a PostCard can be sent to many PostCardRecipient records. Each recipient belongs to exactly one post card.
Step 1: Define the ONE_TO_MANY side on PostCard (the "one" side):
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
Step 2: Define the MANY_TO_ONE side on PostCardRecipient (the "many" side — holds the foreign key):
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
To create a relation with a built-in Twenty object (Person, Company, etc.), use STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS:
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
| Property | Required | Description |
|---|---|---|
type | Yes | Must be FieldType.RELATION |
relationTargetObjectMetadataUniversalIdentifier | Yes | The universalIdentifier of the target object |
relationTargetFieldMetadataUniversalIdentifier | Yes | The universalIdentifier of the matching field on the target object |
universalSettings.relationType | Yes | RelationType.MANY_TO_ONE or RelationType.ONE_TO_MANY |
universalSettings.onDelete | MANY_TO_ONE only | What happens when the referenced record is deleted: CASCADE, SET_NULL, RESTRICT, or NO_ACTION |
universalSettings.joinColumnName | MANY_TO_ONE only | Database column name for the foreign key (e.g., postCardId) |
You can also define relation fields directly inside defineObject(). In that case, omit objectUniversalIdentifier — it's inherited from the parent object:
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
yarn twenty addInstead of creating entity files by hand, you can use the interactive scaffolder:
yarn twenty add
This prompts you to pick an entity type and walks you through the required fields. It generates a ready-to-use file with a stable universalIdentifier and the correct defineEntity() call.
You can also pass the entity type directly to skip the first prompt:
yarn twenty add object
yarn twenty add logicFunction
yarn twenty add frontComponent
| Entity type | Command | Generated file |
|---|---|---|
| Object | yarn twenty add object | src/objects/<name>.ts |
| Field | yarn twenty add field | src/fields/<name>.ts |
| Logic function | yarn twenty add logicFunction | src/logic-functions/<name>.ts |
| Front component | yarn twenty add frontComponent | src/front-components/<name>.tsx |
| Role | yarn twenty add role | src/roles/<name>.ts |
| Skill | yarn twenty add skill | src/skills/<name>.ts |
| Agent | yarn twenty add agent | src/agents/<name>.ts |
| View | yarn twenty add view | src/views/<name>.ts |
| Navigation menu item | yarn twenty add navigationMenuItem | src/navigation-menu-items/<name>.ts |
| Page layout | yarn twenty add pageLayout | src/page-layouts/<name>.ts |
Each entity type has its own template. For example, yarn twenty add object asks for:
invoiceinvoicesInvoice)Invoices)Other entity types have simpler prompts — most only ask for a name.
The field entity type is more detailed: it asks for the field name, label, type (from a list of all available field types like TEXT, NUMBER, SELECT, RELATION, etc.), and the target object's universalIdentifier.
Use the --path flag to place the generated file in a custom location:
yarn twenty add logicFunction --path src/custom-folder