Skip to main content
A page layout controls how a record’s detail page is arranged: which tabs appear and what widgets they contain. Use definePageLayout() to declare a layout for an object you own, definePageLayoutTab() to add a single tab to a layout that already exists (yours or a standard Twenty one), or definePageLayoutWidget() to add a single widget to a tab that already exists.

definePageLayout

Use this when you own the entire detail page — typically for a custom object you defined yourself.
src/page-layouts/example-record-page-layout.ts

Key points

  • type is one of 'RECORD_INDEX', 'RECORD_PAGE', 'DASHBOARD', 'STANDALONE_PAGE' or 'RECORD_FORM'. Use 'RECORD_PAGE' to customize the detail view of a specific object. 'RECORD_FORM' describes the creation form of an object and holds FORM_FIELD widgets, each rendering a single field. Twenty provisions one 'RECORD_FORM' layout per object with one widget per creatable field, so you rarely need to author one; it does not render record forms yet.
  • objectUniversalIdentifier specifies which object this layout applies to.
  • Each tab defines a section of the page with a title, position, and layoutMode: use VERTICAL_LIST for record pages and standalone pages, and GRID for dashboards. A VERTICAL_LIST tab stacks widgets vertically. Built-in widgets that manage their own scrolling, such as timelines, files, notes, tasks, and workflows, fill one viewport by default; fields, front components, graphs, and other fit-content widgets render at their content or configured height. A GRID tab always lays its widgets out as cards on a 12-column grid.
  • Widgets in a VERTICAL_LIST tab inherit the tab’s layout mode, and their order comes from the widgets array. Do not set position; set the optional top-level heightBehavior to 'TAB_VIEWPORT' when a widget should occupy one full tab viewport and own its scrolling, or to 'FIT_CONTENT' to override a built-in full-height default. Preceding widgets do not reduce that height. Only one active TAB_VIEWPORT widget is allowed per tab, and it must come after fit-content widgets.
  • GRID widgets continue to use position: { layoutMode: PageLayoutTabLayoutMode.GRID, row, column, rowSpan, columnSpan }. Top-level heightBehavior is only valid for VERTICAL_LIST tabs; using it with GRID or CANVAS is a validation error.
  • Existing Canvas tabs and explicit widget positions remain supported. The SDK shows deprecation warnings with the recommended replacements.
  • Set layoutMode explicitly. Omitting it gives you VERTICAL_LIST on a STANDALONE_PAGE and GRID everywhere else, which is rarely what you want on a record page.
  • Each widget inside a tab can render a front component, a relation list, or other built-in widget types.
  • A FRONT_COMPONENT widget can set headerCommandMenuItemUniversalIdentifiers to an ordered array of command menu item universal identifiers from the same app. These actions appear as icon buttons in the widget card header and keep their command-level availability and permission checks. Identifiers must be unique and must resolve when the app is installed.
  • position on tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones.
  • yarn twenty pull writes page layouts back with their tabs and widgets inline. A widget the export cannot carry yet, such as one showing your app’s own front component or one of the deprecated VIEW type, is reported by pull instead of written; see what pull does not write.

Field widgets

A FIELD widget renders one field of the current record. For relation fields, it can also embed a view of records related to the current record. To embed a view that is not scoped to the current record, use a record table widget.
  • fieldMetadataId takes the universal identifier of a field on the layout’s object.
  • fieldDisplayMode is one of 'FIELD', 'CARD', 'EDITOR', 'VIEW' or 'TABLE'. TABLE embeds a view listing the records of a one-to-many relation field.
  • isUIEditable applies to relation fields in TABLE display mode. It defaults to true; set it to false to prevent editing records from the embedded table or kanban.
  • nestedRelationFieldMetadataId is optional and takes the universal identifier of a one-to-many relation field on the relation target object, to list records two relation hops away (e.g. a Company page listing the opportunities of the company’s people, or a Person page listing the opportunities of the person’s company). The first hop can be a one-to-many or a many-to-one relation field, the second must be one-to-many (junction relations are not supported), and it requires fieldDisplayMode: 'TABLE' — combining it with any other display mode is a validation error, since a nested widget always renders as an embedded view.

Record table widgets

A RECORD_TABLE widget embeds a saved view on a dashboard, record page, or standalone page. The saved view determines the records, fields, filters, sorts, and layout shown in the widget.
  • viewUniversalIdentifier identifies the view to display.
  • recordLimit is optional and caps how many records the widget displays.
  • isUIEditable applies to table and kanban views. It enables table cell editing or kanban card editing, moving, and creation. It defaults to true on RECORD_PAGE layouts and false on other layouts.
  • isUIEditable controls UI actions only. Record permissions still apply, and view settings remain editable only in the page layout editor.
Workspace admins can configure the same behavior with Allow editing in the dashboard widget and record page editors.

definePageLayoutTab

Use this when you only want to add a tab to an existing layout — for example, an analytics tab on the standard Company page, or an AI summary tab attached to your own object’s layout.
src/page-layouts/example-extra-tab.ts

Key points

  • pageLayoutUniversalIdentifier is required and must point to a page layout that already exists at install time — either a standard Twenty layout or one defined by your own app. Cross-app references to layouts owned by another installed app are not supported today. When the parent layout is missing, installation fails with a clear validation error.
  • For standard Twenty layouts, import identifiers from twenty-sdk/define:
    Each layout entry also exposes its tabs and their widgets, so you can reference any level:
    A short alias STANDARD_PAGE_LAYOUT is also available:
  • widgets are scoped to this tab only — they reference front components, views, etc. exactly like widgets defined inline in definePageLayout.
  • position controls ordering against existing tabs on the targeted layout. Pick a value that places your tab where you want it relative to built-in tabs.
  • Use this instead of definePageLayout when you only want to add to an existing layout. Use definePageLayout when you own the entire layout.
  • yarn twenty pull writes a tab you added to a page layout you do not own back as a definePageLayoutTab() file, with its widgets inline, under the same limits as widgets of a definePageLayout().

definePageLayoutWidget

Use this when you only want to add a widget to a tab that already exists — for example, a front component on the Home tab of the standard Company page, or a chart on the Home tab of your own object’s record page.
src/page-layout-widgets/example-home-widget.ts

Key points

  • pageLayoutTabUniversalIdentifier is required and must point to a tab that already exists at install time: a standard Twenty tab (STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.<object>RecordPage.tabs.<tab>.universalIdentifier), a tab of one of your own layouts, or a tab of the record page Twenty generates for one of your objects. The generated tabs have deterministic identifiers, so you can compute them:
  • position is required, since the tab is not part of your manifest and its layout mode cannot be inferred. Match the tab’s layout mode: record page tabs are vertical lists, so use { layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, index } with an index above the built-in widgets (the Fields widget sits at 0).
  • For a vertical list, set position.heightBehavior to PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT to fill one tab viewport, or PageLayoutWidgetVerticalListHeightBehavior.FIT_CONTENT to use the widget’s content or configured height. Import this enum from twenty-sdk/define, as in the example above. If omitted, the widget keeps its built-in default. Only one active widget per tab can fill the viewport, and its index must place it after all fit-content widgets. heightBehavior is not supported in GRID or CANVAS positions.
  • configuration works exactly like a widget defined inline in definePageLayout or definePageLayoutTab: it references front components, views, fields and so on.
  • Use this instead of definePageLayoutTab when the tab already exists and you only want to place one widget on it. Use definePageLayoutTab when you want a tab of your own.
  • yarn twenty pull writes a widget you added to a tab you do not own back as a definePageLayoutWidget() file, under the same limits as widgets of a definePageLayout().