Skip to main content
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.
File organization is up to you. Entity detection is AST-based — the SDK finds export default defineEntity(...) calls regardless of where the file lives. Grouping files by type (e.g., logic-functions/, roles/) is just a convention, not a requirement.
Roles encapsulate permissions on your workspace’s objects and actions.
restricted-company-role.ts
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:
  • Identity: identifiers, display name, and description.
  • Permissions: which role its functions and front components use.
  • (Optional) Variables: key–value pairs exposed to your functions as environment variables.
  • (Optional) Pre-install / post-install functions: logic functions that run before or after installation.
src/application-config.ts
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',
  icon: 'IconWorld',
  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).
  • Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in defineApplication().

Marketplace metadata

If you plan to publish your app, these optional fields control how it appears in the marketplace:
FieldDescription
authorAuthor or company name
categoryApp category for marketplace filtering
logoUrlPath to your app logo (e.g., public/logo.png)
screenshotsArray of screenshot paths (e.g., public/screenshot-1.png)
aboutDescriptionLonger markdown description for the “About” tab. If omitted, the marketplace uses the package’s README.md from npm
websiteUrlLink to your website
termsUrlLink to terms of service
emailSupportSupport email address
issueReportUrlLink to issue tracker

Roles and permissions

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.
  • The runtime token injected as TWENTY_APP_ACCESS_TOKEN is derived from this role.
  • The typed client is restricted to the permissions granted to that role.
  • Follow least-privilege: create a dedicated role with only the permissions your functions need.
Default function role
When you scaffold a new app, the CLI creates a default role file:
src/roles/default-role.ts
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:
  • *.role.ts defines what the role can do.
  • application-config.ts points to that role so your functions inherit its permissions.
Notes:
  • Start from the scaffolded role, then progressively restrict it following least-privilege.
  • Replace objectPermissions and fieldPermissions with the objects and fields your functions actually need.
  • permissionFlags control access to platform-level capabilities. Keep them minimal.
  • See a working example: 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:
postCard.object.ts
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:
  • Use defineObject() for built-in validation and better IDE support.
  • The universalIdentifier must be unique and stable across deployments.
  • Each field requires a name, type, label, and its own stable universalIdentifier.
  • The fields array is optional — you can define objects without custom fields.
  • You can scaffold new objects using yarn twenty add, which guides you through naming, fields, and relationships.
Base fields are created automatically. When you define a custom object, Twenty automatically adds standard fields such as id, name, createdAt, updatedAt, createdBy, updatedBy and deletedAt. You don’t need to define these in your fields array — only add your custom fields. You can override default fields by defining a field with the same name in your fields array, but this is not recommended.
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:
src/fields/company-loyalty-tier.field.ts
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.
  • When defining fields inline in 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 typeDescriptionHas foreign key?
MANY_TO_ONEMany records of this object point to one record of the targetYes (joinColumnName)
ONE_TO_MANYOne record of this object has many records of the targetNo (inverse side)

How relations work

Every relation requires two fields that reference each other:
  1. The MANY_TO_ONE side — lives on the object that holds the foreign key
  2. The ONE_TO_MANY side — lives on the object that owns the collection
Both fields use FieldType.RELATION and cross-reference each other via relationTargetFieldMetadataUniversalIdentifier.

Example: Post Card has many Recipients

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):
src/fields/post-card-recipients-on-post-card.field.ts
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):
src/fields/post-card-on-post-card-recipient.field.ts
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',
  },
});
Circular imports: Both relation fields reference each other’s universalIdentifier. To avoid circular import issues, export your field IDs as named constants from each file, and import them in the other file. The build system resolves these at compile time.

Relating to standard objects

To create a relation with a built-in Twenty object (Person, Company, etc.), use STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS:
src/fields/person-on-self-hosting-user.field.ts
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',
  },
});

Relation field properties

PropertyRequiredDescription
typeYesMust be FieldType.RELATION
relationTargetObjectMetadataUniversalIdentifierYesThe universalIdentifier of the target object
relationTargetFieldMetadataUniversalIdentifierYesThe universalIdentifier of the matching field on the target object
universalSettings.relationTypeYesRelationType.MANY_TO_ONE or RelationType.ONE_TO_MANY
universalSettings.onDeleteMANY_TO_ONE onlyWhat happens when the referenced record is deleted: CASCADE, SET_NULL, RESTRICT, or NO_ACTION
universalSettings.joinColumnNameMANY_TO_ONE onlyDatabase column name for the foreign key (e.g., postCardId)

Inline relation fields in defineObject

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
  ],
});

Scaffolding entities with yarn twenty add

Instead 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

Available entity types

Entity typeCommandGenerated file
Objectyarn twenty add objectsrc/objects/<name>.ts
Fieldyarn twenty add fieldsrc/fields/<name>.ts
Logic functionyarn twenty add logicFunctionsrc/logic-functions/<name>.ts
Front componentyarn twenty add frontComponentsrc/front-components/<name>.tsx
Roleyarn twenty add rolesrc/roles/<name>.ts
Skillyarn twenty add skillsrc/skills/<name>.ts
Agentyarn twenty add agentsrc/agents/<name>.ts
Viewyarn twenty add viewsrc/views/<name>.ts
Navigation menu itemyarn twenty add navigationMenuItemsrc/navigation-menu-items/<name>.ts
Page layoutyarn twenty add pageLayoutsrc/page-layouts/<name>.ts

What the scaffolder generates

Each entity type has its own template. For example, yarn twenty add object asks for:
  1. Name (singular) — e.g., invoice
  2. Name (plural) — e.g., invoices
  3. Label (singular) — auto-populated from the name (e.g., Invoice)
  4. Label (plural) — auto-populated (e.g., Invoices)
  5. Create a view and navigation item? — if you answer yes, the scaffolder also generates a matching view and sidebar link for the new object.
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.

Custom output path

Use the --path flag to place the generated file in a custom location:
yarn twenty add logicFunction --path src/custom-folder