Skip to main content

Overview

Custom objects are new record types your app adds to a workspace — Post Card, Invoice, Subscription, anything specific to your domain. Each object declares its schema (fields, relations, default values) and a stable universal identifier that survives across syncs and deploys.
src/objects/post-card.object.ts

Key points

  • 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.
  • openRecordIn sets where records of this object open when clicked: ObjectOpenRecordIn.USER_CHOICE (the default, following each workspace member’s own preference from Settings → Experience), ObjectOpenRecordIn.SIDE_PANEL, or ObjectOpenRecordIn.RECORD_PAGE. Pin it to RECORD_PAGE for records that need a full page to be usable, the way workflows and dashboards do, or to SIDE_PANEL for records that only make sense as a quick panel, the way calendar events do.
  • disabledObjectPermissionActions lists object permissions that cannot be granted on the object: any of 'canReadObjectRecords', 'canUpdateObjectRecords', 'canSoftDeleteObjectRecords' and 'canDestroyObjectRecords', the same flags a role sets per object. Users, API keys and agents never get these actions on the object, whatever their role grants, and saving an object permission that grants one of them fails. Your app’s own logic functions keep them. For example, ['canUpdateObjectRecords', 'canSoftDeleteObjectRecords', 'canDestroyObjectRecords'] makes an object only your app can write, for configuration-like records that grant behavior.
  • color sets the accent color of the object in the UI. Omit it and Twenty picks one for the object.
  • imageIdentifierFieldMetadataUniversalIdentifier names the field whose value is shown as the record avatar. Omit it to let Twenty pick the default.
  • isLabelSyncedWithName, on an object or on a field, keeps the API name in sync with the label when the label is edited in Settings. It defaults to false, so a renamed label leaves the name untouched.
  • isSearchable: true, on a field, includes its values in the object’s full-text search (global search and the command menu). It requires the object itself to be searchable and a text-compatible field type. An omitted value means not searchable, except for the object’s label identifier field, which is always searchable and cannot be opted out. Each sync enforces the declared state, so a field toggled searchable from Settings reverts on your app’s next sync unless the manifest declares it.
  • isAuditLogged: false, on a field, keeps its changes out of the record timeline. It defaults to true, except on a POSITION field, whose changes render blank in the timeline and are never logged. Set it on fields your app rewrites on a schedule, such as a last-contact timestamp or a rollup relation, so each sync does not add an activity row to every record it touches. An update whose whole diff is made of non audit logged fields produces no timeline activity at all. Like isSearchable, each sync enforces the declared state.
  • disabledFieldPermissionActions, on a field, lists field permissions that cannot be granted on it: 'canReadFieldValue', 'canUpdateFieldValue' or both, the same flags a role sets per field. Users, API keys, agents and other apps never get these actions on the field, whatever their role grants, and saving a field permission that grants one of them fails. Your app’s own logic functions keep them. For example, ['canUpdateFieldValue'] keeps a value your app computes or syncs, such as a status or a score, read-only for everyone else. It also works on fields you add to objects you don’t own with defineField().
  • Inline fields defined here do not need an objectUniversalIdentifier — it’s inherited from the parent object. Use defineField() to add fields to objects you don’t own.
  • You can scaffold new objects with twenty app add object, which guides you through naming, fields, and relationships. See Architecture → Scaffolding entities.
Base fields are added automatically. When you define a custom object, Twenty creates standard fields like id, name, createdAt, updatedAt, createdBy, updatedBy, and deletedAt for you. You don’t need to declare them in your fields array — only your custom fields. You can override a default field by declaring one with the same name, but this is rarely a good idea.

Field types

The full set of FieldType values, exported from twenty-sdk/define: Composite types store multiple sub-fields (e.g. FULL_NAME = first + last name; CURRENCY = amountMicros + currencyCode). SELECT and MULTI_SELECT require an options array as in the example above.

Select options

For SELECT and MULTI_SELECT, provide a non-empty options array of objects. Each option has a value, label, and position. Its color is optional and defaults to gray when omitted or null. When provided, color must be one of: red, ruby, crimson, tomato, orange, amber, yellow, lime, grass, green, jade, mint, turquoise, cyan, sky, blue, iris, violet, purple, plum, pink, bronze, gold, brown, gray. Both defineObject() and defineField() return validation errors for unsupported colors or option entries that are null, primitives, or arrays.

Default values

Literal string defaults must be wrapped in single quotes inside the string — defaultValue: "'Draft'", not defaultValue: "Draft". That’s why the status field above uses `'${PostCardStatus.DRAFT}'`. Unquoted strings are reserved for computed defaults, evaluated when a record is created:
  • 'uuid' — generates a UUID (for UUID fields)
  • 'now' — the current timestamp (for DATE_TIME fields)
The same convention applies to string sub-fields of composite defaults (e.g. { source: "'MANUAL'" } on an ACTOR field) and to SELECT/MULTI_SELECT values. A literal string default left unquoted raises a warning when your app is built.

Nullability

isNullable controls whether a field accepts NULL. It defaults to true — omit it for optional fields. Set isNullable: false to make a field required at the database level. Changes to isNullable are applied on every sync, including syncs that update an existing field — so you can flip a field’s nullability by editing the manifest and re-syncing.
Making an existing field non-nullable requires a default value. When you change a field to isNullable: false, you must also provide a non-null defaultValue. The default backfills any existing NULL rows before the NOT NULL constraint is applied; without it the sync fails with Default value cannot be null for non-nullable fields. Relation fields and TS_VECTOR fields are always nullable, so isNullable has no effect on them.

What’s next

  • Connect this object to others — see Relations for the bidirectional relation pattern.
  • Add fields to objects from other apps — see Extending Objects for defineField().
  • Display this object in the UI — see Navigation Menu Items to add a sidebar entry; see Views to add custom list configurations.