First Class Modules

Dynamic Records

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

Open raw .md
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.

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

PurposeRouteProcedure
Public field schemaGET /api/modules/dynamic/{moduleKey}/schemasp_DynamicModules_GetSchemaSecured
List/detailGET /api/modules/dynamic/{moduleKey} and /{recordId}sp_DynamicModules_ListSecured, sp_DynamicModules_GetSecured
Create/updatePOST /api/modules/dynamic/{moduleKey} and PUT /{recordId}sp_DynamicModules_CreateSecured, sp_DynamicModules_UpdateSecured
FavoritePUT /api/modules/dynamic/{moduleKey}/{recordId}/favoritesp_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.

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 kindRuntime blockUseful Dynamic Records projection
metric-setcore.metric-stripVisible records, lifecycle counts, due work, and exceptions
entity-listcore.entity-listRecently changed records from one explicit custom module
progress-listcore.progress-listDistribution by status, type, owner, or location
series-chartcore.series-chartCreate and update volume for the selected module
data-gridcore.presentation-gridOne bounded page using only declared display fields
timelinecore.timelineCreation, 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, 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. For exact current technical availability, use the generated API Map and first-class module inventory.