# data-table-filters — full documentation > Open-source React data table with faceted filters, sorting, infinite scroll, and virtualization. Distributed as shadcn registry blocks you install into your own repo — not as an npm dependency, so there is no library to wrap and nothing to eject from. - Stack: React 19+, TanStack Table v8, Tailwind CSS v4, shadcn/ui. Next.js App Router is first-class; the blocks work in any React app. - Install with `npx shadcn@latest add `. The shadcn CLI resolves block dependencies, rewrites `@/` import paths to match components.json, and injects the required CSS variables. - Built for large tables: filtering, faceted counts, sorting, and cursor pagination all execute in SQL, and rows are virtualized, so table size is bounded by the database rather than the browser. - One `createTableSchema` definition drives the columns, the filter controls, the row detail sheet, the server-side query handler, the natural-language filter parser, and the MCP tool schema. Source: https://data-table.openstatus.dev/docs — index at https://data-table.openstatus.dev/llms.txt --- # Introduction Source: https://data-table.openstatus.dev/docs/introduction _**It’s not a library. It’s a playbook.**_ Stop hand-rolling data tables. Copy proven patterns, install the [agent skill](/docs/quick-start#agent-skill) and start shipping. Built on the stack that actually scales: - **[shadcn registry](https://ui.shadcn.com)** — drop components directly into your codebase - **[TanStack Table](https://tanstack.com/table) + [Query](https://tanstack.com/query)** — sorting, filtering, infinite scroll, done - **[Drizzle ORM](/docs/drizzle-orm)** — server-side `WHERE`, cursors, and faceted counts out of the box - **[nuqs](https://nuqs.47ng.com) / [Zustand](https://zustand.docs.pmnd.rs/getting-started/introduction)** — URL or memory state, your call - **[Agent SKILL.md](/docs/quick-start#agent-skill)** — describe your schema, let the agent wire it up Define a schema. Generate columns, filters, and sheet details. Done. ![data-table with some filters and activated live mode](/assets/docs/data-table.png) This guide covers the system end to end — from defining a table schema to rendering filters, columns, and row details. Get started with the [Quick Start](/docs/quick-start) and jump to a [Full Example](/docs/full-example). ## Examples - [Auto](/auto) — zero-config table from raw data (no schema needed) - [Drizzle](/drizzle) — server-side filtering with Drizzle ORM - [Default](/default) — client-side pagination without infinite scroll - [Infinite](/infinite) — infinite scroll with cursor pagination (mock) - [Light](/light) — lightweight frontend for [light.openstatus.dev](https://light.openstatus.dev) - [Builder](/builder) — interactive schema builder > Questions, ideas, or feedback? [Open an issue on GitHub](https://github.com/openstatusHQ/data-table-filters/issues). --- ## Quick Overview ### Zero Config Pass data, get a fully filtered table — columns, filters, and display types are auto-inferred: ```tsx import { DataTableAuto } from "@/components/data-table/data-table-auto"; ; ``` See the [/auto](/auto) example for a live demo. ### With Schema When you need full control, define a schema and wire up state management: ```tsx import { col, createTableSchema } from "@/lib/table-schema"; import { generateColumns, generateFilterFields } from "@/lib/table-schema"; import { createSchema, field } from "@/lib/store/schema"; // 1. Define your table const tableSchema = createTableSchema({ level: col.presets.logLevel(["error", "warn", "info", "debug"]), latency: col.presets.duration("ms").label("Latency").sortable(), host: col.string().label("Host"), }); // 2. Generate everything the components need const columns = generateColumns(tableSchema.definition); const filterFields = generateFilterFields(tableSchema.definition); // 3. Wire up state + components ``` --- ## Architecture The data-table is built on three layers: 1. **Table Schema** — a declarative builder that defines columns, filters, display, sorting, and row details in one place 2. **State Management** — a pluggable adapter system for filter state (URL, Zustand, or custom) 3. **UI Components** — pre-built filter controls, command palette, infinite scroll table, and row detail drawer For server-side data, the [Drizzle ORM](/docs/drizzle-orm) helpers handle `WHERE` conditions, sorting, cursor pagination, and faceted counts. The [Data Fetching](/docs/data-fetching) layer covers the API response shape and React Query integration. ``` Table Schema → Generators → Components ↓ ↓ ↓ col.* columns[] DataTableInfinite presets filterFields DataTableFilterControls .sheet() sheetFields DataTableSheetDetails filterSchema DataTableFilterCommand ``` --- # Quick Start Source: https://data-table.openstatus.dev/docs/quick-start ## Zero Config The fastest way to try data-table-filters — pass an array of objects, get a fully filtered table: ```tsx import { DataTableAuto } from "@/components/data-table/data-table-auto"; const data = [ { name: "Alice", role: "admin", rating: 5, created_at: "2026-01-15T10:00:00Z", }, { name: "Bob", role: "user", rating: 3, created_at: "2026-02-20T14:30:00Z" }, ]; ; ``` Columns, filters, display types, and cell renderers are auto-inferred from your data. When you need customization, [eject to the Builder](/builder) or define a [Table Schema](/docs/table-schema). See the [/auto](/auto) example for a live demo. ## Agent Skill Install the plugin in Claude Code: ```bash /plugin marketplace add openstatushq/data-table-filters /plugin install data-table-filters@openstatus ``` Or install the skill with any agent that supports the `skills` CLI: ```bash npx skills add https://github.com/openstatushq/data-table-filters --skill data-table-filters ``` Let the agent handle the rest: > Add a data table with filters to my project It detects your stack, installs the right blocks, and wires them up. For agents without the skill, see [For AI Agents](/docs/agents) — `llms.txt`, raw markdown docs, and Cursor rules. ## Install Components Start by installing the core `data-table` block — it includes the table engine, memory store adapter, and all 4 filter types: ```bash npx shadcn@latest add https://data-table.openstatus.dev/r/data-table.json ``` Then add any extension block from the [registry](#blocks) below. ## Blocks The following blocks can be installed via shadcn CLI. ```bash npx shadcn@latest add https://data-table.openstatus.dev/r/ ``` | Name | Block | Components | | ------------------------------ | ---------------------------------------------------------------------------------------------------------- | ---------: | | `data-table` | [data-table.json](https://data-table.openstatus.dev/r/data-table.json) | 53 | | `data-table-filter-command` | [data-table-filter-command.json](https://data-table.openstatus.dev/r/data-table-filter-command.json) | 2 | | `data-table-cell` | [data-table-cell.json](https://data-table.openstatus.dev/r/data-table-cell.json) | 17 | | `data-table-sheet` | [data-table-sheet.json](https://data-table.openstatus.dev/r/data-table-sheet.json) | 5 | | `data-table-nuqs` | [data-table-nuqs.json](https://data-table.openstatus.dev/r/data-table-nuqs.json) | 4 | | `data-table-zustand` | [data-table-zustand.json](https://data-table.openstatus.dev/r/data-table-zustand.json) | 3 | | `data-table-schema` | [data-table-schema.json](https://data-table.openstatus.dev/r/data-table-schema.json) | 13 | | `data-table-drizzle` | [data-table-drizzle.json](https://data-table.openstatus.dev/r/data-table-drizzle.json) | 7 | | `data-table-query` | [data-table-query.json](https://data-table.openstatus.dev/r/data-table-query.json) | 3 | | `data-table-filter-command-ai` | [data-table-filter-command-ai.json](https://data-table.openstatus.dev/r/data-table-filter-command-ai.json) | 11 | | `data-table-mcp` | [data-table-mcp.json](https://data-table.openstatus.dev/r/data-table-mcp.json) | 5 | > Looking to add natural language filtering? The `data-table-filter-command-ai` block translates queries like _"5xx errors last 24h"_ into structured filters using any LLM. See [AI Filters](/docs/ai-filters) for setup. ## Next Steps - Browse the [/infinite](/infinite) demo to see all components in action - Learn about the [Table Schema](/docs/table-schema) builder for type-safe column definitions - Explore the [UI Components](/docs/ui-components) available out of the box --- # Table Schema Source: https://data-table.openstatus.dev/docs/table-schema The table schema is a type-safe builder for defining your entire table in one place. Instead of manually wiring up `columns.tsx`, `filterFields`, `sheetFields`, and a filter state schema separately, a single schema definition generates all of them. ## Defining a Schema ```tsx import { col, createTableSchema, type InferTableType, } from "@/lib/table-schema"; const LEVELS = ["error", "warn", "info", "debug"] as const; const METHODS = ["GET", "POST", "PUT", "DELETE"] as const; export const tableSchema = createTableSchema({ level: col.presets.logLevel(LEVELS).description("Log severity"), date: col.presets.timestamp().label("Date").size(200).sheet(), latency: col.presets .duration("ms") .label("Latency") .sortable() .size(110) .sheet(), status: col.presets.httpStatus().label("Status").size(60), method: col.presets.httpMethod(METHODS).size(69), host: col.string().label("Host").size(125).sheet(), path: col.presets.pathname().label("Path").size(130).sheet(), traceId: col.presets.traceId().label("Request ID").hidden().sheet(), headers: col.record().label("Headers").sheetOnly().sheet(), }); // Row type inferred from the schema export type ColumnSchema = InferTableType; ``` ## Column Factories Choose the factory based on your data type: ```ts col.string(); // string — text display, input filter col.number(); // number — number display, input filter col.boolean(); // boolean — boolean display, checkbox filter col.timestamp(); // Date — timestamp display, timerange filter col.enum(values); // union — badge display, checkbox filter col.array(item); // array — badge display, checkbox filter col.record(); // Record — text display, not filterable col.select(); // boolean — row selection checkbox column ``` Each factory returns a `ColBuilder` where `T` is the inferred TypeScript type and `F` constrains which filter types are valid at compile time: | Factory | Allowed Filters | Notes | | ----------------------------- | ----------------------------------- | ----------------------------------------------- | | `col.string()` | `"input"` | Text search | | `col.number()` | `"input"`, `"slider"`, `"checkbox"` | Slider for ranges, checkbox for discrete values | | `col.boolean()` | `"checkbox"` | Pre-wired with true/false options | | `col.timestamp()` | `"timerange"` | Date range picker | | `col.enum(values)` | `"checkbox"` | Options auto-derived from values | | `col.array(col.enum(values))` | `"checkbox"` | Multi-value tags, regions, labels | | `col.record()` | _none_ (`never`) | Use `.sheetOnly()` for detail drawers | | `col.select()` | _none_ (`never`) | Row selection checkbox + floating bar | ### Row Selection with `col.select()` `col.select()` adds a checkbox column for row selection. It renders a select-all checkbox in the header and a per-row checkbox in each cell. Pair it with `DataTableFloatingBar` to show bulk actions when rows are selected. ```ts export const tableSchema = createTableSchema({ select: col.select().size(37), // ...other columns }); ``` > `col.select()` is not filterable, not sortable, and excluded from the sheet. It should typically be the first column in your schema. See [DataTableFloatingBar](/docs/ui-components#datatablefloatingbar) for the bulk action bar. ## Presets Pre-configured builders for common patterns. All remain fully customizable via chaining. ```ts col.presets.logLevel(["error", "warn", "info", "debug"]); // → enum + badge + checkbox + defaultOpen col.presets.httpMethod(["GET", "POST", "PUT", "DELETE"]); // → enum + text display + checkbox col.presets.httpStatus(); // → number + checkbox with common codes (200, 201, 204, 301, ..., 504) // Custom codes: col.presets.httpStatus([200, 400, 500]) col.presets.duration("ms"); // → number + formatted display with unit + slider (0–5000) // Custom bounds: col.presets.duration("s", { min: 0, max: 60 }) col.presets.timestamp(); // → Date + relative time display + timerange filter + sortable col.presets.traceId(); // → string + code display + not filterable col.presets.pathname(); // → string + text display + input filter ``` ## Builder Methods All methods return a new builder instance (immutable) for fluent chaining. ### Label & Description ```ts col .string() .label("Host") // Column header label (required) .description("Origin server"); // For AI agents / MCP tools (not shown in UI) ``` > Descriptions are essential for [AI filter](/docs/ai-filters) accuracy. Without them, the AI only sees field names and types, which can lead to ambiguous results. Add `.description()` to any column you want the AI to handle well. ### Display Controls how the cell value renders. Built-in display types: | Type | Use case | | ------------------- | ------------------------------------------------------- | | `"text"` | Plain text with overflow tooltip (default for strings) | | `"code"` | Monospace — IDs, paths, hashes | | `"number"` | Formatted number with optional `unit` suffix | | `"bar"` | Horizontal bar with `min`/`max` range and optional unit | | `"heatmap"` | Background color intensity based on `min`/`max` range | | `"badge"` | Colored chip (default for enums) | | `"timestamp"` | Relative time ("3m ago"), absolute on hover | | `"boolean"` | Checkmark / dash icon | | `"star"` | Filled yellow star (true) / outlined muted star (false) | | `"status-code"` | HTTP status code coloring | | `"level-indicator"` | Severity dot indicator | | `"custom"` | Developer-supplied JSX (not serializable) | ```ts col.number().display("number", { unit: "ms" }) col.number().display("bar", { min: 0, max: 5000, unit: "ms" }) col.number().display("heatmap", { min: 0, max: 100 }) col.enum(v).display("badge", { colorMap: { error: "#ef4444", warn: "#f59e0b" } }) col.enum(v).display("custom", { cell: (value, row) => , }) ``` ### Filtering ```ts col.string().filterable("input") col.number().filterable("slider", { min: 0, max: 5000 }) col.enum(v).filterable("checkbox") col.enum(v).filterable("checkbox", { options: v.map(v => ({ label: v, value: v })), component: (props) => , }) col.timestamp().filterable("timerange") col.string().notFilterable() // Disables filtering (F becomes never) col.enum(v).defaultOpen() // Expand in filter sidebar by default col.timestamp().commandDisabled() // Exclude from command palette ``` > Fields with `.commandDisabled()` are hidden from the manual command palette but **still available to the AI**. This is useful for fields like date ranges that are easier to express in natural language (e.g., _"last 24 hours"_). See [AI Filters — Schema Considerations](/docs/ai-filters#schema-considerations). Passing a filter type not in `F` is a **compile-time error** — e.g. `col.string().filterable("slider")` won't compile. ### Visibility & Layout ```ts col.string().hidden(); // Hidden by default (toggleable in column menu) col.enum(v).hideHeader(); // Hide header label, keep column visible col.string().resizable(); // Enable drag-to-resize col.string().size(125); // Fixed width in pixels (initial width if resizable) ``` ### Sorting & Optionality ```ts col.number().sortable(); // Click-to-sort on column header col.string().optional(); // T becomes T | undefined in InferTableType ``` ### Sheet (Row Detail Drawer) ```ts col.string().sheet() // Include in detail drawer col.string().sheet({ label: "Server", skeletonClassName: "w-24" }) col.number().sheet({ component: (row) => <>{row.latency}ms, skeletonClassName: "w-16", }) col.record().sheetOnly() // hidden + notFilterable + enableHiding: false ``` ## Generators The schema drives four generators that produce everything the table components need. ```tsx import { generateColumns, generateFilterFields, generateFilterSchema, generateSheetFields, getDefaultColumnVisibility, } from "@/lib/table-schema"; // TanStack Table ColumnDef[] const columns = generateColumns(tableSchema.definition); // Filter sidebar and command palette fields const filterFields = generateFilterFields(tableSchema.definition); // Row detail drawer fields const sheetFields = generateSheetFields(tableSchema.definition); // Initial column visibility from .hidden() columns const defaultColumnVisibility = getDefaultColumnVisibility( tableSchema.definition, ); ``` You can append custom virtual columns that span multiple fields: ```tsx const allColumns = [ ...generateColumns(tableSchema.definition), { id: "timing", header: "Timing Phases", cell: ({ row }) => , size: 130, }, ]; ``` ### generateFilterSchema Bridges the table schema to filter state by generating a filter schema. Add non-column state fields (sort, pagination, live mode) alongside: ```tsx import { createSchema, field } from "@/lib/store/schema"; export const filterSchema = createSchema({ ...generateFilterSchema(tableSchema.definition).definition, sort: field.sort(), live: field.boolean().default(false), size: field.number().default(40), }); ``` > The `/infinite` route writes the filter state schema manually instead of using `generateFilterSchema` for better TypeScript inference. Both approaches work — use `generateFilterSchema` for convenience, manual for full control. The mapping from column types to filter state field types: | Column + Filter | filter state Field | | --------------------------------- | ----------------------------------------------- | | `col.string()` + `"input"` | `field.string()` | | `col.number()` + `"input"` | `field.number()` | | `col.number()` + `"slider"` | `field.array(field.number()).delimiter("-")` | | `col.number()` + `"checkbox"` | `field.array(field.number()).delimiter(",")` | | `col.enum(v)` + `"checkbox"` | `field.array(field.stringLiteral(v))` | | `col.boolean()` + `"checkbox"` | `field.array(field.boolean()).delimiter(",")` | | `col.timestamp()` + `"timerange"` | `field.array(field.timestamp()).delimiter("-")` | See the [Builder](/docs/builder) page for visual schema creation, serialization, and AI integration. --- # State Management Source: https://data-table.openstatus.dev/docs/state-management An adapter pattern that decouples filter state management from the table components. Use **nuqs** (URL-based), **Zustand** (client-side), **memory** (ephemeral), or build your own adapter. See the [/infinite](/infinite) demo for a live example using the nuqs adapter. ## Schema Definition The schema is the single source of truth for filter field types, defaults, and serialization. ```tsx import { createSchema, field } from "@/lib/store/schema"; export const filterSchema = createSchema({ // Checkbox filters (arrays of values) level: field.array(field.stringLiteral(["success", "warning", "error"])), status: field.array(field.number()).delimiter(","), regions: field.array(field.stringLiteral(["ams", "gru", "syd"])), // Text input filters host: field.string(), // Slider filters (ranges) latency: field.array(field.number()).delimiter("-"), // Date range filter date: field.array(field.timestamp()).delimiter("-"), // Sorting sort: field.sort(), // UI state live: field.boolean().default(false), size: field.number().default(40), }); export type FilterState = typeof filterSchema._type; ``` ## Field Types ```ts // Primitives (default to null) field.string() // string | null field.number() // number | null (parseInt) field.boolean() // boolean | null field.timestamp() // Date | null (serialized as ms) // Constrained field.stringLiteral(["a", "b", "c"]) // 'a' | 'b' | 'c' | null // Sorting field.sort() // { id: string; desc: boolean } | null // Serialized as "columnId.asc" / "columnId.desc" // Arrays field.array(field.string()) // string[] field.array(field.number()) // number[] field.array(field.stringLiteral([...])) // Literal[] // Modifiers (chainable) field.array(field.string()) .default(["default"]) // Default value on reset .delimiter(",") // URL serialization separator ``` ## Adapters Goal is to BYOS (Bring Your Own Store) - so if you want to use a specific state management, you can. Here are some adapters we've implemented for you. ### nuqs Adapter (URL-based) Syncs filter state to URL search params. Enables shareable URLs, browser history, and server-side parsing. Best for most use cases. | Option | Default | Description | | ------------ | -------- | ------------------------------------------- | | `id` | required | Table identifier (namespaces URL params) | | `shallow` | `true` | Don't trigger Next.js navigation on change | | `history` | `"push"` | `"push"` or `"replace"` for browser history | | `throttleMs` | `50` | Throttle URL updates (ms) | | `scroll` | `false` | Scroll to top on filter change | ```tsx import { useNuqsAdapter } from "@/lib/store/adapters/nuqs"; function Client() { const adapter = useNuqsAdapter(filterSchema.definition, { id: "my-table", shallow: true, // Don't trigger navigation history: "push", // "push" | "replace" throttleMs: 50, // Throttle URL updates }); return ( ); } ``` For server-side parsing: ```tsx import { createNuqsSearchParams } from "@/lib/store/adapters/nuqs/server"; export const { searchParamsParser, searchParamsCache, searchParamsSerializer } = createNuqsSearchParams(filterSchema.definition); // In page.tsx export default async function Page({ searchParams }) { const search = await searchParamsCache.parse(searchParams); // Use for server-side data fetching or prefetching... } ``` ### Zustand Adapter (Client-side) Integrates with existing Zustand stores. Use when you already have a Zustand store and want filter state to live alongside your app state, or when you don't want filters in the URL. ```tsx import { createFilterSlice, useZustandAdapter, } from "@/lib/store/adapters/zustand"; import { create } from "zustand"; export const useFilterStore = create>((set, get) => ({ ...createFilterSlice(filterSchema.definition, "my-table", set, get), })); function Client() { const adapter = useZustandAdapter(useFilterStore, filterSchema.definition, { id: "my-table", }); return ( ); } ``` ### Memory Adapter (Ephemeral) Lightweight in-memory state. No URL sync, no external store. State resets on unmount. Useful for embedded tables, the builder, or preview contexts where you don't want to pollute the URL. ```tsx import { useMemoryAdapter } from "@/lib/store/adapters/memory"; const adapter = useMemoryAdapter(filterSchema.definition); ``` ### Custom Adapters Implement the `StoreAdapter` interface: ```ts interface StoreAdapter> { subscribe(listener: () => void): () => void; getSnapshot(): { state: T; version: number }; getServerSnapshot?(): { state: T; version: number }; setState(partial: Partial): void; setField(key: K, value: T[K]): void; reset(fields?: (keyof T)[]): void; pause(): void; resume(): void; isPaused(): boolean; destroy(): void; getTableId(): string; getSchema(): SchemaDefinition; getDefaults(): T; } ``` ## Provider & Hooks ### DataTableStoreProvider Wraps your components with the adapter context: ```tsx {children} ``` ### useFilterState Read filter state. Uses `useSyncExternalStore` for optimal React 18+ compatibility. ```tsx // Read entire state const state = useFilterState(); // Read with selector (only re-renders when selected value changes) const live = useFilterState((s) => s.live); const regions = useFilterState((s) => s.regions); ``` ### useFilterActions Modify filter state. ```tsx const { setFilter, // Set a single field setFilters, // Set multiple fields at once resetFilter, // Reset a single field to default resetAllFilters, // Reset all filters pause, // Pause state updates (for live mode) resume, // Resume and apply queued changes isPaused, // Check if paused } = useFilterActions(); setFilter("regions", ["ams", "gru"]); setFilters({ regions: ["ams"], host: "api.example.com" }); resetFilter("regions"); resetAllFilters(); ``` ### useReactTableSync Bidirectional sync between adapter filter state and React Table's `columnFilters`. Useful when you need React Table's internal filtering to stay in sync with adapter state. ```tsx import { useReactTableSync } from "@/lib/store"; const table = useReactTable({ ... }); useReactTableSync({ table, filterFields, }); ``` ### useFilterField Work with a single field. Combines read and write. ```tsx const { value, setValue, reset } = useFilterField( "regions", ); setValue(["ams", "gru", "fra"]); reset(); // Back to default ``` --- # UI Components Source: https://data-table.openstatus.dev/docs/ui-components See the [/infinite](/infinite) example for a live demo with all components in action. ![data-table with filters and timeline chart](/assets/docs/data-table.png) ## DataTableInfinite The main infinite scroll table component. Located at `src/app/infinite/data-table-infinite.tsx`. ```tsx props.row?.original.pathname} getRowId={(row) => row.uuid} chartData={chartData} chartDataColumnId="date" /> ``` ## DataTableFilterControls The left sidebar with accordion-based filter controls. Renders the appropriate UI component for each filter type: - **Checkbox** — multi-select with search and count display - **Slider** — dual min/max range inputs - **Input** — text search with debounce - **Timerange** — date range picker with preset shortcuts Toggle with `Cmd+B`. For responsive layouts, `DataTableFilterControlsDrawer` provides a mobile-friendly drawer alternative. ## DataTableFilterCommand The command palette for text-based filtering. Opens with `Cmd+K`. Supports filter syntax: - Regular: `host:API` - Union: `regions:ams,gru` - Range: `latency:100-500` - Quoted: `host:"API Server"` (for values with space) ```tsx ``` > Want AI-powered natural language queries in your command palette? Install the `data-table-filter-command-ai` block for an enhanced version that translates free-form text into structured filters. See [AI Filters](/docs/ai-filters). ## DataTableFilterAICommand A drop-in replacement for `DataTableFilterCommand` that adds natural language query support. It includes everything from the standard command palette (structured `key:value` syntax, autocomplete, history) plus: - **AI inference** — type a free-form query like _"5xx errors in the last hour"_ and press Enter. The input is sent to your AI endpoint, which streams back structured filters. - **TextShimmer** — while the AI is processing, the closed command bar displays the query with a shimmer animation so the user knows it's working. - **Sparkles indicator** — an `AI:` badge in the footer and a sparkles icon on the infer suggestion hint that the input is being treated as natural language. The component automatically detects whether the input is structured (`host:API`) or natural language (`show me slow requests`) based on whether it matches known field names. ```tsx ``` | Prop | Type | Description | | ------------- | ----------------------- | ------------------------------------------------------------- | | `schema` | `SchemaDefinition` | BYOS schema for parsing/serializing filter values | | `tableSchema` | `TableSchemaDefinition` | Table schema for AI context generation | | `api` | `string` | API endpoint that streams AI filter results | | `tableId` | `string` | Unique ID for localStorage namespacing (default: `"default"`) | > For full setup including the API route and provider configuration, see [AI Filters](/docs/ai-filters). ## DataTableSheetDetails ![data-table sheet detail drawer](/assets/docs/data-table-sheet.png) Row detail drawer. Opens when a row is selected. Supports keyboard navigation between rows. ```tsx {row?.original.pathname}} titleClassName="font-mono" > ``` ## DataTableFloatingBar A fixed bottom bar that appears when rows are selected, providing a slot for bulk action buttons. Dismisses when no rows are selected. - Shows `"{n} selected"` count with a deselect button - `Cmd+Shift+X` hotkey to deselect all rows - Uses a render prop that receives the selected `rows` and `table` instance ```tsx > {({ rows, table }) => ( )} ``` Pass it via the `floatingBarSlot` prop on `DataTableInfinite`: ```tsx > {({ rows }) => } } // ...other props /> ``` ## Cell Components Built-in cell renderers (used automatically by `generateColumns`): - `DataTableCellText` — plain text with overflow tooltip - `DataTableCellCode` — monospace - `DataTableCellNumber` — formatted with optional unit - `DataTableCellBar` — the number over a bar filled to its share of `min`–`max` - `DataTableCellHeatmap` — the number over a cell tinted by that same share - `DataTableCellGauge` — the number beside a circular gauge - `DataTableCellTimestamp` — relative time with absolute tooltip - `DataTableCellBadge` — colored chip - `DataTableCellBoolean` — checkmark / dash - `DataTableCellStar` — filled yellow star / outlined muted star - `DataTableCellStatusCode` — HTTP status styling - `DataTableCellLevelIndicator` — severity dot Each takes an optional `color` (a hex string). Rather than passing it per cell, set `colorMap` on the column's display config — `generateColumns` and the sheet look the row's value up in it and pass the result through as `color`. ## Filter Functions Register custom filter functions on your table: ```ts import { inDateRange, arrSome } from "@/lib/table/filterfns"; // In your table config filterFns: { inDateRange, arrSome; } ``` - `inDateRange` — matches dates within [start, end] range - `arrSome` — matches if row value is in the filter array ## Extending TanStack Table Types ```tsx import "@tanstack/react-table"; declare module "@tanstack/react-table" { interface TableMeta { getRowClassName?: (row: Row) => string; } interface ColumnMeta { headerClassName?: string; cellClassName?: string; label?: string; } interface FilterFns { inDateRange?: FilterFn; arrSome?: FilterFn; } } ``` --- # Data Fetching Source: https://data-table.openstatus.dev/docs/data-fetching Data fetching is powered by [TanStack React Query](https://tanstack.com/query) using `useInfiniteQuery` for cursor-based pagination. A factory function creates the query options so you don't have to wire up cursors, serialization, or caching manually. See the [/infinite](/infinite) demo for a working example. ### Query Options Factory `createDataTableQueryOptions` generates `infiniteQueryOptions` for your table. It handles cursor management, search param serialization, and [SuperJSON](https://github.com/flightcontrolhq/superjson) deserialization: ```ts import { createDataTableQueryOptions } from "@/lib/data-table"; const _dataOptions = createDataTableQueryOptions({ queryKeyPrefix: "my-table", apiEndpoint: "/my-table/api", searchParamsSerializer: searchParamsSerializer, }); export const dataOptions = (search: SearchParamsType) => _dataOptions(search as unknown as Record); ``` The factory configures: - **Query key** — derived from serialized search params (excludes `cursor`, `direction`, `uuid`, `live` for stable cache keys) - **Initial cursor** — `Date.now()` (most recent data first) - **Page params** — `getNextPageParam` / `getPreviousPageParam` from `nextCursor` / `prevCursor` - **Caching** — `keepPreviousData` for smooth filter transitions, 5-minute stale time, no refetch on window focus ### useInfiniteQuery Pattern ```tsx function DataTableContent() { const search = useFilterState(); const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery( dataOptions(search), ); // Flatten pages into a single array const flatData = React.useMemo( () => data?.pages?.flatMap((page) => page.data ?? []) ?? [], [data?.pages], ); // Derive column filters from state (exclude non-filter fields) const { sort, cursor, direction, uuid, live, size, ...filter } = search; const defaultColumnFilters = React.useMemo(() => { return Object.entries(filter) .map(([key, value]) => ({ id: key, value })) .filter(({ value }) => { if (value === null || value === undefined) return false; if (Array.isArray(value) && value.length === 0) return false; return true; }); }, [filter]); return ( ); } ``` When the user scrolls to the bottom, `DataTableInfinite` calls `fetchNextPage`. React Query fetches the next page using the `nextCursor` from the last page's response and appends it to `data.pages`. ### Server Prefetch Server-side prefetching with React Query's `HydrationBoundary` avoids a loading spinner on first render: ```tsx // page.tsx import { getQueryClient } from "@/providers/get-query-client"; import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; export default async function Page({ searchParams }) { const search = await searchParamsCache.parse(searchParams); const queryClient = getQueryClient(); await queryClient.prefetchInfiniteQuery(dataOptions(search)); return ( ); } ``` The query is prefetched on the server and dehydrated into the HTML. On the client, `useInfiniteQuery` picks up the cached data immediately — no extra network request. --- # Data Layer Source: https://data-table.openstatus.dev/docs/data-layer These concepts apply regardless of your ORM — Drizzle, Prisma, or raw SQL. The [Drizzle ORM](/docs/drizzle-orm) guide shows a concrete implementation. See the [/infinite](/infinite) demo for a live example with all data layer features in action. ### API Response Shape Your API endpoint should return this shape (serialized with [SuperJSON](https://github.com/flightcontrolhq/superjson) to preserve `Date` objects): ```ts { data: ColumnSchema[]; meta: { totalRowCount: number; // Total rows in the table (unfiltered) filterRowCount: number; // Rows matching current filters chartData: BaseChartSchema[]; facets: Record; metadata?: TMeta; // Custom metadata (e.g. percentiles) }; nextCursor: number | null; // Timestamp (ms) of last row prevCursor: number | null; // Timestamp (ms) of first row } ``` ### Three-Pass Filtering A strategy to keep slider bounds stable when users adjust them. Without it, dragging a slider collapses its own min/max range. **Pass 1 — Date only** Apply only the date range filter. This gives the base time window for all facet computation. **Pass 2 — Date + non-slider filters** Add checkbox, input, and other non-slider filters. Use these conditions to compute slider facet bounds (min/max). Because slider values are excluded, moving a slider doesn't shrink its own range. **Pass 3 — All filters (including sliders)** The final set of conditions used for the data query, counts, and non-slider facets. ``` Date filters ──────────────────────────┐ ├─▶ Pass 1 → (used for date-only facets) │ + Non-slider filters ─────────────────├─▶ Pass 2 → Slider min/max bounds │ + Slider filters ─────────────────────└─▶ Pass 3 → Data, counts, checkbox facets ``` ### Faceted Search Facets provide grouped counts for filter options and min/max ranges for sliders. They're returned in `meta.facets`: ```ts type FacetMetadataSchema = { rows: Array<{ value: string | number | boolean; total: number }>; total: number; min?: number; // For slider and numeric columns max?: number; }; ``` **Checkbox facets** — grouped counts per value (e.g. `{ success: 120, error: 8 }`). Computed from Pass 3 conditions. **Slider facets** — `min`/`max` of the column. Computed from Pass 2 conditions so they don't collapse. **Array columns** — unnest the array values before grouping (e.g. PostgreSQL `unnest()`). All facet queries can run in parallel for better performance. #### Client-Side Usage Inject server-side facets into your filter fields at runtime so checkboxes show counts and sliders get dynamic bounds: ```tsx const dynamicFilterFields = React.useMemo(() => { return filterFields.map((field) => { const facetsField = facets?.[field.value as string]; if (!facetsField) return field; if (field.options && field.options.length > 0) return field; const options = facetsField.rows.map(({ value }) => ({ label: `${value}`, value, })); if (field.type === "slider") { return { ...field, min: facetsField.min ?? field.min, max: facetsField.max ?? field.max, options, }; } return { ...field, options }; }); }, [facets]); ``` ### Cursor Pagination Cursor-based pagination (not offset) is used for infinite scroll. It's stable under concurrent inserts and performs better on large tables. - Uses a timestamp column (e.g. `date`) as the cursor - **Next page** (older rows): `WHERE date < cursor ORDER BY date DESC LIMIT size` - **Previous page** (newer rows, for live mode): `WHERE date > cursor ORDER BY date ASC LIMIT size` (then reverse the results) - Client sends `cursor` + `direction`, server returns `nextCursor` + `prevCursor` ### Chart Data Optional time-series aggregation for the timeline chart. Group rows into time buckets and count by category: ```ts type BaseChartSchema = { timestamp: number; // UNIX ms (bucket start) [key: string]: number; // e.g. "success", "warning", "error" }; ``` Use `date_bin()` (PostgreSQL) or equivalent for intelligent bucketing. The interval should adapt to the date range — smaller ranges get finer buckets. ### Indexing Add database indexes on columns used for filtering, sorting, and pagination: - **Cursor column** (e.g. `date`) — required for cursor pagination performance - **Frequently filtered columns** (e.g. `level`, `status`) — speeds up WHERE clauses - **Composite indexes** — useful when filters are often combined Use `EXPLAIN ANALYZE` to verify your indexes are being used. ```ts import { index, pgTable } from "drizzle-orm/pg-core"; export const logs = pgTable( "logs", { // ... columns }, (table) => [ index("logs_date_idx").on(table.date), index("logs_level_idx").on(table.level), index("logs_status_idx").on(table.status), ], ); ``` --- # Drizzle ORM Source: https://data-table.openstatus.dev/docs/drizzle-orm A step-by-step guide to wiring up the data table with [Drizzle ORM](https://orm.drizzle.team) and PostgreSQL. See the [live example](/drizzle) and the [Database](/docs/data-layer) page for the concepts behind the implementation. ### File Checklist Every file you need to create: | File | Purpose | | ------------------------------- | ---------------------------------------------------------------- | | `db/drizzle/schema.ts` | Drizzle table definition (enums, columns) | | `db/drizzle/index.ts` | Database client | | `app/[route]/column-mapping.ts` | Maps schema keys → Drizzle columns | | `app/[route]/table-schema.tsx` | UI column config (display, filters, sorting) | | `app/[route]/schema.ts` | Zod validation + ColumnSchema type | | `app/[route]/search-params.ts` | URL search param parsers (derived from schema) | | `app/[route]/query-options.ts` | React Query infinite query config | | `app/[route]/api/route.ts` | API handler with `createDrizzleHandler` | | `app/[route]/api/ai/route.ts` | AI filter handler (optional, see [AI Filters](/docs/ai-filters)) | | `app/[route]/client.tsx` | Client component | | `app/[route]/page.tsx` | Page wrapper | ### Step 1: Define Your Drizzle Table Define your PostgreSQL table with Drizzle's schema builder: ```ts // db/drizzle/schema.ts import { integer, jsonb, pgEnum, pgTable, text, timestamp, uuid, } from "drizzle-orm/pg-core"; export const levelEnum = pgEnum("level", ["success", "warning", "error"]); export const methodEnum = pgEnum("method", ["GET", "POST", "PUT", "DELETE"]); export const logs = pgTable("logs", { uuid: uuid("uuid").defaultRandom().primaryKey(), level: levelEnum("level").notNull(), method: methodEnum("method").notNull(), host: text("host").notNull(), pathname: text("pathname").notNull(), status: integer("status").notNull(), latency: integer("latency").notNull(), regions: text("regions").array().notNull(), date: timestamp("date", { withTimezone: true }).notNull(), timingDns: integer("timing_dns").notNull(), timingConnection: integer("timing_connection").notNull(), timingTls: integer("timing_tls").notNull(), timingTtfb: integer("timing_ttfb").notNull(), timingTransfer: integer("timing_transfer").notNull(), headers: jsonb("headers").$type>().notNull(), message: text("message"), }); ``` Set up the database client using the `node-postgres` driver: ```ts // db/drizzle/index.ts import { drizzle } from "drizzle-orm/node-postgres"; import * as schema from "./schema"; const connectionString = process.env.DATABASE_URL; if (!connectionString) { throw new Error("DATABASE_URL environment variable is not set"); } export const db = drizzle(connectionString, { schema }); ``` > **Note:** If you're using a connection pooler (Supabase transaction mode, PgBouncer, Neon), make sure to use the transaction pooler URL (port `6543` for Supabase). Install `pg` as a dependency: `pnpm add pg @types/pg`. ### Step 2: Column Mapping The column mapping bridges your table schema keys to Drizzle columns. This is the **only file that couples UI to DB** — one file per table: ```ts // app/drizzle/column-mapping.ts import { logs } from "@/db/drizzle/schema"; import type { ColumnMapping } from "@/lib/drizzle"; export const columnMapping = { level: logs.level, date: logs.date, status: logs.status, latency: logs.latency, method: logs.method, host: logs.host, pathname: logs.pathname, regions: logs.regions, "timing.dns": logs.timingDns, "timing.connection": logs.timingConnection, "timing.tls": logs.timingTls, "timing.ttfb": logs.timingTtfb, "timing.transfer": logs.timingTransfer, } satisfies ColumnMapping; ``` Keys can use dot notation (`"timing.dns"`) to map nested UI names to flat database columns (`timingDns`). ### Step 3: Table Schema Define how each column appears in the UI — labels, display format, filter type, sizing: ```tsx // app/drizzle/table-schema.tsx import { col, createTableSchema } from "@/lib/table-schema"; export const tableSchema = createTableSchema({ // Checkbox filter with enum options level: col .enum(["success", "warning", "error"]) .label("Level") .filterable("checkbox", { options: [ { label: "success", value: "success" }, { label: "warning", value: "warning" }, { label: "error", value: "error" }, ], }) .size(80), // Timerange filter for date column date: col .timestamp() .label("Date") .display("timestamp") .defaultOpen() .size(200) .sortable(), // Checkbox filter with number options status: col .number() .label("Status") .filterable("checkbox", { options: [ { label: "200", value: 200 }, { label: "400", value: 400 }, { label: "404", value: 404 }, { label: "500", value: 500 }, ], }) .size(60), // Slider filter with range latency: col .number() .label("Latency") .filterable("slider", { min: 0, max: 5000, unit: "ms" }) .size(110) .sortable(), // Text input filter host: col.string().label("Host").filterable("input").size(125), // ... add more columns as needed }); ``` The filter type you choose here (`checkbox`, `slider`, `input`, `timerange`) determines how the [three-pass filtering strategy](/docs/data-layer#three-pass-filtering) handles each column. Additional methods: `hidden()` to hide by default, `sortable()` for sort support, `sheet()` for the row detail drawer, `resizable()` for resizable columns. ### Step 4: API Route The API route uses `createDrizzleHandler` which implements the [three-pass filtering strategy](/docs/data-layer#three-pass-filtering), faceted search, counts, and cursor pagination: ```ts // app/drizzle/api/route.ts import { db } from "@/db/drizzle"; import { logs } from "@/db/drizzle/schema"; import { createDrizzleHandler } from "@/lib/drizzle"; import { NextRequest } from "next/server"; import SuperJSON from "superjson"; import { columnMapping } from "../column-mapping"; import { searchParamsCache } from "../search-params"; import { tableSchema } from "../table-schema"; export const dynamic = "force-dynamic"; const handler = createDrizzleHandler({ db, table: logs, schema: tableSchema.definition, // Auto-derives slider/facet/date keys columnMapping, cursorColumn: "date", defaultSize: 40, }); export async function GET(req: NextRequest): Promise { const _search: Map = new Map(); req.nextUrl.searchParams.forEach((value, key) => _search.set(key, value)); const search = searchParamsCache.parse(Object.fromEntries(_search)); const result = await handler.execute(search as Record); // Map DB rows to your ColumnSchema shape const data = result.data.map((row) => ({ uuid: row.uuid, level: row.level, status: row.status, latency: row.latency, host: row.host, date: row.date, // ... map all columns })); return Response.json( SuperJSON.stringify({ data, meta: { totalRowCount: result.totalRowCount, filterRowCount: result.filterRowCount, chartData: [], // Add chart data query if needed facets: result.facets, }, prevCursor: result.prevCursor, nextCursor: result.nextCursor, }), ); } ``` `createDrizzleHandler` accepts either `schema: tableSchema.definition` (auto-derives filter types) or explicit `sliderKeys`, `facetKeys`, and `dateKeys` arrays when the table schema can't be imported on the server. > To add AI-powered filtering, create a second route at `app/[route]/api/ai/route.ts` using `createAIFilterHandler` from `@/lib/ai`. It reuses the same `tableSchema` to generate the AI prompt and output schema. See [AI Filters — API Route Setup](/docs/ai-filters#api-route-setup). ### Step 5: Client Component Wire up `useInfiniteQuery` to `DataTableInfinite`. The key pattern is populating filter fields dynamically from the facet data returned by the API: ```tsx // app/drizzle/client.tsx "use client"; import { DataTableInfinite } from "@/components/data-table/data-table-infinite"; import { DataTableStoreProvider, useFilterState } from "@/lib/store"; import { useNuqsAdapter } from "@/lib/store/adapters/nuqs"; import { generateColumns, generateFilterFields, getDefaultColumnVisibility, } from "@/lib/table-schema"; import { useInfiniteQuery } from "@tanstack/react-query"; import * as React from "react"; import { dataOptions } from "./query-options"; import type { ColumnSchema, FilterState } from "./schema"; import { filterSchema } from "./schema"; import { tableSchema } from "./table-schema"; const columns = generateColumns(tableSchema.definition); const filterFields = generateFilterFields(tableSchema.definition); const defaultColumnVisibility = getDefaultColumnVisibility( tableSchema.definition, ); export function Client() { const adapter = useNuqsAdapter(filterSchema.definition, { id: "drizzle" }); return ( ); } function ClientInner() { const search = useFilterState(); const { data, isFetching, isLoading, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery(dataOptions(search)); const flatData = React.useMemo( () => data?.pages?.flatMap((page) => page.data ?? []) ?? [], [data?.pages], ); const lastPage = data?.pages?.[data?.pages.length - 1]; const facets = lastPage?.meta?.facets; // Populate filter options from server-side facet data const dynamicFilterFields = React.useMemo(() => { return filterFields.map((field) => { const facetsField = facets?.[field.value as string]; if (!facetsField) return field; if (field.options && field.options.length > 0) return field; const options = facetsField.rows.map(({ value }) => ({ label: `${value}`, value, })); if (field.type === "slider") { return { ...field, min: facetsField.min ?? field.min, max: facetsField.max ?? field.max, options, }; } return { ...field, options }; }); }, [facets]); const { sort, size, uuid, cursor, direction, live, ...filter } = search; const defaultColumnFilters = React.useMemo(() => { return Object.entries(filter) .map(([key, value]) => ({ id: key, value })) .filter(({ value }) => { if (value === null || value === undefined) return false; if (Array.isArray(value) && value.length === 0) return false; return true; }); }, [filter]); return ( ); } ``` ### Step 6: Page Wrapper A minimal server component with `HydrationBoundary` for React Query: ```tsx // app/drizzle/page.tsx import { getQueryClient } from "@/providers/get-query-client"; import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; import { Client } from "./client"; export const dynamic = "force-dynamic"; export default function Page() { return ( ); } ``` ### Helper Functions Reference The `@/lib/drizzle` module exports four functions that `createDrizzleHandler` uses internally. You can also use them directly for custom query logic: ```ts import { buildWhereConditions, buildOrderBy, buildCursorPagination, computeFacets, } from "@/lib/drizzle"; ``` - **`buildWhereConditions(mapping, filters, options?)`** — converts filter state to SQL WHERE clauses. Handles `ilike` for strings, `between` for slider ranges, `inArray` for checkboxes, date ranges, and PostgreSQL array overlap (`&&`) for array columns. - **`buildOrderBy(mapping, sort)`** — returns an `asc` or `desc` SQL clause from a sort descriptor. - **`buildCursorPagination({ cursor, direction, size, cursorColumn })`** — returns cursor condition, order clause, and a `needsReverse` flag for bidirectional infinite scroll. - **`computeFacets(db, table, mapping, conditions, facetKeys, options?)`** — computes grouped counts (or min/max for slider keys) via parallel SQL queries. --- # Features Source: https://data-table.openstatus.dev/docs/features Try all features live in the [/infinite](/infinite) demo. ### Timeline Chart The timeline chart visualizes data distribution over time as a stacked bar chart. ```ts type BaseChartSchema = { timestamp: number; // UNIX ms [key: string]: number; // Level counts, e.g. "success", "warning", "error" }; ``` Pass chart data via the `chartData` and `chartDataColumnId` props on `DataTableInfinite`. Selecting a range on the chart sets the corresponding date filter. #### Zoom The timeline chart supports click-and-drag zoom. Click and drag across bars to select a time range — a `ReferenceArea` marks the selection with a line and grip on each end, fades the buckets outside it, and a card shows the range, duration, and row count as you drag. On mouse up the selection is parked rather than applied: the same card grows **Cancel** and **Zoom** buttons, so confirm with **Zoom** (or `Enter`) to set the date filter on the table, or discard with **Cancel** (or `Escape`). The axis tick format adapts automatically based on the visible time span (seconds for ≤10 min, hours for ≤1 day, day+hour for ≤1 week, full date otherwise). ### Live Mode Live mode polls for new data every 5 seconds using `fetchPreviousPage` from `useInfiniteQuery`. New rows are prepended to the data array. The `live` field in the filter schema controls this: ```ts live: field.boolean().default(false), ``` When live mode is active, rows older than the activation timestamp are visually dimmed. ### Keyboard Shortcuts Built-in keyboard shortcuts for common interactions: - `Cmd+K` — Toggle command palette - `Cmd+B` — Toggle filter sidebar - `Cmd+U` — Reset column state (order, visibility) - `Cmd+J` — Toggle live mode - `Esc` — Reset table filters - `Cmd+.` — Reset element focus - `Cmd+Shift+X` — Deselect all rows (when rows are selected) > Row selection requires `col.select()` in your [Table Schema](/docs/table-schema#row-selection-with-colselect). Use [DataTableFloatingBar](/docs/ui-components#datatablefloatingbar) to add bulk action buttons to the selection bar. --- # AI Filters Source: https://data-table.openstatus.dev/docs/ai-filters The `data-table-filter-command-ai` block adds natural language filtering to your data table. Type a query like _"5xx errors in production last 24h"_ and the AI translates it into structured filters — applied progressively as the response streams in. Provider-agnostic. Works with any LLM via the [Vercel AI SDK](https://sdk.vercel.ai). ## Installation ```bash npx shadcn@latest add https://data-table.openstatus.dev/r/data-table-filter-command-ai.json ``` This installs the AI command palette component and the `@/lib/ai` utilities. ## Prerequisites - The **data-table-filter-command** block must be installed (auto-installed as a dependency) - The **data-table-schema** block must be installed — AI context is generated from your table schema - An LLM provider package (e.g., `@ai-sdk/anthropic`, `@ai-sdk/openai`) ```bash pnpm add @ai-sdk/anthropic ``` ## API Route Setup Create a POST route that streams AI-inferred filter state. The `createAIFilterHandler` factory generates the system prompt and Zod output schema from your table schema automatically. ### Direct Provider ```ts // app/api/ai-filters/route.ts import { anthropic } from "@ai-sdk/anthropic"; import { createAIFilterHandler } from "@/lib/ai"; import { tableSchema } from "../table-schema"; export const POST = createAIFilterHandler({ model: anthropic("claude-sonnet-4-20250514"), schema: tableSchema.definition, }); ``` Set your API key in `.env`: ``` ANTHROPIC_API_KEY=sk-ant-... ``` Swap `anthropic(...)` for any Vercel AI SDK provider — `openai("gpt-4o-mini")`, `google("gemini-2.0-flash")`, etc. ### Vercel AI Gateway The [live demo](/drizzle) uses the Vercel AI Gateway, which provides a unified endpoint for multiple providers: ```ts // app/api/ai-filters/route.ts import { createAnthropic } from "@ai-sdk/anthropic"; import { createAIFilterHandler } from "@/lib/ai"; import { tableSchema } from "../table-schema"; const anthropic = createAnthropic({ baseURL: "https://ai-gateway.vercel.sh/v1", apiKey: process.env.AI_GATEWAY_API_KEY, }); export const POST = createAIFilterHandler({ model: anthropic("anthropic/claude-sonnet-4-20250514"), schema: tableSchema.definition, }); ``` ## Client Integration Drop `DataTableFilterAICommand` into the `commandSlot` prop of `DataTableInfinite`: ```tsx import { DataTableFilterAICommand } from "@/components/data-table/data-table-filter-command-ai"; } />; ``` ### Props | Prop | Type | Description | | ------------- | ----------------------- | --------------------------------------------------------- | | `schema` | `SchemaDefinition` | BYOS filter schema for structured query parsing | | `tableSchema` | `TableSchemaDefinition` | Table schema for AI context and output schema generation | | `api` | `string` | API endpoint path (e.g., `"/api/ai-filters"`) | | `tableId` | `string` | Unique ID for localStorage history (default: `"default"`) | ## How It Works The AI command palette shares the same input as the regular command palette. What happens when you press Enter depends on the input: 1. **Structured input** (`host:api`, `regions:ams,gru`, `latency:100-500`) — parsed instantly by the existing command palette. No AI involved. 2. **Natural language** (_"slow requests from eu regions"_) — sent to your API route. The AI streams back a structured JSON object matching your table schema's filter types. The response is **applied progressively** as it streams: - **Input** and **checkbox** filters update immediately as values arrive - **Slider** and **timerange** filters wait until both bounds are present before applying (to avoid flashing partial ranges) After the stream completes, a final validation pass clamps slider values to defined bounds, strips invalid checkbox options, and converts ISO date strings to Date objects. The command palette also stores the last 5 searches in localStorage (namespaced by `tableId`) for quick re-use. ## Schema Considerations The quality of AI filter inference depends directly on the metadata in your table schema. Two things matter most: ### Add descriptions to your columns Descriptions are essential for AI filtering accuracy. Without them, the AI only sees field names and types, which can lead to ambiguous results. A `description` tells the AI what each column represents: ```ts const tableSchema = createTableSchema({ host: col .string() .label("Host") .description("Origin server hostname, e.g. api.example.com") .filterable("input"), latency: col .number() .label("Latency") .description("Response time in milliseconds") .filterable("slider", { min: 0, max: 5000, unit: "ms" }), }); ``` The description, label, allowed values, min/max bounds, and unit are all included in the AI prompt. The more context you provide, the better the inference. ### `commandDisabled` fields are still available to AI Fields with `.commandDisabled()` are hidden from the manual command palette suggestions, but they are **still included in the AI prompt**. This is intentional — some fields are hard to use with `key:value` syntax but easy to express in natural language. For example, the `date` column in the [live demo](/drizzle) has `commandDisabled()` because manually typing ISO date ranges is impractical. But saying _"last 24 hours"_ or _"this week"_ works naturally with the AI. ```ts date: col .timestamp() .label("Date") .commandDisabled() // Hidden from manual palette, available to AI .sortable(), ``` ## Customization ### Swapping providers Replace the model in your API route. Any [Vercel AI SDK provider](https://sdk.vercel.ai/providers) works: ```ts import { openai } from "@ai-sdk/openai"; export const POST = createAIFilterHandler({ model: openai("gpt-4o-mini"), schema: tableSchema.definition, }); ``` ### Prompt caching The handler automatically applies Anthropic prompt caching to the static system prompt via `cacheControl: { type: "ephemeral" }`. This means repeated queries reuse the cached prompt, reducing latency and cost. This works out of the box with Anthropic models — no configuration needed. ### Using the library exports directly If you don't use the Vercel AI SDK, you can use the library exports to build a custom integration: ```ts import { generateAIPrompt, generateAIOutputSchema, parseAIResponse, } from "@/lib/ai"; // Generate the prompt and Zod schema from your table schema const prompt = generateAIPrompt(tableSchema.definition, { now: new Date() }); const schema = generateAIOutputSchema(tableSchema.definition); // Call your LLM with the prompt and schema const response = await yourLLM({ system: prompt, schema }); // Validate and apply the response const state = parseAIResponse(tableSchema.definition, response); if (state) { for (const [key, value] of Object.entries(state)) { table.getColumn(key)?.setFilterValue(value); } } ``` --- # Builder Source: https://data-table.openstatus.dev/docs/builder The [/builder](/builder) page lets you visually build a table schema: 1. **Paste JSON data** (or upload a CSV) in the left panel 2. Click **Generate Schema** — the schema is auto-inferred from your data 3. A **live table preview** appears with filters, sorting, and row details 4. Edit the **Schema JSON** directly and click **Apply** for instant updates 5. Click **Export TS** to copy the generated `createTableSchema(...)` code ## Type Inference The inference engine detects column types from your data: - ISO 8601 strings / Unix-ms numbers → `timestamp` + `timerange` filter - Booleans → `boolean` + `checkbox` filter - Numbers with varying values → `number` + `slider` filter - Strings with ≤10 distinct values → `enum` + `checkbox` filter - Arrays of strings → `array` with enum items - Plain objects → `record` (not filterable) Post-inference heuristics apply smart defaults (ID-like → code display, latency → "ms" unit, log level → `defaultOpen`, etc.). > The same inference engine powers the [zero-config DataTableAuto](/auto) component — pass raw data and get a fully rendered table with no schema definition needed. ## Example Datasets Example datasets (Pokemon, HTTP Logs, Space Missions, Cocktails, Orders, Issues, Movies, RPG Characters) are included to explore different configurations. ## Serialization & AI The schema serializes to a function-free JSON descriptor for AI agents, MCP tools, or remote storage. ```ts // Schema → JSON const json = tableSchema.toJSON(); // JSON.stringify(tableSchema) also works // JSON → Schema (e.g. from an AI agent) const reconstructed = createTableSchema.fromJSON(json); // JSON → TypeScript code import { schemaToTypeScript } from "@/lib/table-schema/to-typescript"; const tsCode = schemaToTypeScript(json); ``` > Custom renderers (`display("custom", ...)`, `filter.component`, `sheet.component`) are functions and cannot be serialized. They are stripped during `toJSON()` and must be applied manually on reconstructed schemas. --- # Full Example Source: https://data-table.openstatus.dev/docs/full-example This wires together everything from the previous sections. Each step links back to its guide page for details. ## 1. Define Your Table Use `createTableSchema` to declare every column — display, filters, sorting, and row details — in one place. See [Table Schema](/docs/table-schema) for the full API. ```tsx // table-schema.tsx import { col, createTableSchema, type InferTableType, } from "@/lib/table-schema"; export const tableSchema = createTableSchema({ level: col.presets.logLevel(["error", "warn", "info", "debug"]), date: col.presets.timestamp().label("Date").size(200).sheet(), latency: col.presets .duration("ms") .label("Latency") .sortable() .size(110) .sheet(), status: col.presets.httpStatus().label("Status").size(60), method: col.presets.httpMethod(["GET", "POST", "DELETE"]).size(69), host: col.string().label("Host").size(125).sheet(), }); export type ColumnSchema = InferTableType; ``` ## 2. Create Filter Schema Bridge the table schema to filter state with `generateFilterSchema`, then add non-column fields like sort and pagination. See [State Management](/docs/state-management) for details. ```tsx // schema.ts import { createSchema, field } from "@/lib/store/schema"; import { generateFilterSchema } from "@/lib/table-schema"; export const filterSchema = createSchema({ ...generateFilterSchema(tableSchema.definition).definition, sort: field.sort(), live: field.boolean().default(false), size: field.number().default(40), }); export type FilterState = typeof filterSchema._type; ``` ## 3. Set Up the API Route Create an API handler with `createDrizzleHandler` that implements three-pass filtering, faceted search, and cursor pagination. See [Drizzle ORM](/docs/drizzle-orm) and [Data Fetching](/docs/data-fetching) for the underlying concepts. ```tsx // app/api/route.ts import { createDrizzleHandler } from "@/lib/drizzle"; const handler = createDrizzleHandler({ db, table: logs, schema: tableSchema.definition, columnMapping, cursorColumn: "date", }); export async function GET(req: NextRequest) { const search = searchParamsCache.parse( Object.fromEntries(req.nextUrl.searchParams), ); const result = await handler.execute(search); return Response.json( SuperJSON.stringify({ data: result.data, meta: { totalRowCount: result.totalRowCount, filterRowCount: result.filterRowCount, facets: result.facets, }, prevCursor: result.prevCursor, nextCursor: result.nextCursor, }), ); } ``` ## 4. Render Wire up `useInfiniteQuery` with `DataTableInfinite` and populate filter fields dynamically from server-side facet data. See [Components](/docs/ui-components) for all available props. ```tsx // client.tsx import { DataTableStoreProvider, useFilterState } from "@/lib/store"; import { useNuqsAdapter } from "@/lib/store/adapters/nuqs"; import { generateColumns, generateFilterFields, generateSheetFields, } from "@/lib/table-schema"; import { useInfiniteQuery } from "@tanstack/react-query"; const columns = generateColumns(tableSchema.definition); const filterFields = generateFilterFields(tableSchema.definition); const sheetFields = generateSheetFields(tableSchema.definition); export function Client() { const adapter = useNuqsAdapter(filterSchema.definition, { id: "logs" }); return ( ); } function DataTableContent() { const search = useFilterState(); const { data, isFetching, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery(dataOptions(search)); const flatData = React.useMemo( () => data?.pages?.flatMap((page) => page.data ?? []) ?? [], [data?.pages], ); const { sort, live, size, ...filter } = search; const defaultColumnFilters = Object.entries(filter) .map(([key, value]) => ({ id: key, value })) .filter( ({ value }) => value != null && !(Array.isArray(value) && !value.length), ); return ( } /> ); } ``` > The `commandSlot` above uses the AI-enhanced command palette. It handles both structured queries (`host:api`) and natural language (_"slow requests from eu"_) in the same input. See [AI Filters](/docs/ai-filters) for the full setup including the API route. ## Live Examples | Route | Pagination | Data Source | Live Mode | Chart | | ---------------------- | --------------- | ----------- | --------- | ----- | | [/auto](/auto) | Infinite scroll | In-memory | No | No | | [/default](/default) | Client-side | In-memory | No | No | | [/infinite](/infinite) | Infinite scroll | Mock API | Yes | Yes | | [/drizzle](/drizzle) | Infinite scroll | PostgreSQL | Yes | Yes | | [/light](/light) | Infinite scroll | Tinybird | Yes | Yes | --- # Debugging Source: https://data-table.openstatus.dev/docs/debugging We use `react-scan` for performance debugging: ``` # .env.local NEXT_PUBLIC_REACT_SCAN=true ``` TanStack Query devtools appear automatically in development. For table debug logs: ``` NEXT_PUBLIC_TABLE_DEBUG=true ``` Try the debugging tools on the [/infinite](/infinite) demo — it has all three enabled in development. --- # MCP Server Source: https://data-table.openstatus.dev/docs/mcp The `data-table-mcp` block exposes your data table as an MCP endpoint. AI agents can discover the table's schema via `tools/list` and query it with filters, cursor-based pagination, sorting, and faceted stats via a single `query_table` tool. Stateless. Serverless-compatible. Schema auto-generated from your existing field definitions. > Looking for the other direction? These docs are themselves an MCP server, so > an agent can ask how to build the table before it queries one — see > [For AI Agents](/docs/agents). ## Installation ```bash npx shadcn@latest add https://data-table.openstatus.dev/r/data-table-mcp.json ``` This installs `@/lib/mcp` and the `@modelcontextprotocol/sdk` dependency. ## Prerequisites - The **data-table** block must be installed (provides the BYOS schema system) - A data source — any async function that accepts filters and returns rows + total ## Route Setup Create a route handler co-located with your table's API. Export POST, GET, and DELETE for Streamable HTTP spec compliance. POST carries every request; GET is answered with 405, since the handler is stateless and has nothing to push on the standalone SSE stream. The recommended pattern derives the MCP schema from your existing `filterSchema`, excluding UI-only fields (`live`, `uuid`) via destructuring. This automatically includes all filter fields, sorting, and cursor-based pagination. ### In-Memory Example ```ts // app/infinite/api/mcp/route.ts import { createTableMCPHandler } from "@/lib/mcp"; import { filterData, getFacetsFromData, sortData, splitData, } from "@/app/infinite/api/helpers"; import { filterSchema } from "@/app/infinite/filter-schema"; import { mock, mockLive } from "@/app/infinite/api/mock"; import type { SchemaDefinition } from "@/lib/store/schema"; // Derive MCP schema from filterSchema, excluding UI-only fields const { live, uuid, ...mcpSchema } = filterSchema.definition satisfies SchemaDefinition; const handler = createTableMCPHandler({ name: "data-table-filters", description: "Query HTTP request logs with filters for level, method, host, pathname, latency, status, regions, date range, sort, and cursor-based pagination (cursor, size, direction)", schema: mcpSchema, getData: async ({ filters }) => { const totalData = [...mockLive, ...mock]; const filtered = filterData(totalData, filters); const sorted = sortData(filtered, filters.sort ?? null); const paginated = splitData(sorted, { cursor: filters.cursor ?? null, size: filters.size ?? 40, direction: filters.direction ?? "next", }); const facets = getFacetsFromData(filtered); const total = filtered.length; const rows = paginated.map((row) => ({ ...row, date: row.date.toISOString(), })); return { rows, total, facets }; }, }); export { handler as POST, handler as GET, handler as DELETE }; ``` ### Drizzle/Postgres Example ```ts // app/drizzle/api/mcp/route.ts import { createTableMCPHandler } from "@/lib/mcp"; import { db } from "@/db/drizzle"; import { logs } from "@/db/drizzle/schema"; import { createDrizzleHandler } from "@/lib/drizzle"; import { filterSchema } from "@/app/infinite/filter-schema"; import { columnMapping } from "../../column-mapping"; import { tableSchema } from "../../table-schema"; import type { SchemaDefinition } from "@/lib/store/schema"; const { live, uuid, ...mcpSchema } = filterSchema.definition satisfies SchemaDefinition; const drizzleHandler = createDrizzleHandler({ db, table: logs, schema: tableSchema.definition, columnMapping, cursorColumn: "date", defaultSize: 40, }); const handler = createTableMCPHandler({ name: "data-table-drizzle", description: "Query HTTP request logs (Drizzle/Postgres) with filters for level, method, host, pathname, latency, status, regions, date range, sort, and cursor-based pagination (cursor, size, direction)", schema: mcpSchema, getData: async ({ filters }) => { const result = await drizzleHandler.execute( filters as Record, ); type LogRow = typeof logs.$inferSelect; const rows = result.data.map((row) => { const r = row as LogRow; return { uuid: r.uuid, level: r.level, method: r.method, host: r.host, pathname: r.pathname, status: r.status, latency: r.latency, regions: r.regions, date: r.date.toISOString(), message: r.message ?? undefined, "timing.dns": r.timingDns, "timing.connection": r.timingConnection, "timing.tls": r.timingTls, "timing.ttfb": r.timingTtfb, "timing.transfer": r.timingTransfer, }; }); return { rows, total: result.filterRowCount, facets: result.facets }; }, }); export { handler as POST, handler as GET, handler as DELETE }; ``` ## Config | Option | Type | Default | Description | | ------------- | ------------------------ | -------------- | ---------------------------------------- | | `schema` | `SchemaDefinition` | — | BYOS field definitions for filter params | | `description` | `string` | — | Tool description (shown to agents) | | `getData` | `(opts) => Promise<...>` | — | Data source function | | `name` | `string` | `"data-table"` | MCP server name in `serverInfo` | ## The `query_table` Tool The handler registers a single MCP tool with these parameters: | Parameter | Type | Default | Description | | --------- | ------------------- | -------- | ------------------------------- | | `filters` | object (optional) | `{}` | Auto-generated from your schema | | `format` | `"json" \| "stats"` | `"json"` | Output format | ### Pagination & Sorting Pagination and sorting are part of the `filters` object, derived from your `filterSchema`: | Filter field | Type | Default | Description | | ------------ | ------------------ | -------- | -------------------- | | `cursor` | `number` (Unix ms) | now | Cursor timestamp | | `size` | `number` | `40` | Rows per page | | `direction` | `"prev" \| "next"` | `"next"` | Pagination direction | | `sort` | `{ id, desc }` | — | Sort descriptor | ### Output formats **`json`** — returns rows with total count: ```json { "rows": [...], "total": 1234 } ``` **`stats`** — returns total count and facet metadata (no rows): ```json { "total": 1234, "facets": { "level": { "rows": [{ "value": "error", "total": 42 }], "total": 1234 } } } ``` ## Schema Mapping Your BYOS field types are automatically converted to typed tool parameters: | Field type | Tool parameter type | | --------------------------------------- | ---------------------------- | | `field.string()` | `string` (optional) | | `field.number()` | `number` (optional) | | `field.boolean()` | `boolean` (optional) | | `field.timestamp()` | `number` (optional, Unix ms) | | `field.stringLiteral(["a", "b"])` | `enum ["a", "b"]` (optional) | | `field.array(field.number())` | `array of number` (optional) | | `field.array(field.stringLiteral(...))` | `array of enum` (optional) | Timestamp fields accept Unix milliseconds over the wire and are deserialized to `Date` objects before reaching your `getData` function. ## Connecting Clients ### Claude Code Add to `.mcp.json` in your project root: ```json { "mcpServers": { "data-table-infinite": { "type": "http", "url": "http://localhost:3000/infinite/api/mcp" }, "data-table-drizzle": { "type": "http", "url": "http://localhost:3000/drizzle/api/mcp" } } } ``` ### Verification (curl) ```bash # Initialize curl -X POST http://localhost:3000/infinite/api/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' # List tools (see auto-generated schema) curl -X POST http://localhost:3000/infinite/api/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' # Query with filters curl -X POST http://localhost:3000/infinite/api/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"query_table","arguments":{"filters":{"level":["error"],"size":5},"format":"stats"}}}' ``` ## Use Cases ### Conversational data exploration Connect your MCP endpoint to Claude Desktop or Cursor. Ask questions in natural language — the agent translates them into `query_table` calls with the right filters, paginates through results, and summarizes findings. > "Show me all 500 errors from the HKG region in the last hour" ### Monitoring and alerting An autonomous agent polls `format: "stats"` on a schedule, watching facet distributions for anomalies. When the error rate spikes or latency percentiles drift, it triggers alerts via Slack, PagerDuty, or email — no dashboard required. ### Post-deploy validation After a deploy, a CI/CD agent queries the logs table for error spikes in a short window. If the error count exceeds a threshold compared to the pre-deploy baseline, it flags the release for rollback. ### Cross-table orchestration Multiple MCP endpoints (logs, orders, customers) each expose a `query_table` tool. An agent queries across all of them to answer cross-cutting questions like "which customers experienced 500 errors today?" — joining data that lives in separate tables or services. ### Internal tooling for non-developers Teams that don't have dashboard access or SQL knowledge can query production data through a chat interface. The MCP schema provides guardrails — agents can only filter on defined fields, preventing arbitrary queries. ### Agent-to-agent pipelines One agent's output feeds another's input. A triage agent identifies error patterns from the logs table, then passes those patterns to a root-cause agent that correlates with deploy metadata from a different MCP endpoint. ## How It Works 1. `createTableMCPHandler` converts your BYOS schema to a Zod schema at startup 2. The MCP SDK auto-converts Zod to JSON Schema for `tools/list` responses 3. On each request, a fresh `McpServer` + `WebStandardStreamableHTTPServerTransport` is created (stateless) 4. Tool calls are validated by the SDK via Zod, then timestamp values are deserialized (`number` → `Date`) 5. Your `getData` function is called with typed filters (including pagination and sort) 6. Results are formatted as JSON (`rows` + `total`) or stats (`total` + `facets`) ## Dependencies - `@modelcontextprotocol/sdk` — MCP protocol implementation - `zod` — Schema validation (already installed with the core block) --- # For AI Agents Source: https://data-table.openstatus.dev/docs/agents Most decisions about which data table to use now happen inside an agent's context window. These endpoints exist so an agent can read the library, pick the right blocks, and wire them up without guessing. ## Machine-readable endpoints | Endpoint | What it is | | ----------------------------------------------------------------------- | ---------------------------------------------------- | | `/api/mcp` | These docs as an MCP server — see below | | [`/llms.txt`](https://data-table.openstatus.dev/llms.txt) | Index: blocks, install recipes, links to every page | | [`/llms-full.txt`](https://data-table.openstatus.dev/llms-full.txt) | Every documentation page inlined in one file | | [`/r/index.md`](https://data-table.openstatus.dev/r/index.md) | Block catalog with install commands and dependencies | | [`/r/registry.json`](https://data-table.openstatus.dev/r/registry.json) | The shadcn registry manifest | | `/docs/.md` | Any docs page as raw markdown | Every docs page has a **Copy as markdown** button, and appending `.md` to any docs URL returns the source — [`/docs/drizzle-orm.md`](https://data-table.openstatus.dev/docs/drizzle-orm.md), for example. Agents fetching the rendered HTML pay several times the tokens and lose code-fence fidelity on the way through. ## Ask the docs instead of reading them The endpoints above hand an agent pages. `/api/mcp` serves the same content as an MCP server, so the agent asks a question and gets back the section that answers it — a targeted section costs a fraction of the page it lives in, and the page costs a fraction of `llms-full.txt`. ```bash claude mcp add --transport http data-table-filters https://data-table.openstatus.dev/api/mcp ``` Any MCP client works — it speaks Streamable HTTP, needs no auth, and holds no session: ```json { "mcpServers": { "data-table-filters": { "type": "http", "url": "https://data-table.openstatus.dev/api/mcp" } } } ``` Four read-only tools: | Tool | Ask it | | ------------------ | ----------------------------------------------------------------- | | `get_install_plan` | "I need a table over 2M Postgres rows" → the exact shadcn command | | `search_docs` | "how do faceted counts work" → the sections that answer it | | `get_doc` | A page in full, or one section of it, as markdown | | `list_blocks` | The catalog: what each block adds and when it applies | Start with `get_install_plan` — it returns the command, the wiring notes, and the pages to read next, which is the whole decision an agent has to make before it writes any code. > This is the docs server. The [`data-table-mcp`](/docs/mcp) block is the other > direction: it turns _your_ table into an MCP endpoint so agents can query your > data. ## Agent rules and skills **Claude Code** — install the plugin, which ships the full skill with reference docs for every block: ```bash /plugin marketplace add openstatushq/data-table-filters /plugin install data-table-filters@openstatus ``` **Any agent with the `skills` CLI:** ```bash npx skills add https://github.com/openstatushq/data-table-filters --skill data-table-filters ``` **Cursor** — copy [`.cursor/rules/data-table-filters.mdc`](https://github.com/openstatushq/data-table-filters/blob/main/.cursor/rules/data-table-filters.mdc) into your project. **Everything else** — [`AGENTS.md`](https://github.com/openstatushq/data-table-filters/blob/main/AGENTS.md) in the repository root covers both using the library and contributing to it. Then describe what you want: > Add a filterable table for my `logs` Postgres table with server-side filtering > and infinite scroll ## Install recipes ### Large table — 100k+ rows, filtered in SQL ```bash npx shadcn@latest add \ https://data-table.openstatus.dev/r/data-table.json \ https://data-table.openstatus.dev/r/data-table-schema.json \ https://data-table.openstatus.dev/r/data-table-cell.json \ https://data-table.openstatus.dev/r/data-table-sheet.json \ https://data-table.openstatus.dev/r/data-table-drizzle.json \ https://data-table.openstatus.dev/r/data-table-query.json \ https://data-table.openstatus.dev/r/data-table-nuqs.json ``` Define the table once with [`createTableSchema`](/docs/table-schema), pass it to [`createDrizzleHandler`](/docs/drizzle-orm) in a route handler and to [`createDataTableQueryOptions`](/docs/data-fetching) on the client. Filtering, faceted counts, sorting, and cursor pagination execute in SQL, and rows are virtualized — the client only ever holds the pages it rendered. ### Client-side table — data already in memory ```bash npx shadcn@latest add \ https://data-table.openstatus.dev/r/data-table.json \ https://data-table.openstatus.dev/r/data-table-cell.json \ https://data-table.openstatus.dev/r/data-table-sheet.json ``` Use `useMemoryAdapter`. No API route and no schema block required. ### Unknown data shape Install `data-table` and `data-table-schema`, then render `` — see [Auto-infer](/docs/quick-start#zero-config). ## Why the schema matters for agents One `createTableSchema` definition drives six surfaces: 1. Column definitions and cell renderers 2. Filter controls 3. The row detail sheet 4. The server-side query handler 5. The natural-language filter parser 6. The [MCP tool schema](/docs/mcp) An agent writing a table from scratch has to keep those six in sync by hand, which is where hand-rolled tables quietly go wrong. Here the schema is written once and everything else is derived from it. ## Expose your table to agents at runtime The docs above are for agents that _build_ the table. The [`data-table-mcp`](/docs/mcp) block is for agents that _query_ it: it turns the same schema into an MCP endpoint, so an agent can filter and page through your data with typed parameters.