diff --git a/.claude/commands/add-block.md b/.claude/commands/add-block.md
index fddeb70b482..2e0b8a01260 100644
--- a/.claude/commands/add-block.md
+++ b/.claude/commands/add-block.md
@@ -27,7 +27,7 @@ export const {ServiceName}Block: BlockConfig = {
name: '{Service Name}', // Human readable
description: 'Brief description', // One sentence
longDescription: 'Detailed description for docs',
- docsLink: 'https://docs.sim.ai/tools/{service}',
+ docsLink: 'https://docs.sim.ai/integrations/{service}',
category: 'tools', // 'tools' | 'blocks' | 'triggers'
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
@@ -626,7 +626,7 @@ export const ServiceBlock: BlockConfig = {
name: 'Service',
description: 'Integrate with Service API',
longDescription: 'Full description for documentation...',
- docsLink: 'https://docs.sim.ai/tools/service',
+ docsLink: 'https://docs.sim.ai/integrations/service',
category: 'tools',
integrationType: IntegrationType.DeveloperTools,
tags: ['oauth', 'api'],
diff --git a/.claude/commands/add-integration.md b/.claude/commands/add-integration.md
index 66553efbe1d..b1d07b39f6d 100644
--- a/.claude/commands/add-integration.md
+++ b/.claude/commands/add-integration.md
@@ -121,7 +121,7 @@ export const {Service}Block: BlockConfig = {
name: '{Service}',
description: '...',
longDescription: '...',
- docsLink: 'https://docs.sim.ai/tools/{service}',
+ docsLink: 'https://docs.sim.ai/integrations/{service}',
category: 'tools',
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
diff --git a/.claude/commands/validate-integration.md b/.claude/commands/validate-integration.md
index ee188565415..e641486819b 100644
--- a/.claude/commands/validate-integration.md
+++ b/.claude/commands/validate-integration.md
@@ -185,7 +185,7 @@ For **each tool** in `tools.access`:
- [ ] `name` is human-readable (e.g., `'X'`, `'Cloudflare'`)
- [ ] `description` is a concise one-liner
- [ ] `longDescription` provides detail for docs
-- [ ] `docsLink` points to `'https://docs.sim.ai/tools/{service}'`
+- [ ] `docsLink` points to `'https://docs.sim.ai/integrations/{service}'`
- [ ] `category` is `'tools'`
- [ ] `bgColor` uses the service's brand color hex
- [ ] `icon` references the correct icon component from `@/components/icons`
diff --git a/.claude/rules/sim-settings-pages.md b/.claude/rules/sim-settings-pages.md
new file mode 100644
index 00000000000..671d76f4c57
--- /dev/null
+++ b/.claude/rules/sim-settings-pages.md
@@ -0,0 +1,123 @@
+---
+paths:
+ - "apps/sim/app/workspace/*/settings/**"
+ - "apps/sim/ee/**/components/**"
+---
+
+# Settings Pages
+
+Every settings page renders through the shared **`SettingsPanel`** primitive
+(`@/app/workspace/[workspaceId]/settings/components/settings-panel`). It owns the
+page chrome so pages never hand-roll it: a fixed header bar (right-aligned
+actions), a scroll region, and a centered `max-w-[48rem]` content column led by a
+**title + description that come from navigation metadata**. Pages render only
+their body.
+
+Do NOT hand-roll any of these in a settings page — they are the panel's job:
+
+- `
` shell
+- the header bar (`flex flex-shrink-0 … px-[16px] pt-[8.5px] pb-[8.5px]`)
+- the scroll container (`min-h-0 flex-1 overflow-y-auto px-6 [scrollbar-gutter:stable_both-edges]`)
+- the content column (`mx-auto … max-w-[48rem] … gap-7`)
+- a title block (`
` + `
`)
+- the page-level search input
+
+## Canonical page shape
+
+```tsx
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
+
+return (
+
+ Create
+
+ }
+ search={{ value: searchTerm, onChange: setSearchTerm, placeholder: 'Search …' }}
+ >
+ {/* body only — sections, lists, forms */}
+
+)
+```
+
+When the page has modal/dialog siblings, wrap them with the panel in a fragment:
+
+```tsx
+return (
+ <>
+ {body}
+
+ >
+)
+```
+
+## `SettingsPanel` props
+
+- `actions?: ReactNode` — right-aligned header chips. Wrap multiple in a fragment;
+ the slot reserves the 30px chip height even when empty, so vertical rhythm is
+ identical across pages. Conditional actions are fine: `actions={canManage && }`.
+- `search?: { value; onChange: (value: string) => void; placeholder?; disabled? }` —
+ renders the canonical search field directly below the title. Pass `setSearchTerm`
+ straight to `onChange`. Use this for a standalone search; if search shares a row
+ with other controls (sort, filters, a date picker), render that whole row in
+ `children` instead and omit the prop.
+- `title?` / `description?` — overrides for the nav-driven defaults. **Only** for a
+ detail sub-view that needs a different heading; normal pages never pass these.
+- `scrollContainerRef?: React.Ref` — forwards a ref to the scroll
+ region (e.g. programmatic scroll-to-bottom).
+- `contentClassName?` — layout/spacing only; reach for it rarely. Prefer the
+ default `gap-7`.
+
+## Title + description live in navigation metadata
+
+`apps/sim/app/workspace/[workspaceId]/settings/navigation.ts` is the single source
+of truth. Every `NavigationItem` carries a one-line `description`; `SettingsPanel`
+resolves both via `getSettingsSectionMeta(section)` and the
+`SettingsSectionProvider` the settings shell wraps around the active section.
+
+Adding a new settings page:
+
+1. Add the `SettingsSection` id + a `NavigationItem` (with `label` **and**
+ `description`) in `navigation.ts`. Keep descriptions verb-first, one line,
+ ~40–55 chars, in the product voice (see `.claude/rules/constitution.md`).
+2. Render the component inside the shell's `effectiveSection` switch in
+ `settings/[section]/settings.tsx`.
+3. Build the component body inside `` — no shell, no title block.
+
+## Other shared settings primitives (do not re-roll these)
+
+- **`SettingsEmptyState`** (`…/components/settings-empty-state`) — the canonical
+ muted status message. `variant='fill'` (default) centers in the available
+ height (empty list, or a not-entitled/loading gate); `variant='inline'` sits in
+ flow (a search "no results"). Never hand-roll
+ ``
+ or `
`. It owns the `--text-muted` + `text-sm`
+ tokens, so it also keeps these messages consistent across pages.
+- **`RowActionsMenu`** (`…/components/row-actions-menu`) — the trailing `...`
+ actions menu for a list row. Pass `label` (aria-label) and
+ `actions: RowAction[]` (`{ label, onSelect, destructive?, disabled? }`); the
+ component renders the canonical flush `...` trigger + `DropdownMenuContent`.
+ Conditional items become array spreads: `...(canManage ? [{…}] : [])`. Never
+ hand-roll the `` + `` trigger per page.
+
+## Detail sub-views (the one exception)
+
+A drill-down view reached from a list row (selected MCP server, workflow MCP
+server, credential set, permission group) keeps its **own** chrome because it
+needs a left-aligned back button (``), which the panel
+header (right-actions only) does not model. Leave those returns as hand-rolled
+shells; only the list/main view uses `SettingsPanel`. Gate/early-return states
+(not-entitled, loading, upgrade prompts) also stay as-is.
+
+## Audit checklist
+
+A settings page is design-system-clean when:
+
+- [ ] Its main return is a `` (or `<>……>` with modal siblings) — no hand-rolled shell/header/scroll/column.
+- [ ] It renders **no** hand-rolled ``/description title block — the title comes from nav metadata.
+- [ ] Header chips are in `actions`; a standalone search is in the `search` prop.
+- [ ] Its `NavigationItem` has an accurate, consistent-length `description`.
+- [ ] Detail sub-views and entitlement/loading gates keep their own chrome (intentional).
+- [ ] No business logic, handlers, or conditional rendering changed by the migration.
+- [ ] `tsc`, `biome`, and the page's tests pass.
diff --git a/.claude/skills/add-settings-page/SKILL.md b/.claude/skills/add-settings-page/SKILL.md
new file mode 100644
index 00000000000..9e5d7b5671c
--- /dev/null
+++ b/.claude/skills/add-settings-page/SKILL.md
@@ -0,0 +1,57 @@
+---
+name: add-settings-page
+description: Add a new Sim settings page, or audit existing settings pages for design-system compliance with the shared SettingsPanel layout. Use when creating a settings tab, or when asked to check/clean up settings pages so they match the design system (consistent title, header, search, spacing).
+---
+
+# Settings Page (add / audit)
+
+Sim settings pages all render through the shared **`SettingsPanel`** primitive,
+which owns the page chrome and renders a nav-driven title + description. The full
+convention lives in `.claude/rules/sim-settings-pages.md` — read it first; this
+skill is the procedure.
+
+Key paths:
+- Layout primitive: `apps/sim/app/workspace/[workspaceId]/settings/components/settings-panel/settings-panel.tsx`
+- Nav metadata (titles + descriptions): `apps/sim/app/workspace/[workspaceId]/settings/navigation.ts`
+- Section switch + provider: `apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx`
+- Pages: `apps/sim/app/workspace/[workspaceId]/settings/components//.tsx` and EE pages under `apps/sim/ee//components/`
+
+## Mode A — Add a new settings page
+
+1. **Navigation.** In `navigation.ts`: add the id to the `SettingsSection` union,
+ then a `NavigationItem` with `label` AND a one-line `description` (verb-first,
+ ~40–55 chars, product voice per `.claude/rules/constitution.md`). Place it in
+ the right `section` group and set any gating flags (`requiresHosted`,
+ `requiresEnterprise`, etc.).
+2. **Wire the switch.** Add the component to the `effectiveSection` render switch
+ in `settings/[section]/settings.tsx` (lazy `dynamic(...)` like its siblings).
+3. **Build the body inside `SettingsPanel`.** Never hand-roll the shell, header
+ bar, scroll region, content column, or title block. Put header buttons in
+ `actions`, a standalone search in `search={{ value, onChange, placeholder }}`,
+ and the page content as `children`. Modals go beside the panel inside a `<>`.
+4. **Verify:** `cd apps/sim && bunx tsc --noEmit`; `bunx biome check --write `.
+
+## Mode B — Audit existing settings pages
+
+For each page component, confirm the checklist in `.claude/rules/sim-settings-pages.md`:
+
+1. Find hand-rolled shells that should be `SettingsPanel`:
+ `git grep -n "flex h-full flex-col bg-\[var(--bg)\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
+ — every match should be either `settings-panel.tsx`, a **detail sub-view**
+ (has a `` back button), or an entitlement/loading
+ **gate** early-return. Anything else is a page that still needs migrating.
+2. Find hand-rolled title blocks (should be 0 outside detail views):
+ `git grep -n "text-\[var(--text-body)\] text-lg" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
+3. Confirm each page imports `SettingsPanel` and that its `NavigationItem` has an
+ accurate `description` of consistent length with its peers.
+4. When migrating a page, change ONLY the structural shell→`SettingsPanel` swap:
+ move header chips to `actions`, the standalone search to `search`, delete the
+ `` title block, replace the three closing `
` (column/scroll/shell)
+ with ``, and keep modal siblings in a `<>` fragment. Do NOT
+ touch handlers, state, queries, conditional rendering, or detail/gate returns.
+ Drop per-page `gap-*`/`pt-*` on the content column in favor of the panel default.
+5. Remove now-unused imports (`ChipInput`/`Search`) ONLY after grepping that
+ they are not still used elsewhere in the file (e.g. by a detail view).
+6. **Verify the whole sweep:** `tsc --noEmit`, `biome check` on every touched
+ file, and run the affected pages' tests. Diff each file against the base and
+ confirm the change is purely structural before shipping.
diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore
index 2473d52587b..d4dabf71e70 100644
--- a/apps/docs/.gitignore
+++ b/apps/docs/.gitignore
@@ -38,3 +38,6 @@ next-env.d.ts
# Fumadocs
/.source/
.plans/
+
+# fumadocs generates .source dirs anywhere a source.config sits
+**/.source/
diff --git a/apps/docs/app/[lang]/[[...slug]]/page.tsx b/apps/docs/app/[lang]/[[...slug]]/page.tsx
index eaae5fe1c57..86539c52302 100644
--- a/apps/docs/app/[lang]/[[...slug]]/page.tsx
+++ b/apps/docs/app/[lang]/[[...slug]]/page.tsx
@@ -75,16 +75,23 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
}
const isOpenAPI = '_openapi' in data && data._openapi != null
const isApiReference = slug?.some((s) => s === 'api-reference') ?? false
+ // Academy lessons are video-first: drop the "On this page" TOC and go full
+ // width so the lesson hero/video gets the room (chapters live in-page instead).
+ const isAcademy = slug?.[0] === 'academy'
const pageTreeRecord = source.pageTree as Record
const pageTree = pageTreeRecord[lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0]
const rawNeighbours = pageTree ? findNeighbour(pageTree, page.url) : null
- const neighbours = isApiReference
+ // Academy and API Reference are self-contained sections; keep prev/next inside
+ // the section instead of spilling into the main documentation tree. Match both
+ // the section's pages (`//...`) and its index (`/`).
+ const sectionSlug = isApiReference ? 'api-reference' : isAcademy ? 'academy' : null
+ const inSection = (url?: string) =>
+ url != null && (url.includes(`/${sectionSlug}/`) || url.endsWith(`/${sectionSlug}`))
+ const neighbours = sectionSlug
? {
- previous: rawNeighbours?.previous?.url.includes('/api-reference/')
- ? rawNeighbours.previous
- : undefined,
- next: rawNeighbours?.next?.url.includes('/api-reference/') ? rawNeighbours.next : undefined,
+ previous: inSection(rawNeighbours?.previous?.url) ? rawNeighbours?.previous : undefined,
+ next: inSection(rawNeighbours?.next?.url) ? rawNeighbours?.next : undefined,
}
: rawNeighbours
@@ -197,18 +204,18 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
/>
{children}
+