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
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 holdsFORM_FIELDwidgets, 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.objectUniversalIdentifierspecifies which object this layout applies to.- Each
tabdefines a section of the page with atitle,position, andlayoutMode: useVERTICAL_LISTfor record pages and standalone pages, andGRIDfor dashboards. AVERTICAL_LISTtab 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. AGRIDtab always lays its widgets out as cards on a 12-column grid. - Widgets in a
VERTICAL_LISTtab inherit the tab’s layout mode, and their order comes from thewidgetsarray. Do not setposition; set the optional top-levelheightBehaviorto'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 activeTAB_VIEWPORTwidget is allowed per tab, and it must come after fit-content widgets. GRIDwidgets continue to useposition: { layoutMode: PageLayoutTabLayoutMode.GRID, row, column, rowSpan, columnSpan }. Top-levelheightBehavioris only valid forVERTICAL_LISTtabs; using it withGRIDorCANVASis a validation error.- Existing Canvas tabs and explicit widget positions remain supported. The SDK shows deprecation warnings with the recommended replacements.
- 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.yarn twenty pullwrites 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 deprecatedVIEWtype, is reported by pull instead of written; see what pull does not write.
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. -
yarn twenty pullwrites a tab you added to a page layout you do not own back as adefinePageLayoutTab()file, with its widgets inline, under the same limits as widgets of adefinePageLayout().
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
-
pageLayoutTabUniversalIdentifieris 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: -
positionis 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.heightBehaviortoPageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORTto fill one tab viewport, orPageLayoutWidgetVerticalListHeightBehavior.FIT_CONTENTto use the widget’s content or configured height. Import this enum fromtwenty-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.heightBehavioris not supported inGRIDorCANVASpositions. -
configurationworks exactly like a widget defined inline indefinePageLayoutordefinePageLayoutTab: it references front components, views, fields and so on. -
Use this instead of
definePageLayoutTabwhen the tab already exists and you only want to place one widget on it. UsedefinePageLayoutTabwhen you want a tab of your own. -
yarn twenty pullwrites a widget you added to a tab you do not own back as adefinePageLayoutWidget()file, under the same limits as widgets of adefinePageLayout().