Skip to main content
Pages and record tabs give your app surfaces of its own. This guide builds a deal health app with both, then links them together. We’ll build:
  1. A Dashboard page under your app’s own URL, laid out with Grid, Card and Stack.
  2. An EmptyState for workspaces with no data yet.
  3. A Health record tab on deal pages.
  4. Links between the page, the tab and records with AttioLink and navigate.
Everything here is client code. Where the examples call a server function, swap in your own; see Querying Attio data for how to fetch records.

1. Scaffold a page

A page is a folder under src/app/pages/ with a page.tsx that default-exports Extensions.definePage. The folder name is the URL slug.
src/app/pages/dashboard/page.tsx
Run attio dev, install the app in your development workspace, and the app appears in the Apps section of the sidebar. Click it and you’re on /{workspaceSlug}/apps/{appSlug}/dashboard.

2. Lay out the dashboard

A dashboard is a grid of cards. Grid handles the columns and collapses them as the window narrows. Card gives each metric a bordered box. Stack arranges what’s inside.
src/app/pages/dashboard/page.tsx
Two things to notice. The metric tiles use minColumnWidth="small" because each holds a single value; the default "medium" would give them more room than they need. And the table card sits outside the grid, as a sibling, because there are no column spans: anything full-width goes next to the grid, not in it. Page suspends while useDealHealth loads and Attio shows the page skeleton in the meantime. You don’t need a loading state of your own.

3. Handle the empty case

A fresh install has no deals to report on. Return an EmptyState as the only child of the page and it fills the surface with a centred title, description and actions. Anywhere else in the tree it doesn’t render, so it has to be the whole page.
src/app/pages/dashboard/page.tsx

4. Add a record tab

The dashboard shows health across all deals. For one deal, a record tab is the right surface: it appears on every deal page after install, gets the full width below the tab bar, and admins can reorder it with “Configure page” like any built-in tab.
src/app/extensions/health/extension.tsx
src/app/extensions/health/health-tab.tsx
The same Grid and Card work here as on the page: the grid collapses on the width of its container, so it lays out correctly whether the record page is wide or the sidebar is open. The tab’s URL is /{workspaceSlug}/deals/{recordId}/{appSlug}/health. Pick id carefully: it’s part of that URL, and it can’t be one of Attio’s built-in tab slugs (overview, activity, notes and the rest; the reference lists them all). The dashboard’s at-risk table should link to each deal, and ideally straight to its Health tab. AttioLink renders a real link, so cmd-click and copy link work.
src/app/pages/dashboard/at-risk-deals.tsx
tab: "health" is your record tab id. The same option takes built-in slugs too ("activity", "notes"). Both appPage slugs and tab autocomplete against the pages and tabs the CLI found in your app, with no setup; see typed slugs. Going the other way, a link from the tab back to the dashboard is one line:
If a destination can’t be resolved (a deleted record, a slug you renamed), nothing breaks: AttioLink shows plain text and navigate logs to the console. A record that no longer exists lands on Attio’s normal “not found” page, same as a stale bookmark.

What you’ve built

  • src/app/pages/dashboard/page.tsx, a page in the sidebar at /apps/{appSlug}/dashboard, laid out with Grid, Card and Stack, with an EmptyState for new installs.
  • src/app/extensions/health/extension.tsx, a record tab on deal pages.
  • Links between them with AttioLink, and a navigate call to settings.
From here:
  • Add a second page and the app row in the sidebar expands to list both. See Pages.
  • Use variant: "centered" on definePage for reading-heavy pages.
  • Fetch real data with useQuery and server functions.