definePageLayout() to declare a layout for an object you own, or definePageLayoutTab() to add a single tab to a layout that already exists (yours or a standard Twenty one).
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
typeis 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 only acceptsFORM_FIELDwidgets, each rendering a single field; conversely, every other widget type is rejected in a'RECORD_FORM'layout. Twenty does not render record forms yet.objectUniversalIdentifierspecifies which object this layout applies to.- Each
tabdefines a section of the page with atitle,position, andlayoutMode:VERTICAL_LISTfor record pages and standalone pages,GRIDfor dashboards, andCANVASfor a single widget that should fill the tab viewport. AVERTICAL_LISTtab stacks widgets vertically. Built-in widgets that manage their own scrolling, such as timelines, files, notes, tasks, and workflows, fill one viewport; fields, front components, graphs, and other fit-content widgets render at their content or configured height. AGRIDtab always lays its widgets out as cards on a 12-column grid. ACANVASwidget has no coordinates and can omitposition; Twenty normalizes it to{ layoutMode: PageLayoutTabLayoutMode.CANVAS }from the tab layout mode. If a canvas tab contains multiple widgets, they render at their content height instead of filling the viewport. - A widget
positionmatches its tab layout mode.VERTICAL_LISTuses{ layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, index },GRIDuses{ layoutMode: PageLayoutTabLayoutMode.GRID, row, column, rowSpan, columnSpan }, andCANVASuses{ layoutMode: PageLayoutTabLayoutMode.CANVAS }or omitsposition. - Set
layoutModeexplicitly. Omitting it gives youVERTICAL_LISTon aSTANDALONE_PAGEandGRIDeverywhere else, which is rarely what you want on a record page. - Each
widgetinside a tab can render a front component, a relation list, or other built-in widget types. - A
FRONT_COMPONENTwidget can setheaderCommandMenuItemUniversalIdentifiersto 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. positionon tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones.
Field widgets
AFIELD 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.
fieldMetadataIdtakes the universal identifier of a field on the layout’s object.fieldDisplayModeis one of'FIELD','CARD','EDITOR','VIEW'or'TABLE'.TABLEembeds a view listing the records of a one-to-many relation field.isUIEditableapplies to relation fields inTABLEdisplay mode. It defaults totrue; set it tofalseto prevent editing records from the embedded table or kanban.nestedRelationFieldMetadataIdis 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 requiresfieldDisplayMode: '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
ARECORD_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.
viewUniversalIdentifieridentifies the view to display.recordLimitis optional and caps how many records the widget displays.isUIEditableapplies to table and kanban views. It enables table cell editing or kanban card editing, moving, and creation. It defaults totrueonRECORD_PAGElayouts andfalseon other layouts.isUIEditablecontrols UI actions only. Record permissions still apply, and view settings remain editable only in the page layout editor.
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
-
pageLayoutUniversalIdentifieris 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 itstabsand theirwidgets, so you can reference any level:A short aliasSTANDARD_PAGE_LAYOUTis also available: -
widgetsare scoped to this tab only — they reference front components, views, etc. exactly like widgets defined inline indefinePageLayout. -
positioncontrols 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
definePageLayoutwhen you only want to add to an existing layout. UsedefinePageLayoutwhen you own the entire layout.