---
title: "Dynamic Records"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/dynamic-records.html"
reviewed_at: "2026-09-14"
authority: Official
---

# Dynamic Records

Renders tenant-owned custom modules from their validated field schema with secured list, search, create, detail, edit, status, relations, activity, and favorites.

## In brief

- **Runtime key:** module.dynamic-records
- **Experience:** Builder-defined data
- **Security:** Server-enforced

Use Dynamic Records for the domain-specific objects that make an app unique—such as assets, inspections, applications, policies, opportunities, or inventory—without writing a new native renderer for every object type.

[Open SaaS Apps](https://dev.buildwithhq.com/apps) [Page editor](../builder-guide/page-editor.html) [Menus & user types](../builder-guide/menus-user-types.html) [Templates](../builder-guide/templates-forking.html)

Note

The bundled renderer key is module.dynamic-records. First-class means BuildWithHQ supplies a native, typed, secured runtime experience inside normal app provisioning and the signed-in user's existing permissions.

## What it does

Renders tenant-owned custom modules from their validated field schema with secured list, search, create, detail, edit, status, relations, activity, and favorites.

## Key capabilities

- Load only a validated tenant-owned module key supplied by the page contract.
- Render the public field schema while excluding hidden and encrypted fields.
- Search, create, inspect, edit, and change status according to existing permissions.
- Participate in relations, activity, location scope, and favorites through universal RecordId.

## Common uses

- Industry-specific business objects.
- Rapidly modeled MVP records and internal tools.
- Reusable template-defined objects that do not need a specialized native experience.

## How it connects

Dynamic records compose with all universal record features and can relate to Contacts, Calendar, Files, Conversations, Work Orders, and each other. A specialized module can later replace the generic presentation while preserving canonical record identity.

Where applicable, its records use the universal `RecordId` conventions so they can participate in secured relationships, activity history, favorites, dynamic fields, notifications, Inbox attention, and global search without copying the source record.

## Security and data boundary

The server accepts only tenant-owned custom module keys and refuses generic access to built-in modules. Field schema, DataRole, field, record, and location rules determine which values and controls are returned.

- The authenticated service derives the SaaS app, tenant account, user, DataRole, and location scope; browser identifiers are never authorization proof.
- The page editor composes React components with validated data bindings. Those bindings call typed runtime APIs, whose application services execute reviewed stored procedures.
- List, search, detail, relation, activity, favorite, and write operations reapply their required server-side permissions.

## Add it to an app

1. Open the SaaS app in the Developer Console and identify the user journey and page where this module belongs.
2. Add the validated `module.dynamic-records` module block through the supported page/template authoring flow.
3. Configure the module with the page editor's React components and validated data bindings; the bindings call authenticated platform APIs backed by reviewed stored procedures.
4. Place the page in the correct user-type menus and assign existing DataRole, record, field, and location permissions.
5. Test list, detail, search, empty, denied, stale-update, and cross-location behavior before publishing an exact version.

## Copy/paste a complete Dynamic Records page

Set `moduleKey` to a real tenant-owned custom module key from the builder. The native renderer loads its public schema, list, create/edit form, detail, dynamic fields, relations/activity counts, and favorite state.

```
{
  "blocks": [
    {
      "_id": "asset-register", "_type": "module.dynamic-records",
      "props": { "moduleKey": "assets" }, "children": []
    }
  ]
}
```

### Schema-driven field conversion

Keep this helper beside the custom component. It converts only field definitions returned by the validated schema; hidden/encrypted fields are absent and therefore cannot be submitted by this form.

```
import type { DynamicModuleDetail, DynamicModuleFieldDefinition, DynamicModuleFieldInput } from "@buildwithhq/module-sdk";

export function dynamicRawValue(detail: DynamicModuleDetail | null, fieldKey: string) {
  const field = detail?.fields.find(value => value.fieldKey === fieldKey);
  if (!field) return "";
  const value = field.valueText?? field.valueInt?? field.valueDecimal?? field.valueDateTime?? field.valueBool?? field.valueGuid?? field.valueJson;
  return value == null? "": typeof value === "object"? JSON.stringify(value): String(value);
}

export function dynamicFieldInput(field: DynamicModuleFieldDefinition, raw: string): DynamicModuleFieldInput {
  const dataType = field.dataType.toLowerCase();
  if (!raw &&!field.required) return { fieldKey: field.fieldKey, clear: true };
  if (["boolean", "bool"].includes(dataType)) return { fieldKey: field.fieldKey, valueBool: raw === "true" };
  if (["int", "integer", "bigint"].includes(dataType)) return { fieldKey: field.fieldKey, valueInt: Number(raw) };
  if (["decimal", "number", "currency"].includes(dataType)) return { fieldKey: field.fieldKey, valueDecimal: Number(raw) };
  if (["datetime", "date"].includes(dataType)) return { fieldKey: field.fieldKey, valueDateTime: new Date(raw).toISOString() };
  if (dataType === "guid") return { fieldKey: field.fieldKey, valueGuid: raw };
  if (dataType === "json") return { fieldKey: field.fieldKey, valueJson: JSON.parse(raw) };
  return { fieldKey: field.fieldKey, valueText: raw };
}
```

### Custom unstyled React CRUD workspace

This example requests schema and data separately, pages on the server, and builds controls from the schema. The configured module key is a query target, never tenant authority; built-in or foreign module keys fail closed.

```
import { FormEvent, useEffect, useState } from "react";
import { dynamicFieldDisplayValue, type DynamicModuleDetail, type DynamicModuleList, type DynamicModuleSchema } from "@buildwithhq/module-sdk";
import { createDynamicModuleRecord, getDynamicModuleRecord, getDynamicModuleSchema, listDynamicModuleRecords, setDynamicModuleFavorite, updateDynamicModuleRecord } from "./api";
import { dynamicFieldInput, dynamicRawValue } from "./dynamicFieldInputs";

const empty = (moduleKey: string): DynamicModuleList => ({ contractVersion: 1, moduleKey, records: [], page: 1, pageSize: 50, total: 0, hasMore: false });

export function UnstyledDynamicRecords({ moduleKey, defaultLocationId }: { moduleKey: string; defaultLocationId?: string }) {
  const [schema, setSchema] = useState<DynamicModuleSchema | null>(null);
  const [result, setResult] = useState<DynamicModuleList>(empty(moduleKey));
  const [selected, setSelected] = useState<DynamicModuleDetail | null>(null);
  const [search, setSearch] = useState("");
  const [statusFilter, setStatusFilter] = useState("");
  const [page, setPage] = useState(1);
  const [sort, setSort] = useState<"created_asc" | "created_desc" | "title_asc" | "title_desc">("created_desc");
  const [editing, setEditing] = useState(false);
  const [creating, setCreating] = useState(false);
  const [title, setTitle] = useState("");
  const [status, setStatus] = useState("Active");
  const [values, setValues] = useState<Record<string, string>>({});
  const [error, setError] = useState<string | null>(null);

  async function load(signal?: AbortSignal) {
    const [nextSchema, nextList] = await Promise.all([getDynamicModuleSchema(moduleKey, signal), listDynamicModuleRecords(moduleKey, search, signal, statusFilter, page, 50, sort)]);
    setSchema(nextSchema); setResult(nextList);
  }
  useEffect(() => { const controller = new AbortController(); load(controller.signal).catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error? caught.message: "Records could not be loaded."); }); return () => controller.abort(); }, [moduleKey, page, search, sort, statusFilter]);

  async function open(recordId: string) { setSelected(await getDynamicModuleRecord(moduleKey, recordId)); setEditing(false); }
  function begin(detail: DynamicModuleDetail | null) { setCreating(!detail); setEditing(true); setSelected(detail); setTitle(detail?.title || ""); setStatus(detail?.status || "Active"); setValues(Object.fromEntries((schema?.fields || []).map(field => [field.fieldKey, dynamicRawValue(detail, field.fieldKey)]))); }
  async function save(event: FormEvent) {
    event.preventDefault(); if (!schema) return;
    const fields = schema.fields.map(field => dynamicFieldInput(field, values[field.fieldKey] || ""));
    const mutation = creating
? await createDynamicModuleRecord(moduleKey, { title, locationId: defaultLocationId, fields })
: await updateDynamicModuleRecord(moduleKey, selected!.recordId, { title, status, fields });
    setSelected(await getDynamicModuleRecord(moduleKey, mutation.recordId)); setEditing(false); setCreating(false); await load();
  }
  async function favorite() { if (!selected) return; await setDynamicModuleFavorite(moduleKey, selected.recordId,!selected.isFavorite); setSelected(await getDynamicModuleRecord(moduleKey, selected.recordId)); }

  return <section className="custom-dynamic-records">
    <header><div><small>Custom module</small><h1>{schema?.displayName || moduleKey}</h1></div>{schema?.canCreate && <button onClick={() => begin(null)}>New record</button>}</header>
    <form role="search" onSubmit={event => { event.preventDefault(); setPage(1); }}><label>Search<input value={search} maxLength={400} onChange={event => { setSearch(event.target.value); setPage(1); }} /></label><label>Status<input value={statusFilter} maxLength={50} onChange={event => { setStatusFilter(event.target.value); setPage(1); }} /></label><label>Sort<select value={sort} onChange={event => { setSort(event.target.value as typeof sort); setPage(1); }}><option value="created_desc">Newest</option><option value="created_asc">Oldest</option><option value="title_asc">Title A-Z</option><option value="title_desc">Title Z-A</option></select></label></form>
    {error && <p role="alert">{error}</p>}
    <div className="dynamic-columns"><ol>{result.records.map(record => <li key={record.recordId}><button onClick={() => void open(record.recordId)}><strong>{record.title || "Untitled"}</strong><span>{record.status}</span></button></li>)}</ol><article>{editing && schema? <form onSubmit={save}><label>Title<input required maxLength={300} value={title} onChange={event => setTitle(event.target.value)} /></label>{!creating && <label>Status<input maxLength={50} value={status} onChange={event => setStatus(event.target.value)} /></label>}{schema.fields.map(field => <label key={field.fieldKey}>{field.label}<input type={["boolean", "bool"].includes(field.dataType.toLowerCase())? "checkbox": "text"} required={field.required} maxLength={field.maxLength || undefined} pattern={field.validationRegex || undefined} checked={["boolean", "bool"].includes(field.dataType.toLowerCase())? values[field.fieldKey] === "true": undefined} value={["boolean", "bool"].includes(field.dataType.toLowerCase())? undefined: values[field.fieldKey] || ""} onChange={event => setValues(current => ({...current, [field.fieldKey]: event.target.type === "checkbox"? String(event.target.checked): event.target.value }))} /></label>)}<button>Save</button><button type="button" onClick={() => setEditing(false)}>Cancel</button></form>: selected? <><h2>{selected.title || "Untitled"}</h2><p>{selected.status} - {selected.locationName || "All locations"}</p><dl>{selected.fields.map(field => <div key={field.fieldKey}><dt>{field.label}</dt><dd>{dynamicFieldDisplayValue(field)}</dd></div>)}</dl><p>{selected.relationCount} relations - {selected.activityCount} activity</p><button onClick={() => void favorite()}>{selected.isFavorite? "Remove favorite": "Add favorite"}</button>{selected.canEdit && <button onClick={() => begin(selected)}>Edit</button>}</>: <p>Select a record.</p>}</article></div>
    <nav aria-label="Record pages"><button disabled={page <= 1} onClick={() => setPage(value => value - 1)}>Previous</button><span>Page {result.page}</span><button disabled={!result.hasMore} onClick={() => setPage(value => value + 1)}>Next</button></nav>
  </section>;
}
```

### Location changes and the current concurrency boundary

Location is supported by the write contract. Omit both values to preserve location, send a permitted `locationId` to replace it, or send `clearLocation: true` to clear it. The current Dynamic Records update contract does **not** accept `expectedUpdatedUtc`; concurrent editors can overwrite one another, so do not advertise conflict-safe collaboration until that contract is migrated.

```
import { getDynamicModuleRecord, updateDynamicModuleRecord } from "./api";

export async function moveDynamicRecord(moduleKey: string, recordId: string, locationId: string) {
  await updateDynamicModuleRecord(moduleKey, recordId, { locationId });
  return getDynamicModuleRecord(moduleKey, recordId);
}

export async function clearDynamicRecordLocation(moduleKey: string, recordId: string) {
  await updateDynamicModuleRecord(moduleKey, recordId, { clearLocation: true });
  return getDynamicModuleRecord(moduleKey, recordId);
}
```

### Optional starter styling

```
.custom-dynamic-records { width: 100%; max-width: none; }
.custom-dynamic-records > header,.custom-dynamic-records > form,.custom-dynamic-records > nav { align-items: end; display: flex; flex-wrap: wrap; gap:.75rem; justify-content: space-between; }
.dynamic-columns { display: grid; gap: 1rem; grid-template-columns: minmax(240px, 32%) minmax(0, 1fr); margin: 1rem 0; }
.dynamic-columns > ol { list-style: none; margin: 0; padding: 0; }
.dynamic-columns > ol button { background: transparent; border: 0; display: grid; padding:.75rem; text-align: left; width: 100%; }
.dynamic-columns > article { border: 1px solid var(--line, #d8dee8); padding: 1rem; }
.dynamic-columns article form,.dynamic-columns article dl > div { display: grid; gap:.4rem; }
@media (max-width: 760px) {.dynamic-columns { grid-template-columns: 1fr; } }
```

### Exact data path

| Purpose | Route | Procedure |
| --- | --- | --- |
| Public field schema | `GET /api/modules/dynamic/{moduleKey}/schema` | `sp_DynamicModules_GetSchemaSecured` |
| List/detail | `GET /api/modules/dynamic/{moduleKey}` and `/{recordId}` | `sp_DynamicModules_ListSecured`, `sp_DynamicModules_GetSecured` |
| Create/update | `POST /api/modules/dynamic/{moduleKey}` and `PUT /{recordId}` | `sp_DynamicModules_CreateSecured`, `sp_DynamicModules_UpdateSecured` |
| Favorite | `PUT /api/modules/dynamic/{moduleKey}/{recordId}/favorite` | `sp_DynamicModules_SetFavoriteSecured` |

Lists are capped at 100 records and writes at 200 dynamic fields. The server accepts only an active tenant-owned custom module and excludes hidden/encrypted definitions from the browser schema.

## Build a professional Dynamic Records dashboard

These six registry-backed presentation blocks let a designer turn the secured Dynamic Records API into a complete admin page without writing a chart, grid, status badge, empty state, or timeline from scratch. The normal operational API begins at `/api/modules/dynamic`; a dashboard-wide count or trend should come from a separate purpose-built presentation binding so the browser never downloads a broad record population to calculate one number.

[Download Developer Kit 1.20.13](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.13.zip) [Installation and component guide](../builder-guide/professional-template-foundation.html) [Complete headless SaaS guide](../api/headless-saas.html)

The ZIP contains this feature's fictional design composition at `pages/first-class/dynamic-records-sample.json`, its executable authenticated page at `pages/first-class/dynamic-records-live.json`, and the catalog-driven `FirstClassPresentationGallery.tsx`. Use the sample only for visual design. The live page calls the real native API and reviewed stored procedures under the signed-in user's scope.

| Payload kind | Runtime block | Useful Dynamic Records projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Visible records, lifecycle counts, due work, and exceptions |
| `entity-list` | `core.entity-list` | Recently changed records from one explicit custom module |
| `progress-list` | `core.progress-list` | Distribution by status, type, owner, or location |
| `series-chart` | `core.series-chart` | Create and update volume for the selected module |
| `data-grid` | `core.presentation-grid` | One bounded page using only declared display fields |
| `timeline` | `core.timeline` | Creation, field edits, status changes, relations, favorites, and archive events |

### Copy/paste design preview: all six blocks

This complete static page document renders immediately in Puck/Monaco and is useful while styling a template. Its names, counts, dates, and IDs are fictional design fixtures; static preview values are not live tenant facts.

**Copy the complete six-block page JSON**

```
{
  "blocks": [
    {
      "_id": "dynamic-records-metrics",
      "_type": "core.metric-strip",
      "props": {
        "title": "Custom record operations",
        "asOfUtc": "2026-09-04T18:00:00Z",
        "items": [
          {
            "key": "visible",
            "label": "Visible assets",
            "value": 1264,
            "format": "number",
            "tone": "neutral"
          },
          {
            "key": "active",
            "label": "In service",
            "value": 1108,
            "format": "number",
            "tone": "success"
          },
          {
            "key": "review",
            "label": "Review due",
            "value": 47,
            "format": "number",
            "tone": "warning"
          },
          {
            "key": "attention",
            "label": "Need attention",
            "value": 12,
            "format": "number",
            "tone": "danger"
          }
        ]
      },
      "children": []
    },
    {
      "_id": "dynamic-records-recent",
      "_type": "core.entity-list",
      "props": {
        "title": "Recently updated assets",
        "hasMore": true,
        "items": [
          {
            "id": "dynamic-records-sample-1",
            "recordId": "dynamic-records-record-1",
            "primary": "Compressor A-104",
            "secondary": "Asset register - Reno plant",
            "status": {
              "key": "in-service",
              "label": "In service",
              "tone": "success"
            },
            "trailing": "3 relations"
          },
          {
            "id": "dynamic-records-sample-2",
            "recordId": "dynamic-records-record-2",
            "primary": "Compressor A-104 - Follow-up",
            "secondary": "Asset register - Reno plant - Updated two hours ago by the assigned owner",
            "status": {
              "key": "in-review",
              "label": "In review",
              "tone": "primary"
            },
            "trailing": "Today"
          },
          {
            "id": "dynamic-records-sample-3",
            "recordId": "dynamic-records-record-3",
            "primary": "Compressor A-104 - West region",
            "secondary": "Asset register - Reno plant - Related to three visible records at the Reno location",
            "status": {
              "key": "on-track",
              "label": "On track",
              "tone": "success"
            },
            "trailing": "3 related"
          },
          {
            "id": "dynamic-records-sample-4",
            "recordId": "dynamic-records-record-4",
            "primary": "Compressor A-104 - Customer response",
            "secondary": "Asset register - Reno plant - Waiting for an external response before work can continue",
            "status": {
              "key": "scheduled",
              "label": "Scheduled",
              "tone": "warning"
            },
            "trailing": "Tomorrow"
          },
          {
            "id": "dynamic-records-sample-5",
            "recordId": "dynamic-records-record-5",
            "primary": "Compressor A-104 - Regional operations review with a deliberately long title",
            "secondary": "Asset register - Reno plant - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
            "status": {
              "key": "needs-attention",
              "label": "Needs attention",
              "tone": "danger"
            },
            "trailing": "Review",
            "tertiary": "Long-content fixture: verify keyboard focus, wrapping, narrow columns, and mobile overflow before publishing."
          },
          {
            "id": "dynamic-records-sample-6",
            "recordId": "dynamic-records-record-6",
            "primary": "Compressor A-104 - Completed preview",
            "secondary": "Asset register - Reno plant - Closed after review with its related evidence retained",
            "status": {
              "key": "complete",
              "label": "Complete",
              "tone": "success"
            },
            "trailing": "Closed"
          }
        ]
      },
      "children": []
    },
    {
      "_id": "dynamic-records-bystatus",
      "_type": "core.progress-list",
      "props": {
        "title": "Assets by lifecycle",
        "items": [
          {
            "key": "service",
            "label": "In service",
            "value": 1108,
            "maximum": 1264,
            "displayValue": "1108",
            "tone": "success",
            "status": {
              "key": "service",
              "label": "In service",
              "tone": "success"
            }
          },
          {
            "key": "maintenance",
            "label": "Maintenance",
            "value": 109,
            "maximum": 1264,
            "displayValue": "109",
            "tone": "warning",
            "status": {
              "key": "maintenance",
              "label": "Maintenance",
              "tone": "warning"
            }
          },
          {
            "key": "retired",
            "label": "Retired",
            "value": 47,
            "maximum": 1264,
            "displayValue": "47",
            "tone": "neutral",
            "status": {
              "key": "retired",
              "label": "Retired",
              "tone": "neutral"
            }
          }
        ]
      },
      "children": []
    },
    {
      "_id": "dynamic-records-trend",
      "_type": "core.series-chart",
      "props": {
        "title": "Custom record activity",
        "variant": "bar",
        "defaultPeriodKey": "d7",
        "periods": [
          {
            "key": "d7",
            "label": "7 days",
            "labels": [
              "Fri",
              "Sat",
              "Sun",
              "Mon",
              "Tue",
              "Wed",
              "Thu"
            ],
            "series": [
              {
                "key": "primary",
                "label": "Created",
                "tone": "primary",
                "values": [
                  8,
                  5,
                  4,
                  12,
                  15,
                  11,
                  17
                ]
              },
              {
                "key": "secondary",
                "label": "Updated",
                "tone": "success",
                "values": [
                  6,
                  4,
                  3,
                  9,
                  12,
                  10,
                  14
                ]
              }
            ]
          }
        ]
      },
      "children": []
    },
    {
      "_id": "dynamic-records-table",
      "_type": "core.presentation-grid",
      "props": {
        "title": "Visible asset records",
        "columns": [
          {
            "key": "asset",
            "label": "Asset",
            "type": "text",
            "align": "left"
          },
          {
            "key": "category",
            "label": "Category",
            "type": "text",
            "align": "left"
          },
          {
            "key": "location",
            "label": "Location",
            "type": "text",
            "align": "left"
          },
          {
            "key": "status",
            "label": "Status",
            "type": "status",
            "align": "left"
          },
          {
            "key": "updated",
            "label": "Updated",
            "type": "date",
            "align": "left"
          }
        ],
        "rows": [
          {
            "id": "dynamic-records-row-1",
            "recordId": "dynamic-records-record-1",
            "cells": {
              "asset": "Compressor A-104",
              "category": "Equipment",
              "location": "Reno plant",
              "status": {
                "key": "service",
                "label": "In service",
                "tone": "success"
              },
              "updated": "2026-09-04T16:18:00Z"
            }
          },
          {
            "id": "dynamic-records-row-2",
            "recordId": "dynamic-records-record-2",
            "cells": {
              "asset": "Compressor A-104 - Follow-up",
              "category": "Equipment",
              "location": "Reno plant",
              "status": {
                "key": "in-review",
                "label": "In review",
                "tone": "primary"
              },
              "updated": "2026-09-04T15:42:00Z"
            }
          },
          {
            "id": "dynamic-records-row-3",
            "recordId": "dynamic-records-record-3",
            "cells": {
              "asset": "Compressor A-104 - West region",
              "category": "Equipment",
              "location": "Reno plant",
              "status": {
                "key": "on-track",
                "label": "On track",
                "tone": "success"
              },
              "updated": "2026-09-04T12:18:00Z"
            }
          },
          {
            "id": "dynamic-records-row-4",
            "recordId": "dynamic-records-record-4",
            "cells": {
              "asset": "Compressor A-104 - Customer response",
              "category": "Equipment",
              "location": "Reno plant",
              "status": {
                "key": "scheduled",
                "label": "Scheduled",
                "tone": "warning"
              },
              "updated": "2026-09-03T21:07:00Z"
            }
          },
          {
            "id": "dynamic-records-row-5",
            "recordId": "dynamic-records-record-5",
            "cells": {
              "asset": "Compressor A-104 - Regional operations review with a deliberately long title",
              "category": "Equipment - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
              "location": "Reno plant",
              "status": {
                "key": "needs-attention",
                "label": "Needs attention",
                "tone": "danger"
              },
              "updated": "2026-09-03T16:31:00Z"
            }
          },
          {
            "id": "dynamic-records-row-6",
            "recordId": "dynamic-records-record-6",
            "cells": {
              "asset": "Compressor A-104 - Completed preview",
              "category": "Equipment",
              "location": "Reno plant",
              "status": {
                "key": "complete",
                "label": "Complete",
                "tone": "success"
              },
              "updated": "2026-09-02T19:14:00Z"
            }
          }
        ],
        "page": {
          "pageNumber": 1,
          "pageSize": 6,
          "totalRecords": 1264,
          "totalIsExact": true,
          "hasMore": true
        }
      },
      "children": []
    },
    {
      "_id": "dynamic-records-timeline",
      "_type": "core.timeline",
      "props": {
        "title": "Custom record activity",
        "hasMore": true,
        "items": [
          {
            "id": "dynamic-records-event-1",
            "recordId": "dynamic-records-record-1",
            "occurredUtc": "2026-09-04T17:58:00Z",
            "title": "Inspection date updated",
            "description": "Compressor A-104 passed its scheduled inspection.",
            "actor": "Jordan Lee",
            "tone": "success"
          },
          {
            "id": "dynamic-records-event-2",
            "recordId": "dynamic-records-record-2",
            "occurredUtc": "2026-09-04T15:42:00Z",
            "title": "Inspection date updated - Follow-up",
            "description": "Compressor A-104 passed its scheduled inspection. Updated two hours ago by the assigned owner.",
            "actor": "Avery Patel",
            "tone": "primary"
          },
          {
            "id": "dynamic-records-event-3",
            "recordId": "dynamic-records-record-3",
            "occurredUtc": "2026-09-04T12:18:00Z",
            "title": "Inspection date updated - West region",
            "description": "Compressor A-104 passed its scheduled inspection. Related to three visible records at the Reno location.",
            "actor": "Sam Rivera",
            "tone": "success"
          },
          {
            "id": "dynamic-records-event-4",
            "recordId": "dynamic-records-record-4",
            "occurredUtc": "2026-09-03T21:07:00Z",
            "title": "Inspection date updated - Customer response",
            "description": "Compressor A-104 passed its scheduled inspection. Waiting for an external response before work can continue.",
            "actor": "Maya Chen",
            "tone": "warning"
          },
          {
            "id": "dynamic-records-event-5",
            "recordId": "dynamic-records-record-5",
            "occurredUtc": "2026-09-03T16:31:00Z",
            "title": "Inspection date updated - Regional operations review with a deliberately long title",
            "description": "Compressor A-104 passed its scheduled inspection. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
            "actor": "Automation",
            "tone": "danger"
          },
          {
            "id": "dynamic-records-event-6",
            "recordId": "dynamic-records-record-6",
            "occurredUtc": "2026-09-02T19:14:00Z",
            "title": "Inspection date updated - Completed preview",
            "description": "Compressor A-104 passed its scheduled inspection. Closed after review with its related evidence retained.",
            "actor": "Jordan Lee",
            "tone": "success"
          }
        ]
      },
      "children": []
    }
  ]
}
```

### Copy/paste React composition

Copy `InteractivePresentationComponents.tsx` and its optional CSS from the [Professional Foundation Developer Kit](../builder-guide/professional-template-foundation.html), then add this module-specific composition. It is semantic and unstyled by default; pass `styled` after importing `interactive-presentation-components.css` for the supplied polished foundation. Either version accepts only a bounded already-authorized document and fails closed through the shared strict parsers.

```
import {
  EntityList,
  MetricStrip,
  PresentationGrid,
  ProgressList,
  SeriesChart,
  Timeline,
} from "./InteractivePresentationComponents";

export interface DynamicRecordsPresentationData {
  readonly metrics: unknown;
  readonly recent: unknown;
  readonly byStatus: unknown;
  readonly trend: unknown;
  readonly table: unknown;
  readonly timeline: unknown;
}

export interface DynamicRecordsPresentationProps {
  /** Pass only the already-authorized presentation document returned by the API. */
  readonly data?: DynamicRecordsPresentationData | null;
  readonly loading?: boolean;
  readonly error?: boolean;
  readonly styled?: boolean;
  /** Record identity is navigation context; the detail API must authorize it again. */
  readonly onOpenRecord?: (recordId: string) => void;
}

export function DynamicRecordsPresentation({
  data,
  loading = false,
  error = false,
  styled = false,
  onOpenRecord,
}: DynamicRecordsPresentationProps) {
  if (error) return <p role="alert">The Dynamic Records presentation could not be loaded.</p>;
  if (loading ||!data) return <p role="status">Loading Dynamic Records presentation...</p>;

  return (
    <main className={styled? "bwhq-api-example": undefined}>
      <header>
        <p>Dynamic Records</p>
        <h1>Custom record operations</h1>
        <p>Builder-defined records, lifecycle state, ownership, location, relationships, and recent changes.</p>
      </header>

      <MetricStrip data={data.metrics} styled={styled} />
      <div className={styled? "bwhq-api-example__split": undefined}>
        <ProgressList data={data.byStatus} styled={styled} />
        <EntityList data={data.recent} styled={styled} onOpenRecord={onOpenRecord} />
      </div>
      <SeriesChart data={data.trend} styled={styled} />
      <PresentationGrid data={data.table} styled={styled} onOpenRecord={onOpenRecord} />
      <Timeline data={data.timeline} styled={styled} onOpenRecord={onOpenRecord} />
    </main>
  );
}
```

### Copy the live, authenticated module page

This document has no placeholder key and needs no invented endpoint. Save it to a page and add that page to a User Type menu. The registered native component calls `/api/modules/dynamic`, uses the current tenant session, and preserves the module's real list, detail, create/update, pagination, empty, loading, and error behavior. Dynamic Records discovers the organization's real tenant-owned modules when no module key is configured.

**Copy the executable live page**

```
{
  "blocks": [
    {
      "_id": "dynamic-records-live",
      "_type": "module.dynamic-records",
      "props": {},
      "children": []
    }
  ]
}
```

The server derives SaaS app, organization, user, DataRoles, locations, module-specific membership/privacy, and any AI-read gate from verified identity. A returned identifier can select a detail target, but the detail or write endpoint authorizes it again. Page layout, status, tone, totals, action names, and identifiers never grant authority.

Tip

Start with Dynamic Records. Promote a domain object to a specialized first-class renderer only when its interaction model cannot be expressed well through the generic secured experience.

Important

A renderer being bundled in the tenant application does not make its data visible in every app. The server returns only components and records authorized for the current app and signed-in user; unavailable or unauthorized blocks fail closed.

---

Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/first-class-modules/dynamic-records.html).
