# BuildWithHQ Support: full documentation corpus
Source index: https://support.buildwithhq.com/llms.txt
AI support feed: https://support.buildwithhq.com/ai/support-content.jsonl
OpenAPI JSON: https://support.buildwithhq.com/openapi/buildwithhq.json
Pinned OpenAPI JSON: https://support.buildwithhq.com/openapi/versions/1.20.70/buildwithhq.json
---
# Section: Getting Started
---
title: "What is BuildWithHQ?"
section: "Getting Started"
canonical_url: "https://support.buildwithhq.com/getting-started/what-is-buildwithhq.html"
reviewed_at: "2026-10-22"
authority: Official
---
# What is BuildWithHQ?
The platform in plain terms: build, brand, and resell your own SaaS applications.
## In brief
- **Audience:** New builders
- **Reading time:** 4 min
- **Requires:** Nothing yet
BuildWithHQ is a platform for creating software products you sell to your own customers. You design the application — its pages, data, workflows, and AI features — and BuildWithHQ runs the infrastructure behind it: databases, hosting, payments, security, and AI processing.
## Three roles to know
- **Builder** — that's you. You create and configure applications in the builder console, and you own the relationship with your customers.
- **Tenant** — a customer company that signs up for one of your applications. Each tenant's data is isolated from every other tenant's.
- **End user** — a person at a tenant company who logs into your application day to day.
## What the platform runs for you
Each application uses routed database planes for live application data, audit history and AI. App creation starts a staged provisioning operation. Check its readiness and any reported setup requirements before inviting customers; creating an app record does not establish that its databases, domain and workers are ready.
## What you can build today
- **Professional tenant applications** — compose and publish TailAdmin-derived React/Puck pages, app themes, custom CSS, design assets, menus, User Types, DataRoles, and Locations.
- **Complete business workflows** — use first-class modules and dynamic fields with create, view, edit, safe removal, favorites, files, activity, and recursive record relationships.
- **AI-assisted products** — add permission-filtered multimodal search, Knowledge Core, AI Insights, Universal Inbox, and approval-governed GoClaw actions.
- **Headless or embedded products** — use the versioned Developer API, typed SDK, webhooks, MCP, body-only pages, or complete page shells while credentials and database routing remain server-owned.
- **Extensible SaaS products** — install exact-version marketplace features, discover their per-app API schemas, and run approved appliance services without giving extensions direct tenant authority.
Note
Your customers never see BuildWithHQ. Applications run under your brand, on your domains, with your logo and colors.
## Where to go next
Continue with [Create your account](../getting-started/create-your-account.html), then [Launch your first app](../getting-started/your-first-app.html). If you learn best by looking at the end result first, skim [the page editor](../builder-guide/page-editor.html) to see how apps are assembled.
---
Capability review: 2026-10-22. [View the canonical guide](https://support.buildwithhq.com/getting-started/what-is-buildwithhq.html).
---
title: "Current features and setup requirements"
section: "Getting Started"
canonical_url: "https://support.buildwithhq.com/getting-started/current-features.html"
reviewed_at: "2026-10-23"
authority: Official
---
# Current features and setup requirements
Find the current page designer, record scope, AI, API, integrations and diagnostics guidance, and check what your app has enabled.
## In brief
- **Guide updated:** 23 October 2026
- **Audience:** Builders and app users
- **Availability:** App configuration and permissions apply
BuildWithHQ combines a page designer, connected business records and governed automation for SaaS products. Your published app, entitlements, permissions and configured services determine which capabilities you can use. A feature described here or a visible control does not establish that a provider, model server or worker is running.
## Design and publish pages
Compose registered React components in the Puck page designer. Configure themes, custom CSS, assets, menus and User Types, save drafts, preview the result and publish the intended version. User Types select homepages, layouts and menus. Record authority still comes from the signed-in user’s Personal ownership, DataRoles, Locations and action permissions. A page being absent from a menu does not by itself deny access to an entitled published page.
## Choose record visibility explicitly
For records that support Personal, choose Personal or one eligible DataRole. Chat Rooms, Discussion Boards, Knowledge and Company News are shared-only and require one DataRole. Files offers Personal while you have room under your personal file limit. When shared records require a location, choose an authorized location or All if you have all-location access. Leaving a selection blank is not All. Child records inherit the parent’s scope. Read [Record visibility: Personal, DataRoles and Locations](record-visibility.html) before creating or moving shared data.
## Use the modules enabled in your app
Connected modules include contacts, organizations, tasks, checklists, work orders, conversations, files and knowledge. Open the native module for the allowed create, view, edit and removal actions. Some removals archive, cancel or deactivate a record to preserve its history. Search, dashboards and activity projections lead back to that native record rather than duplicating its lifecycle. The [module inventory](../first-class-modules/index.html) describes individual surfaces and boundaries.
## Keep Inbox and AI actions governed
Chat rooms and messages require current membership as well as DataRole and location access, including through Graph, Activity, Favorites and direct record links. Use native Chat Rooms search to find them: general and AI search exclude chat content. Saves check current permissions again. Removing a room preserves its history through soft deletion; an authorized restore restores only messages hidden by that room removal. Individually removed messages stay removed. See [Chat rooms](../first-class-modules/chat-rooms.html).
Universal Inbox collects attention while conversations, submissions and business records remain their source records. Refresh an item that has changed before approving a suggested action. GoClaw planning and research are read-only; material changes require the configured approval policy and action permission, and execution must use the approved current context. A suggestion, an approval and a queued execution are distinct states.
AI search and Knowledge Core apply the current user’s record, field, DataRole, location and AI-read permissions before assembling model context. Multimodal retrieval can preserve both text and visual evidence. Ingestion, serving and execution depend on configured and enabled services; contact your operator when the app reports a dependency is unavailable.
## Connect a headless app, SDK or MCP client
Start with the [generated OpenAPI reference](../api/interactive-api-reference.html) and current versioned Developer Kit. The public API contract is distinct from native browser-only routes. Keep app keys and delegated tokens in trusted server storage. OAuth connections and MCP do not replace tenant identity or record permissions. Sensitive MCP capabilities require explicit app-level approval; connecting a client does not authorize every write. Use the exact documented route and the latest concurrency token, and refresh state after a conflict rather than replaying a consequential request blindly.
## Verify external services before enabling work
Mail, payment processing, webhooks, Snowflake export and automation depend on configured providers and running workers. Verify the destination and credentials for the selected organization, test the connection and explicitly enable supported processing or schedules. A queued reply is not a delivered message, a transfer submission is not bank settlement, and a saved integration is not an active worker. See the [Snowflake setup and export guide](../api/tenant-snowflake.html) for its test, enable and schedule steps.
## Protect sensitive values
Supported Contact secure fields use masked display and an explicit permission-checked reveal. Keep secrets out of ordinary fields and support messages. Do not assume that every module has an operational encryption and reveal flow; the [secure-field guide](../builder-guide/secure-fields.html) distinguishes available contracts and setup requirements.
## Find the evidence for a problem
Authorized builders can open [+ Debug](../builder-guide/debug-drawer.html) for safe outcomes, timings and correlation references, then follow a problem to app Logs. Use its sanitized export for support. Closing a problem preserves its history; another occurrence can reopen it. Do not paste customer payloads, credentials or private keys into a ticket.
## Wait for app readiness
App creation and infrastructure readiness are separate stages. An app has routed Core, Logs and AI database planes; inspect provisioning status and resolve reported setup requirements before inviting customers. A new app record is not proof that databases, DNS, certificates, compute and workers are ready. Exact-version template installs configure presentation; database updates follow a separate reviewed path. Customer-hosted Appliance operation and optional extension compute have separate deployment requirements.
For incidents use [service status](https://status.buildwithhq.com/). A stale or unavailable feed cannot confirm health. Blog product ideas, conference topics and interface previews explain possibilities; they do not certify a live app or production launch.
---
Capability review: 2026-10-23. [View the canonical guide](https://support.buildwithhq.com/getting-started/current-features.html).
---
title: "Record visibility: Personal, DataRoles and Locations"
section: "Getting Started"
canonical_url: "https://support.buildwithhq.com/getting-started/record-visibility.html"
reviewed_at: "2026-10-23"
authority: Official
---
# Record visibility: Personal, DataRoles and Locations
Choose an explicit scope when creating records, understand inherited child visibility, and safely update shared records.
## In brief
- **Applies to:** User-managed business records
- **Choose on:** Top-level create or scope move
- **Authority:** Verified user and current permissions
## Create a Personal record where supported
When the module offers **Personal** in its visibility choices, select it explicitly. The record belongs to you, has no location and gives its owner control. Another user’s membership in your organization does not make your Personal record shared.
## Shared-only modules
Chat Rooms, Discussion Boards, Knowledge and Company News do not offer Personal for new records. Choose one eligible DataRole and, when required, an authorized location or All. Chat rooms and messages additionally require current room membership. They remain accessible through native Chat Rooms search, while general and AI search exclude chat content.
## Personal files
Files offers Personal up to a per-person limit: the app sets a default (25 unless the builder changes it, in the Design studio under Files) and a ceiling, and the organization owner can choose any number up to the ceiling in Account admin > File storage, where 0 turns personal files off. The upload form shows how many personal files you have used. Deleted personal files count until they are permanently removed, and lowering the limit never removes files people already have.
## Create a shared record
1. Select one **DataRole** in which you are allowed to create this kind of record. App navigation and your User Type do not choose a DataRole for you.
2. When active Locations exist, select one authorized location. Select **All** only if you have all-location access. An empty selection is not All, even if there is only one choice.
3. Save the record. The server checks your current authority and commits the record and its selected scope together.
When there are no active Locations, the location field is optional. If no eligible scope choices are available, ask your administrator for the relevant permission rather than attempting to bypass the form.
## Add a child record
Create the child from its authorized parent. The child inherits that parent’s DataRole and location within the create operation. Do not select a different scope to make a child visible outside its parent.
## Edit or move an existing record
Review the displayed current scope. You do not need to reselect scope for an ordinary edit; explicitly choose the destination when moving a record. The destination still requires current permission. Once shared, a record cannot be changed to Personal. If you need a Personal replacement, create it explicitly and remove the shared record only when you have permission and have considered its history and relationships.
## What a page or integration can access
Menus and published pages select presentation. Record reads, edits, removals, files, exports, search and AI each enforce the current user’s relevant permissions. The same contract applies to Developer API, MCP and automation access. A URL, record ID, app ID or organization ID identifies a target; it does not grant authority.
## If saving fails
For missing scope, choose an eligible explicit value. For permission denied, refresh your available choices or contact an administrator. For a stale-write conflict, reload the current record and review the other changes before saving again. Keep your unsaved text if possible; do not repeatedly submit a consequential action. Share the safe error code and correlation reference with support.
---
Capability review: 2026-10-23. [View the canonical guide](https://support.buildwithhq.com/getting-started/record-visibility.html).
---
title: "What makes BuildWithHQ different"
section: "Getting Started"
canonical_url: "https://support.buildwithhq.com/getting-started/what-makes-it-different.html"
reviewed_at: "2026-09-14"
authority: Official
---
# What makes BuildWithHQ different
Seven differences you could verify in the database schema — and what each one means for your day.
## In brief
- **The claim:** Every difference here is structural, not a setting
- **Audience:** Anyone evaluating the platform
- **Format:** What it is → why your life is easier
Every app-builder platform says it's secure, flexible, and AI-powered. So this page makes a different kind of argument: each difference below is a **structural property of how the platform is built** — something an auditor could verify by reading the database schema, not a policy that depends on everyone remembering to follow it. For each one: what it is, and what it does for the people using it.
## 1. Every application gets its own databases — three of them
Most platforms put all customers in shared tables and separate them with a filter column. Here, every application you create is provisioned with three physically separate databases: live data, immutable history, and AI. Each carries its own identity, and a built-in tripwire refuses to serve traffic if a connection ever points at the wrong database — misrouting fails loudly instead of leaking quietly.
**What this means:** database routing separates SaaS applications. Customer organizations inside an application still share that application's databases, with server-enforced account, DataRole, Location, and record permissions. Separate databases help with operational boundaries, but shared hardware still needs capacity planning.
## 2. One security layer, inherited by everything
Everything in an application — contacts, tasks, conversations, files, events — is anchored to a single universal record identity, and every read flows through one security layer that evaluates role, location scope, and capability. Features don't implement their own access checks; they inherit the one model. The [inbox](../builder-guide/universal-inbox.html) has no inbox security code. [AI insights](../first-class-modules/ai-insights.html) inherit visibility from their source records. Even a user's [favorites](../end-user/whats-new-and-favorites.html) silently drop a record the moment access is revoked — the bookmark reads through the same layer as everything else.
**Easier because:** builders configure permissions once and every feature — including ones that ship next year — obeys them. There is no per-feature permission matrix to keep synchronized, and no new feature can introduce a new class of leak, because no feature has an access path of its own.
## 3. Security decisions you can replay
The permission tables keep full version history, and every AI data retrieval logs a cryptographic proof of exactly what the user was allowed to see at that instant — into a per-tenant, hash-chained record that can't be silently edited. Any past access can be [replayed](../trust/authorization-replay.html): re-derived under the rules as they stood then, or under today's rules, and compared against the proof.
**Easier because:** when a customer or auditor asks “did the AI ever see something it shouldn't have?”, the answer is a procedure with a verdict, not a promise. No other builder platform we're aware of can rewind a security decision and check it.
## 4. Encryption that changes what's physically possible
Fields marked [secure](../builder-guide/secure-fields.html) are encrypted with per-tenant keys — and the database itself enforces, by constraint, that a field value holds *either* readable text *or* an encrypted envelope, never both. Search indexing and AI read only the readable side, so an encrypted value isn't “filtered out” of search and AI — **it was never there to filter**.
**Easier because:** protecting a sensitive field is one checkbox with no follow-up checklist. There is no list of downstream features someone must remember to exclude it from, because the exclusion is a property of storage, not of each feature's good behavior.
## 5. Customize everything — and still receive upgrades
Template platforms force a choice: stay stock and get updates, or customize and be stranded. Here every block on every page carries a stable identity, forks record the exact baseline they diverged from, and [upgrades arrive as block-by-block choices](../builder-guide/templates-forking.html) merged three ways against your customizations — with conflicting blocks flagged for a human decision instead of silently overwritten. Premium bespoke work is excluded from automated upgrades entirely, by classification.
**Easier because:** builders customize on day one without mortgaging every future improvement, and tenants can restyle their own pages knowing an upstream release will never flatten their changes.
## 6. AI that was designed inside the rules, not bolted on after
The permission model has an AI-specific read capability as a first-class flag — meaning a role can see data itself while excluding that data from AI entirely. On top of that sit the rest of the controls this documentation covers: [assistants that propose while humans approve](../builder-guide/goclaw-in-depth.html), action budgets enforced by the platform rather than the AI's own logic, full attribution on everything AI produces, and [scored Harness Pack gates](../builder-guide/harness-packs.html) before an AI behavior serves customers.
**Easier because:** you can adopt AI feature by feature, user by user, with the same governance answer every time — instead of choosing between “no AI” and “trust the AI.” And when AI runs on partner or dedicated hardware, the data rules travel with it, because they live in the platform, not the model.
## 7. A history that can prove itself
Every application's audit trail is [append-only and hash-chained](../trust/tenant-logging.html) — tampering is detectable by arithmetic, not just forbidden by policy. Third-party [extensions](../builder-guide/appliances-and-endpoints.html) operate under manifests frozen at release, with every network attempt logged as allowed or blocked and every data access written to a tenant-verifiable disclosure ledger. An active organization owner can also configure [export of selected log metadata to their own Snowflake account](../builder-guide/snowflake-analytics-sync.html). Builder support requires explicit Snowflake scopes and a credential bound to that organization's active owner; it does not grant access to other organizations. Export requires connection verification and explicit enablement.
**Easier because:** disputes end with evidence, security questionnaires get short answers, and tenants get warehouse-scale analytics over their own history without their data ever becoming someone else's dataset — a data-ownership answer most platforms can't give.
To pressure-test these claims — ours and anyone's — take the [ten questions](ten-questions.html) into your next vendor evaluation.
## The same differences, from each chair
| Structural difference | For the builder | For the tenant |
| --- | --- | --- |
| Dedicated database triplet | No noisy neighbors; clean compliance answers | Their data is physically theirs |
| One inherited security layer | Configure once; every feature obeys | Consistent visibility everywhere, no surprises |
| Replayable authorization | Auditor questions become queries | Provable answers about their own data |
| Constraint-enforced encryption | One checkbox, zero follow-up checklist | Sensitive values can't leak into search or AI |
| Fork-and-merge templates | Ship improvements without breaking customers | Customize freely, still receive updates |
| AI inside the permission model | Adopt AI incrementally with one governance story | An assistant that helps but never acts alone |
| Self-proving history | Disputes end with evidence; questionnaires get short answers | Their own history, exportable to their own warehouse |
Note
The honest framing for competitive conversations: no single item above is unique to this platform in isolation — field encryption exists elsewhere, and so do audit logs. What's different is that all of them are structural and all of them compose, in one platform built for people who resell software to their own customers. The integration is the moat.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/getting-started/what-makes-it-different.html).
---
title: "Ten questions to ask any platform (including us)"
section: "Getting Started"
canonical_url: "https://support.buildwithhq.com/getting-started/ten-questions.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Ten questions to ask any platform (including us)
Take these into every vendor evaluation. Our answers are below, each linked to its documentation. We suggest asking everyone else for theirs in writing.
## In brief
- **Format:** Question → why it matters → our answer, with the doc link
- **Use it on:** Every platform you evaluate
- **Our answers:** All ten are “yes,” in writing, below
Platform demos all look the same. The differences live in questions that only come up later — during the audit, the incident, the migration, the customer's security review. Here are ten worth asking *before* you commit, with our answers. Each answer links to the documentation that backs it, because an answer you can't verify is a slogan.
## 1. “If we order you to delete our audit trail, will your system refuse?”
*Why it matters:* the moment an audit trail can be deleted on request — by you, by an admin, by a compromised account — it stops being evidence.
*Our answer:* yes, it refuses. The retention cleanup routine has the compliance-critical history tables — the audit log and every record- and field-level change and access log — hard-excluded at the code level. Asking it to delete them doesn't get a warning; it gets an error. The same discipline runs through the page system: the pruner that trims editing history is written to never delete the record of what was published. [Tenant logging](../trust/tenant-logging.html).
## 2. “Can you prove — not promise — that your AI never accessed something it shouldn't have?”
*Why it matters:* every platform will promise this. Almost none can demonstrate it for a specific query on a specific day last quarter.
*Our answer:* every AI retrieval logs a cryptographic proof of exactly what that user was permitted to see at that instant, into a tamper-evident chain — and the permission rules themselves keep full version history, so any past decision can be replayed and given a verdict. “As of then” catches violations; “as of now” catches drift. [Replayable authorization](../trust/authorization-replay.html).
## 3. “Does your access log distinguish an AI reading a field from a human viewing it?”
*Why it matters:* “who saw this?” and “did an AI ingest this?” are different questions with different regulatory weight, and a log that can't tell them apart can't answer either.
*Our answer:* yes — field-level access logging records the *kind* of access: a human view, an export, a print, an API read, a report read, or an AI read, each its own category. Down to the individual field. [Tenant logging](../trust/tenant-logging.html).
## 4. “What happens if a connection ever points at the wrong customer's database?”
*Why it matters:* on shared-table platforms this question doesn't even parse — there is only one database, and isolation is a WHERE clause. Where separate databases exist, misrouting is the nightmare scenario.
*Our answer:* every tenant database knows its own identity, and a tripwire check refuses to serve a connection whose claimed identity doesn't match — the request fails loudly with a misconnection error naming both sides, instead of quietly serving someone else's data. [The isolation model](what-makes-it-different.html).
## 5. “If we encrypt a field, list every feature we must remember to exclude it from.”
*Why it matters:* the usual answer is a checklist — search, reports, AI, exports, integrations — and the checklist is exactly where breaches live, because someone forgets one.
*Our answer:* the list is empty. A database constraint makes it physically impossible for a secure field to hold readable text alongside its encrypted envelope, and search and AI only ever consume the readable side. Nothing downstream needs to exclude it, because there is nothing to exclude. [Secure fields](../builder-guide/secure-fields.html).
## 6. “Show us every customized page with an upstream update waiting — right now.”
*Why it matters:* template platforms lose track of forks. The honest competitor answer is “customers find out when something looks broken.”
*Our answer:* one query. Every fork records the exact version it diverged from, so the platform can enumerate, at any moment, every customized page whose baseline is behind the current release — with the original, the customization, and the new version side by side, ready for a block-level merge that flags conflicts instead of overwriting. [Templates, forks, and upgrades](../builder-guide/templates-forking.html).
## 7. “When our AI assistant hits its daily limit, whose code enforces it?”
*Why it matters:* if the answer is “the AI is instructed to stop,” the limit is a suggestion. Instructions are what misbehaving software ignores first.
*Our answer:* the platform's, at the moment each action is queued — outside the assistant entirely. A hypothetically malfunctioning assistant is still mechanically capped, and everything it wants to do waits in a human approval queue besides. [GoClaw in depth](../builder-guide/goclaw-in-depth.html).
## 8. “Can someone probe your API to learn which accounts and apps exist?”
*Why it matters:* APIs that answer “not yours” differently from “doesn't exist” are enumeration oracles — reconnaissance tools handed to attackers for free.
*Our answer:* no. Routing and lookup paths deliberately return the same nothing for “not yours” and “not real,” a design rule written into the code as a comment future engineers can't miss. [API overview](../api/overview.html).
## 9. “Can our customers take everything and leave?”
*Why it matters:* the platforms most eager to hold your data are the ones you most need an exit from. Data ownership you can't exercise isn't ownership.
*Our answer:* yes — an authorized account export provides that organization's records and related artifacts with checksums. A whole-SaaS backup is separate and may contain multiple organizations; it is not an individual organization's export. It is a standing owner-only feature available at any time, not an off-boarding concession ([Back up and download your data](../end-user/download-your-data.html) ). Builders can separately export the versioned JSON and JSON Schema definitions for pages and workflows. Customer-owned S3 restore bundles can preserve the database, files, schemas, settings, manifest, and checksums outside BuildWithHQ. For an enterprise relocation or a company shutdown, the application can run in AWS, Azure, another cloud environment, or a virtual machine using the continuity runtime. [Business continuity and application portability](../trust/business-continuity-and-portability.html).
## 10. “When third-party code touches our data, can you show us exactly what ran?”
*Why it matters:* “an integration accessed your data” is not an answer. *Which version, doing what, reaching where* is an answer.
*Our answer:* extensions run under manifests frozen and fingerprinted at release — endpoints, network reach, everything. Every data access lands in a tamper-evident ledger with that fingerprint attached; every network attempt is logged as allowed or blocked, host and port included. The audit trail names the exact code. [Appliances and their endpoints](../builder-guide/appliances-and-endpoints.html).
---
Note
Every answer above is documented on this public site — no NDA, no sales call, no “let me check with engineering.” We'd suggest asking the same ten questions of any platform you're evaluating, and asking for the answers in writing. The ones who can answer will be glad you asked.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/getting-started/ten-questions.html).
---
title: "Create your account"
section: "Getting Started"
canonical_url: "https://support.buildwithhq.com/getting-started/create-your-account.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Create your account
Sign up, verify your email, and secure your account before creating anything.
## In brief
- **Audience:** New builders
- **Time:** 5 min
- **You'll need:** A work email
Your builder account is the owner of everything you create, so a couple of setup steps are required before the platform lets you build.
## Sign up and verify your email
Create your account with your email address and a strong password. A verification link is sent immediately — **you must verify your email before you can create an application or invite teammates**. This is a hard requirement, not a reminder.
## Protect the account
Turn on two-factor authentication under account security. You'll scan a QR code with any authenticator app and receive a set of recovery codes — store those somewhere safe; each one works exactly once.
Important
After 10 failed sign-in attempts within 15 minutes, sign-in is locked for 15 minutes. This protects your account and cannot be lifted early by support.
## Trials and activation
New accounts start on a trial. When a trial ends, the platform enforces it automatically — you'll be prompted to choose a plan to continue. See [Plans and trials](../billing/plans-and-trials.html).
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/getting-started/create-your-account.html).
---
title: "Launch your first app"
section: "Getting Started"
canonical_url: "https://support.buildwithhq.com/getting-started/your-first-app.html"
reviewed_at: "2026-10-22"
authority: Official
---
# Launch your first app
Create an application and understand what gets provisioned when you do.
## In brief
- **Audience:** New builders
- **Time:** 10 min
- **Requires:** Verified email
Creating an application starts a staged provisioning operation. Follow the app’s provisioning status until it reports readiness. If setup is pending or fails, review the safe error and correlation reference before retrying; do not create duplicate apps to work around a failed stage.
## What gets created
- **Your application record** and the default web address configured for it. Open the app after provisioning and routing are ready.
- **Three dedicated databases** — live data, audit history, and AI — reserved for this application alone.
- **Storage and AI allowances** for the app, metered against your plan.
- **You as the owner**, with full administrative rights over the new app.
## Two ways to start
**Start from a template** — pick a template pack and get a complete, working application (pages, menus, user types) that you then customize. This is the fastest path and the one we recommend for a first app.
**Build with AI** — describe the product you want in a guided interview, and the platform drafts the application structure for you. See [Build with AI](../builder-guide/build-with-ai.html).
Tip
You can create additional apps later from the same account — each one gets its own isolated databases and its own domain setup.
## Standard and dedicated runtime
Database routing and service compute are separate concerns. Confirm the app’s configured runtime and enabled workers before depending on background processing. Optional dedicated compute or customer-hosted Appliance operation requires its own deployment setup and agreed commercial terms; creating an app does not provision that compute automatically.
---
Capability review: 2026-10-22. [View the canonical guide](https://support.buildwithhq.com/getting-started/your-first-app.html).
---
title: "Manage customer organizations"
section: "Getting Started"
canonical_url: "https://support.buildwithhq.com/getting-started/manage-customer-organizations.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Manage customer organizations
Create and administer every customer organization inside one SaaS app.
## In brief
- **Audience:** SaaS builders
- **Workspace:** SaaS Apps > Organizations
- **Scope:** One selected app
Open a SaaS app and choose **Organizations** to manage the customer accounts hosted by that product. The directory is limited to the selected app and shows each organization’s owner, status, user count, DataRole count, and Location count.
## Create an organization
Choose **New organization** and enter the customer identity plus its initial owner. API-driven signup can include up to five initial DataRoles and five initial Locations in the same submission; the console provides the first role/location fields and supports adding the rest after creation. The account, owner, owner authority, roles, and locations are committed together; an invalid part leaves no half-created organization.
A builder-owned marketing site can create the same organization through the Developer API after its own signup service verifies the owner. The browser posts to the builder's backend; that backend keeps the app credential private and calls `POST /v1/apps/{saasAppId}/accounts`. API-created organizations appear in this directory without importing or synchronizing them, and the new owner can use the same account administration workspace inside the SaaS. Follow [How customer organizations can signup from your website](../api/customer-organization-website-signup.html) for the complete implementation.
## Manage the account
- Rename the organization, change its primary email, or deactivate/reactivate it with revision protection.
- Add external tenant users and edit or deactivate existing users.
- Create, edit, and safely deactivate DataRoles and Locations.
- Assign each user the allowed DataRoles and either all Locations or a selected Location set.
Before adding ordinary users, enable at least one shared User Type for tenant assignment in the app’s presentation policy. The account-local **owner** type is reserved for the initial owner and cannot be assigned to ordinary users.
Note
Organization IDs select a record; they do not prove authority. The server verifies the signed-in builder, selected app ownership, registered database route, SaaS identity, and AppAccount scope on every request. Browsers never receive database names or connection strings.
Tip
Deactivate accounts, users, DataRoles, or Locations when history must be retained. Hard deletion is intentionally not the normal administrative lifecycle.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/getting-started/manage-customer-organizations.html).
---
title: "Connect a custom domain"
section: "Getting Started"
canonical_url: "https://support.buildwithhq.com/getting-started/custom-domains.html"
reviewed_at: "2026-10-22"
authority: Official
---
# Connect a custom domain
Point your own domain at your application and get automatic TLS.
## In brief
- **Audience:** Builders
- **Time:** 15 min + DNS propagation
- **Requires:** Access to your DNS
After provisioning and routing are ready, connect your own domain using the DNS values shown by the console. Wait for domain verification and certificate readiness before inviting customers. A saved hostname is not proof that HTTPS is ready.
## Add the domain
1. In the app's settings, open **Domains** and add the hostname you want (for example `app.yourcompany.com`).
2. The console shows you a DNS record to create at your DNS provider — copy it exactly.
3. Create the record, then return and click **Verify**. Verification usually completes within minutes, but DNS changes can take up to 24–48 hours to propagate depending on your provider.
## Common problems
| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Verification never completes | Record created at the wrong host, or a conflicting existing record | Check for typos in the host name; remove any old A/CNAME records on the same host |
| Certificate warning in the browser | DNS still propagating | Wait for propagation to finish; certificates issue automatically once verification passes |
| Domain works but shows another site | DNS points somewhere else (old hosting) | Confirm the record target matches what the console shows exactly |
Each customer-facing app can carry multiple hostnames, so staging and production addresses can coexist on one app.
---
Capability review: 2026-10-22. [View the canonical guide](https://support.buildwithhq.com/getting-started/custom-domains.html).
---
title: "Invite your team"
section: "Getting Started"
canonical_url: "https://support.buildwithhq.com/getting-started/team-and-roles.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Invite your team
Add teammates to the builder console and control what they can do.
## In brief
- **Audience:** Builder owners
- **Time:** 5 min
- **Requires:** Verified email
You don't have to build alone. Teammates get their own sign-in to the builder console and act with their own identity — never share credentials.
## Invitations
Invite a teammate by email from your account's team page. They receive a one-time invitation link, create their own password, and appear in your team list. Invitations require your account email to be verified first.
## Roles
The account has one **Owner** — the person who created it — plus team members with assigned permission roles. Owners control billing, can delete applications, and manage the team itself.
Tip
Everything a teammate does is recorded in the audit history under their own identity, which matters when you need to answer “who changed this?” later.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/getting-started/team-and-roles.html).
---
title: "A tour of the three consoles"
section: "Getting Started"
canonical_url: "https://support.buildwithhq.com/getting-started/platform-tour.html"
reviewed_at: "2026-09-14"
authority: Official
---
# A tour of the three consoles
Choose the Admin, Developer, or Operations workspace without changing identity or permissions.
## In brief
- **Audience:** New builders
- **Reading time:** 6 min
- **Format:** Orientation
BuildWithHQ groups work into three focused consoles. They reuse the same login, session, APIs, and authorization; this split changes navigation, not what your identity is allowed to do.
## Choose by job
| Console | What lives there | Covered in |
| --- | --- | --- |
| **Admin** | Team, billing, account settings, branding, communications, security, and audit. | [Admin Console guide](../console-guides/admin-console.html) |
| **Developer** | SaaS apps, templates, harnesses, appliances, marketplace, API clients, and integrations. | [Developer Console guide](../console-guides/developer-console.html) |
| **Operations** | Provisioning, analytics, backups, domains, logs, storage, and AI operations. | [Operations Console guide](../console-guides/operations-console.html) |
Note
Navigation location is not authorization. Every backend request still enforces the existing account, app, role, DataRole, location, and capability rules.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/getting-started/platform-tour.html).
---
# Section: Console Guides
---
title: "Three consoles, one BuildWithHQ identity"
section: "Console Guides"
canonical_url: "https://support.buildwithhq.com/console-guides/overview.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Three consoles, one BuildWithHQ identity
How Admin, Developer, and Operations divide navigation without dividing authentication or authorization.
## In brief
- **Login:** Shared
- **Sessions:** Shared
- **Permissions:** Existing server rules
BuildWithHQ separates the builder experience into three focused consoles. This is a **navigation and design split**: your login, session, account membership, app ownership, roles, DataRoles, locations, and server permissions remain the same.
| Console | Primary job | Production home |
| --- | --- | --- |
| **Admin Console** | People, billing, branding, communications, security, and account governance. | [hello.buildwithhq.com](https://hello.buildwithhq.com/dashboard) |
| **Developer Console** | SaaS apps, templates, AI harnesses, appliances, integrations, API clients, and marketplace publishing. | [dev.buildwithhq.com](https://dev.buildwithhq.com/dashboard) |
| **Operations Console** | Provisioning, runtime health, domains, backups, logs, analytics, storage, and AI operations. | [ops.buildwithhq.com](https://ops.buildwithhq.com/dashboard) |
## What stays shared
- the same sign-in, MFA, recovery, invitation, and account-selection flow;
- the same browser session and account/app context;
- the same backend APIs and stored-procedure-backed application services;
- the same authorization checks on every request; and
- the same audit and correlation evidence.
## Navigation is not permission
A link appearing in a console does not grant access, and hiding a link is not a security boundary. Each service verifies the signed-in identity, account/app relationship, and required capability server-side.
Note
You may use all three consoles with the same identity. What you can view or change remains limited by the permissions already assigned to you.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/console-guides/overview.html).
---
title: "Admin Console guide"
section: "Console Guides"
canonical_url: "https://support.buildwithhq.com/console-guides/admin-console.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Admin Console guide
Manage the business and governance layer behind every SaaS app from hello.buildwithhq.com.
## In brief
- **Home:** hello.buildwithhq.com
- **Focus:** Account governance
- **Local preview:** localhost:4173
Use the Admin Console for account-wide responsibilities. It intentionally keeps app building and runtime operations out of the main navigation so owners can focus on people, money, brand, and governance.
[Open Admin Console](https://hello.buildwithhq.com/dashboard) [Team](https://hello.buildwithhq.com/team) [Billing](https://hello.buildwithhq.com/billing) [Security](https://hello.buildwithhq.com/security)
## Primary workspaces
| Workspace | Use it for | Related guide |
| --- | --- | --- |
| [Dashboard](https://hello.buildwithhq.com/dashboard) | Account status and the work requiring administrative attention. | [Getting Started](../getting-started/index.html) |
| [Team](https://hello.buildwithhq.com/team) | Members, invitations, existing roles, status, and ownership transfer. | [Team and roles](../getting-started/team-and-roles.html) |
| [Billing](https://hello.buildwithhq.com/billing) | Plans, invoices, payment status, storage/resources, and account lifecycle. | [Billing & Account](../billing/index.html) |
| [Branding & communications](https://hello.buildwithhq.com/branding) | White-label identity and account communication settings. | [White-label guidance](../end-user/for-builders-white-label.html) |
| [Security & audit](https://hello.buildwithhq.com/security) | MFA, sessions, security posture, and accountable change history. | [Trust & Security](../trust/index.html) |
## When to switch consoles
Move to Developer when you need to change the product or publish an artifact. Move to Operations when you need to diagnose or operate a running app. Cross-console links preserve the intended destination; they do not elevate permission.
Tip
Keep account-owner and billing work in the Admin Console even when the affected resource is an app. That makes governance responsibilities easier to find and audit.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/console-guides/admin-console.html).
---
title: "Developer Console guide"
section: "Console Guides"
canonical_url: "https://support.buildwithhq.com/console-guides/developer-console.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Developer Console guide
Build apps, publish exact versions, configure integrations, and use the developer platform from dev.buildwithhq.com.
## In brief
- **Home:** dev.buildwithhq.com
- **Focus:** Build and extend
- **Local preview:** localhost:4175
Use the Developer Console for the product definition and its reusable artifacts. It combines visual building with the developer escape hatches needed for APIs, installed services, AI, and exact-version distribution.
[Open Developer Console](https://dev.buildwithhq.com/dashboard) [SaaS Apps](https://dev.buildwithhq.com/apps) [Templates](https://dev.buildwithhq.com/templates) [AI Harnesses](https://dev.buildwithhq.com/templates/harnesses) [Appliances](https://dev.buildwithhq.com/appliances) [Marketplace](https://dev.buildwithhq.com/marketplace)
## Primary workspaces
- **[SaaS Apps](https://dev.buildwithhq.com/apps) ** — create products, inspect app-bound identity, manage API clients, and configure integrations.
- **[Templates](https://dev.buildwithhq.com/templates) ** — author, validate, version, publish, install, and upgrade reusable template content.
- **[AI Harnesses](https://dev.buildwithhq.com/templates/harnesses) ** — compose harness packs, queue exact-version evaluations, publish only passed artifacts, and inspect aggregate installations.
- **[Appliances](https://dev.buildwithhq.com/appliances) ** — package isolated services, declare endpoints/egress, validate releases, and publish exact versions.
- **[Marketplace](https://dev.buildwithhq.com/marketplace) ** — discover entitled versions, install products, and manage the seller catalog, offers, artifacts, revenue, and payouts.
- **[Developer Platform](../api/index.html) ** — use REST, webhooks, connectors, MCP, the CLI, logs, AI, and versioned OpenAPI contracts.
## Customer organizations
Inside a selected SaaS app, use **Organizations** to create and administer that product’s customer accounts, tenant users, DataRoles, and Locations. See [Manage customer organizations](../getting-started/manage-customer-organizations.html) for the complete lifecycle and security boundary.
## Recommended starting path
1. Read [Launch your first app](../getting-started/your-first-app.html).
2. Use the [Builder Guide](../builder-guide/index.html) for product authoring.
3. Follow the [Developer API quickstart](../api/quickstart.html) before writing an integration.
4. Review [Selling Templates](../selling/index.html) before publishing marketplace products.
The Developer Console builds with React components, validated data bindings, reviewed APIs, and declared package or connector contracts. Platform-managed tables, database routes, provider secrets, and tenant infrastructure are not editor surfaces.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/console-guides/developer-console.html).
---
title: "Operations Console guide"
section: "Console Guides"
canonical_url: "https://support.buildwithhq.com/console-guides/operations-console.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Operations Console guide
Operate running SaaS workloads with bounded health, provisioning, backup, domain, log, analytics, and AI evidence.
## In brief
- **Home:** ops.buildwithhq.com
- **Focus:** Operate with evidence
- **Local preview:** localhost:4176
Use the Operations Console after an app exists. Its job is to show bounded deployment and runtime evidence through supported operational APIs.
[Open Operations Console](https://ops.buildwithhq.com/dashboard) [SaaS Operations](https://ops.buildwithhq.com/apps) [AI Operations](https://ops.buildwithhq.com/operations/ai)
## Primary workspaces
- **[Provisioning & deployment](https://ops.buildwithhq.com/apps) ** — current job state, retries, compensation, and logical target evidence. Select the app, then open Provisioning.
- **[Analytics](https://ops.buildwithhq.com/apps) ** — app-level usage and operational trends available to the signed-in identity. Select the app, then open Analytics.
- **[Backups](https://ops.buildwithhq.com/apps) ** — recovery points and configured destinations without exposing storage credentials. Select the app, then open Backups.
- **[Domains](https://ops.buildwithhq.com/apps) ** — hostname, verification, certificate, and routing status. Select the app, then open Domains.
- **[Logs & errors](https://ops.buildwithhq.com/apps) ** — bounded diagnostics and correlation tracing. Select the app, then open Logs.
- **[Resources & storage](https://ops.buildwithhq.com/apps) ** — entitled and consumed capacity without direct host access. Select the app, then open Resources.
- **[AI Operations](https://ops.buildwithhq.com/operations/ai) ** — endpoint health, queue/fleet state, rebuild jobs, and safe failure evidence.
## Operate through reviewed actions
Retries and recovery actions are available only through reviewed operations that enforce current expected state, lease/version fences, authorization, and correlation IDs.
## Incident handoff
Start with the app, component, time window, and correlation ID. Link the relevant bounded evidence and record the approved action taken. For developer-facing log use, see [Logs, diagnostics, and correlation tracing](../api/logs-and-observability.html).
Tip
The Operations Console answers ‘what is running and what needs attention?’ Product definition belongs in Developer; account policy belongs in Admin.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/console-guides/operations-console.html).
---
# Section: How-To Do This
---
title: "How to build CrewOS"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/crewos.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build CrewOS
Build a governed team of specialized AI operators with shared records, approval boundaries, and replayable execution evidence.
## In brief
- **Pattern:** AI agent workforce
- **First release:** One agent, one approved action
- **Build surface:** Developer Console
**CrewOS** follows the **AI agent workforce** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/crewos.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
CrewOS is now implemented and live-proven as the first product-specific Build This production solution. Its governed team roster, three managed record families, six published pages, favorites, secured file attachments, relationships, activity, approval boundaries, and deactivation lifecycle were exercised against a route-resolved disposable tenant and render-checked on desktop and mobile. It is not yet a Template Library listing; exact-version template packaging is intentionally later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Give each agent a narrow job, permitted context, declared tools, and a measurable handoff. The product is successful when a person can see what the agent learned, approve or refuse its proposed work, and reconstruct what happened afterward.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Agent profile** | Role, instructions, enabled tools, authority ceiling, owner, and active version. |
| **Work request** | Goal, requester, priority, source record, current state, and due time. |
| **Suggestion batch** | The plan produced from one activity version and its supporting context. |
| **Action suggestion** | A proposed narrow operation, inputs, risk, evidence, and approval state. |
| **Action execution** | The authorized operation, executor, result, correlation ID, and failure evidence. |
| **Rating** | Human outcome, quality score, correction, and reusable feedback. |
**Relationship rule:** Relate every request to its source business records; keep suggestions, approvals, executions, and ratings as an unbroken chain.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Mission control with agent availability and bounded queue health.
- Work-request detail with source context and plan.
- Approval queue showing evidence, risk, and exact proposed changes.
- Execution timeline and failure evidence.
- Ratings and outcome review by agent/version.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Separate who may request work, inspect context, approve an action, and operate agents.
- Apply the source record's normal DataRole, field, and location rules to agent context.
- Set explicit authority ceilings for customer-facing, financial, destructive, and external actions.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Request → read-only planning → suggestion batch.
- Suggestion → policy check → approval or refusal.
- Approved suggestion → deterministic queued execution → recorded result.
- Material source activity change → mark the old plan stale and require preparation again.
- Completed work → human rating → versioned improvement input.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Expose only declared, action-specific APIs as agent tools.
- Route external calls through managed connections or appliance endpoints.
- Send approval and failure attention to Universal Inbox without moving the source record into the inbox.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use GoClaw as the planner/worker and the canonical suggestion/execution contracts as the boundary between reasoning and mutations. Agent memory may assist retrieval, but it does not replace application records or authorization.
- Start with read-only retrieval against one record family.
- Require structured suggestions with evidence and expected current state.
- Preserve the Inbox `ActivityVersion` fence through preparation, approval, and execution.
- Evaluate exact harness/agent versions and retain ratings by version.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** A research agent reads one permitted account, prepares a follow-up task, and an authorized person approves task creation. A changed source activity invalidates the pending suggestion.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Prove an unauthorized agent cannot retrieve the record.
- Prove refused and stale suggestions cannot execute.
- Retry execution without producing duplicate work.
- Trace request, approval, tool call, and result by correlation ID.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
A useful first agent removes one recurring preparation step. Add more roles only after its permissions, approvals, and outcome measurements are trustworthy.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/crewos.html).
---
title: "How to build SignalIQ"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/signaliq.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build SignalIQ
Build account signals, explainable scoring, deal-risk views, and approved next-best actions over connected revenue records.
## In brief
- **Pattern:** AI sales intelligence
- **First release:** One signal-to-action loop
- **Build surface:** Developer Console
**SignalIQ** follows the **AI sales intelligence** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/signaliq.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
SignalIQ is now implemented and live-proven as a production Build This solution. Six managed record families cover accounts, contacts, opportunities, signals, versioned score snapshots, and evidence-backed recommendations across five professional pages. Every managed record uses the current secured create, view, edit, remove, favorite, relation, file, and activity contracts. AI Insights remain a permission-aware derived feed, recommendations retain evidence and model/score versions, and material actions stay behind the current policy or human-approval boundary. The complete routed API journey and desktop/mobile Chromium experience passed against a disposable tenant. SignalIQ is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Turn scattered account changes into a prioritized queue that tells a seller what changed, why it matters, which evidence supports it, and what action is safe to take next.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Account** | Company identity, segment, owner, health, and commercial context. |
| **Contact** | People, roles, consent, engagement, and account relationship. |
| **Opportunity** | Stage, value, probability, close date, owner, and competing context. |
| **Signal** | Typed product, intent, engagement, support, or external event with observed time. |
| **Score snapshot** | Versioned score, factors, weights, confidence, and calculation time. |
| **Recommendation** | Suggested next action, evidence, owner, state, and expiry. |
**Relationship rule:** Connect signals and recommendations to the exact account, contacts, opportunities, activities, and source events that explain them.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Ranked account and opportunity command center.
- Account 360 with activity, signals, pipeline, and support history.
- Signal feed with filters and evidence drill-down.
- Recommendation queue with owner and expiry.
- Scoring configuration and version comparison.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Scope sellers to assigned or permitted accounts; give leaders only the rollups they are authorized to see.
- Protect sensitive commercial fields separately from general account access.
- Require approval for outbound contact or CRM mutation when policy calls for it.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Inbound event → normalize → deduplicate → relate to account.
- Material signal → recompute versioned score.
- Threshold crossing → create explainable recommendation and notify owner.
- Approved recommendation → task, stage change, or connector action.
- Expired or contradicted evidence → close or replace recommendation.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Ingest CRM, product usage, intent, support, and engagement events through APIs or webhooks.
- Send approved actions to CRM, email, calendar, or sales tools.
- Retain source-system identifiers and idempotency keys for reconciliation.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use AI to summarize permitted account context and propose recommendations, while deterministic rules retain responsibility for required fields, thresholds, ownership, and safe execution.
- Build retrieval from connected account records and approved knowledge.
- Store the evidence and score/model version behind every recommendation.
- Show confidence and missing context instead of manufacturing certainty.
- Evaluate whether recommendations improve accepted actions and outcomes.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Ingest one product-usage signal, update one account score, show the evidence on the account page, and let the owner approve creation of a follow-up task.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Replay duplicate signals without duplicating recommendations.
- Prove account/field restrictions apply to scoring evidence and AI context.
- Test score-version changes against a fixed fixture.
- Trace a recommendation back to every source event.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Rank only the decision your first users actually make. One credible queue beats a broad score nobody can explain.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/signaliq.html).
---
title: "How to build Resolve"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/resolve.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build Resolve
Build a permission-aware support operation that identifies customers, retrieves policy, proposes safe resolutions, and escalates exceptions.
## In brief
- **Pattern:** AI customer support
- **First release:** One issue, one safe resolution
- **Build surface:** Developer Console
**Resolve** follows the **AI customer support** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/resolve.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
Resolve is now implemented and live-proven as a production Build This solution. Native Conversations own communication lifecycle, Universal Inbox remains the attention layer, and Knowledge Core owns versioned policy evidence. Four managed record families cover customer context, issues, resolution suggestions, and escalations across five professional responsive workspaces. Current secured APIs and stored procedures provide create, view, edit, remove, favorite/unfavorite, relationships, files, and activity. Cited AI findings remain permission-aware, and customer-facing or material work stays behind the current policy or human-approval boundary with the Inbox ActivityVersion fence. Golden/disposable SQL parity, the routed product journey, and desktop/mobile Chromium proof passed. Resolve is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Help support teams understand an incoming request, see verified customer context and policy evidence, and complete a narrow resolution without giving an AI or browser broad authority.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Conversation** | The native customer communication and message history. |
| **Customer context** | Permitted contacts, orders, subscriptions, invoices, and entitlements. |
| **Issue** | Intent, category, urgency, owner, SLA, status, and related conversation. |
| **Policy source** | Versioned support policy, product guidance, and effective dates. |
| **Resolution suggestion** | Proposed reply/action, evidence, confidence, and approval state. |
| **Escalation** | Reason, queue, assignee, severity, and handoff evidence. |
**Relationship rule:** Keep conversations and business records in their native models; use Universal Inbox as the attention layer that points to the work requiring action.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Team support inbox and SLA queue.
- Conversation workspace with customer and order context.
- Policy evidence viewer.
- Resolution approval panel.
- Escalation and quality-review queues.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Limit customer/order context to the signed-in support role and location.
- Protect payment, identity, health, or other sensitive fields independently.
- Set action-specific ceilings for credits, cancellation, access reset, and data changes.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Message received → identity match or safe manual resolution.
- Issue classification → queue, owner, SLA, and required context.
- Suggestion → policy and confidence check → approval when required.
- Approved narrow action → execute once → reply and timeline evidence.
- Missing authority or ambiguous identity → escalation, never guessed mutation.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Connect approved email, SMS, and form channels.
- Retrieve order, billing, entitlement, or delivery context through reviewed connectors.
- Expose separate narrow actions for each supported resolution instead of a general mutation endpoint.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use retrieval to ground answers in effective policy and permitted customer records. AI may prepare a reply or resolution action, but policy and authorization decide whether it can run.
- Ingest policy with source/version metadata and visual evidence where needed.
- Return citations/evidence with every suggested answer.
- Represent refunds, resets, cancellations, and escalations as separate action contracts.
- Log retrieval, refusal, approval, and exact result.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Receive one email category, match it to a customer, retrieve one policy, prepare a reply and a bounded action, and require approval before execution.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Test unknown and ambiguous customer identity.
- Test expired policy and insufficient evidence.
- Prove refund/permission ceilings cannot be bypassed.
- Replay inbound messages and actions without duplicate side effects.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Begin with a frequent, reversible issue whose policy is clear. Earn broader automation through measured accuracy and safe escalation.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/resolve.html).
---
title: "How to build LaunchPilot"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/launchpilot.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build LaunchPilot
Build a campaign cockpit for audiences, governed AI assets, approvals, channel activation, experiments, and optimization.
## In brief
- **Pattern:** AI marketing operations
- **First release:** One governed campaign
- **Build surface:** Developer Console
**LaunchPilot** follows the **AI marketing operations** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/launchpilot.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
LaunchPilot is now implemented and live-proven as a production Build This solution. Six managed record families cover campaigns, versioned consent-aware audiences, governed asset versions, approval-gated channel runs, experiments, and performance snapshots across five professional responsive workspaces. Current secured APIs and stored procedures provide create, view, edit, remove, favorite/unfavorite, relationships, evidence files, activity, Calendar, and the personal Favorites workspace. Universal Inbox and the reviewed Workflow approval contract own launch attention and decisions; channel runs retain frozen audience/asset inputs, budget, idempotency, and activation evidence, while provider activation remains prohibited until configured approval. The routed six-entity API journey and desktop/mobile Chromium experience passed against a disposable tenant. LaunchPilot is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Move a campaign from a structured brief through approved content and audience selection to a measurable channel run, while preserving consent, claims, budget, and approval evidence.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Campaign** | Objective, owner, dates, budget, state, and target outcome. |
| **Audience** | Versioned segment definition, consent requirements, and estimated size. |
| **Asset** | Brief, content, brand/policy checks, version, and approval state. |
| **Channel run** | Provider, schedule, audience snapshot, delivery state, and external IDs. |
| **Experiment** | Variants, allocation, holdout, hypothesis, and decision rule. |
| **Performance snapshot** | Period, delivery, conversion, cost, and source evidence. |
**Relationship rule:** Freeze the audience, approved asset versions, budget, and experiment allocation used by each channel run so results remain explainable.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Campaign portfolio and launch calendar.
- Campaign workspace with brief, audience, assets, and approvals.
- Audience builder and consent summary.
- Asset review with claim/brand evidence.
- Experiment and performance dashboard.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Separate content authors, approvers, audience/data operators, and channel activators.
- Restrict customer fields and consent data at record/field scope.
- Require approval for unsupported claims, regulated content, budget changes, and channel activation.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Brief approved → audience and asset work created.
- Asset version → policy/brand checks → human approval.
- Audience snapshot + approved assets + budget → scheduled channel run.
- Provider webhook → normalized delivery/performance events.
- Experiment threshold → bounded recommendation, not silent budget movement.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Connect email, advertising, CRM, CMS, analytics, and consent systems.
- Use provider webhooks to reconcile accepted, delivered, failed, and conversion states.
- Keep provider credentials and raw audience exports out of pages and logs.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Ground generation in approved brand and product knowledge. Keep deterministic validation for required disclosures, consent, budgets, channel rules, and release state.
- Generate variants against a versioned brief and audience purpose.
- Attach the source brand/policy evidence used during review.
- Treat activation and budget changes as governed actions.
- Evaluate asset quality separately from campaign outcome.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Create one campaign and consented audience, generate two asset variants, approve one, send it through a sandbox channel, and ingest delivery results.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Prove unconsented contacts cannot enter the audience snapshot.
- Prove unapproved asset versions cannot launch.
- Deduplicate provider callbacks and reconcile failures.
- Freeze experiment inputs before comparing outcomes.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Make the first release excellent at campaign control and evidence. Add autonomous optimization only after measurement and authority ceilings are proven.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/launchpilot.html).
---
title: "How to build Northstar"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/northstar.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build Northstar
Build evidence-backed executive metrics, material-change signals, forecasts, scenarios, and replayable briefings.
## In brief
- **Pattern:** executive business intelligence
- **First release:** One replayable briefing
- **Build surface:** Developer Console
**Northstar** follows the **executive business intelligence** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/northstar.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
Northstar is now implemented and live-proven as a production Build This solution. Six managed dynamic/custom-field record families cover versioned metric definitions, source-watermarked observations, forecasts, material signals, scenarios, and frozen executive briefings across five professional responsive workspaces. Current secured APIs and stored procedures provide create, view, edit, remove, favorite/unfavorite, relationships, evidence files, activity, and the personal Favorites workspace. AI Insights are permission-aware derived synthesis only; briefings retain exact signal/evidence/assumption manifests, model and harness versions, reviewer/disposition, and frozen time, while financial commitments and operating writes remain approval-gated. Golden/disposable SQL verification, the routed six-entity journey, and desktop/mobile Chromium proof passed. Northstar is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Compress operating changes into a small number of material signals and decisions, while letting leaders inspect the source, assumptions, owner, and confidence behind every claim.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Metric definition** | Formula, source, owner, cadence, unit, threshold, and active version. |
| **Metric observation** | Period, value, source watermark, quality state, and computed time. |
| **Target/forecast** | Versioned plan, assumptions, range, confidence, and owner. |
| **Signal** | Material change, severity, evidence, affected metric, and state. |
| **Scenario** | Baseline, changed drivers, outputs, confidence, and comparison. |
| **Briefing** | Frozen signals, narrative, decisions, attendees, and evidence manifest. |
**Relationship rule:** Every displayed number and narrative claim must resolve to versioned definitions, observations, source watermarks, and the briefing snapshot that used them.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Executive scorecard with freshness and quality indicators.
- Material-change queue.
- Metric drill-down with source and forecast history.
- Scenario comparison workspace.
- Briefing editor, snapshot, and decision log.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Separate broad aggregate visibility from sensitive underlying records.
- Restrict finance, people, customer, and departmental detail with existing permissions.
- Keep financial commitments and operating mutations behind action-specific approval.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Source update → validate freshness/quality → compute observation.
- Threshold/materiality rule → signal with owner and evidence.
- Scheduled synthesis → draft briefing.
- Briefing review → freeze evidence manifest and assumptions.
- Decision → assign follow-up work; never rewrite the historical briefing.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Ingest approved finance, CRM, support, product, and custom operational aggregates.
- Use bounded read APIs or scheduled imports with watermarks.
- Send approved follow-ups to the native work system while retaining a decision link.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use AI to synthesize permitted evidence and explain changes, not to invent metrics or silently alter forecasts. Definitions, calculations, materiality rules, and source freshness remain explicit.
- Retrieve only evidence visible to the current reader.
- Require citations to metric observations and source records.
- Show uncertainty, stale inputs, and conflicting evidence.
- Freeze the exact evidence and prompt/harness version used in a briefing.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Ingest three KPIs, detect one threshold crossing, draft an evidence-linked weekly briefing, and freeze the reviewed briefing with one assigned decision.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Recompute known metric fixtures deterministically.
- Test stale, missing, and conflicting source data.
- Prove restricted readers cannot drill into protected evidence.
- Reconstruct a briefing from its frozen evidence manifest.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Executive trust comes from data lineage and restraint. Start with a handful of decision-driving metrics rather than a wall of charts.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/northstar.html).
---
title: "How to build TeamBox"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/teambox.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build TeamBox
Build a shared communications command center with channel normalization, AI triage, ownership, suggestions, and governed execution.
## In brief
- **Pattern:** universal team and AI inbox
- **First release:** One channel-to-completion loop
- **Build surface:** Developer Console
**TeamBox** follows the **universal team and AI inbox** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/teambox.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
TeamBox is now implemented and live-proven as a production Build This solution. Native Conversations retain thread and message lifecycle while Universal Inbox stays the bounded attention and ownership layer. Six managed dynamic/custom-field record families cover channel intake policy, queues, routing rules, reviewed classifications, SLA policy, and handoff playbooks across five professional responsive workspaces. Every managed record uses current secured create, view, edit, remove, favorite, relation, file, and activity contracts; native Conversations also provide create, update, internal reply, favorite, and safe removal. Classification evidence retains the conversation pointer, ActivityVersion, confidence, model/harness version, and reviewed disposition. Customer-facing replies and material actions remain stale-fenced and policy- or human-approval gated. The complete routed API journey, golden/disposable procedure policy, and desktop/mobile Chromium experience passed against a disposable tenant. TeamBox is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Give a team one attention queue for incoming communications while preserving the native conversation, business record, ownership history, and approval boundary behind each item.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Conversation** | Native thread identity, participants, channel, status, and related business context. |
| **Message** | Inbound/outbound content, sender, time, channel metadata, and attachments. |
| **Inbox activity** | Attention item, activity version, priority, assignee, queue, and state. |
| **Classification** | Intent, urgency, sentiment, confidence, model/version, and evidence. |
| **Suggestion batch** | Prepared replies/actions tied to the current activity version. |
| **SLA/assignment event** | Ownership, handoff, timer, escalation, and completion history. |
**Relationship rule:** Universal Inbox points to conversations, forms, and business records; it does not replace them as the source of truth.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Shared inbox with queue, owner, priority, SLA, and channel filters.
- Conversation workspace with related customer/project context.
- Triage and reassignment panel.
- Suggested reply/action approval panel.
- Queue performance and handoff review.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Scope queues by role, DataRole, team, and location.
- Apply the related record's permission checks before showing context.
- Separate assignment, response, approval, and high-impact action capabilities.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Channel event → native conversation/message → inbox activity.
- Activity version → classify → route by deterministic rules plus permitted AI output.
- Assignee response or suggestion → approval policy → send/action.
- New message or source change → increment activity version and stale old preparation.
- SLA threshold → escalation and visible ownership event.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Connect email, SMS, forms, and authenticated inbound events.
- Normalize provider delivery callbacks into native message state.
- Use action-specific connectors for calendar, CRM, task, or external updates.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use AI for triage, summaries, and prepared actions over permission-filtered context. Preserve the activity-version stale-context fence from preparation through execution.
- Classify intent/urgency with confidence and model version.
- Attach related-record evidence before suggesting work.
- Prepare structured replies and actions separately.
- Refuse or re-prepare when the activity version changes.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Receive one form or email, create the native conversation and inbox activity, classify and assign it, then let the assignee approve and send one prepared reply.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Test duplicate and out-of-order provider events.
- Prove queue and related-record permissions both apply.
- Prove stale suggestions cannot send or mutate.
- Trace channel receipt through assignment, approval, and delivery.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
The inbox should answer what needs attention now. Keep full domain history on the source conversation and related records.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/teambox.html).
---
title: "How to build RelateEverything"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/relateeverything.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build RelateEverything
Build a CRM where people, companies, work, communications, files, and custom records reveal their connected context.
## In brief
- **Pattern:** relationship-first CRM
- **First release:** One useful 360-degree view
- **Build surface:** Developer Console
**RelateEverything** follows the **relationship-first CRM** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/relateeverything.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
RelateEverything is now implemented and live-proven as a production Build This solution. Native Contacts provide complete create, view, edit, favorite, and safe removal while six managed dynamic/custom-field record families cover companies, workstreams, opportunities, relationship context, duplicate review, and connected automations across five professional responsive workspaces. Every managed record uses current secured create, view, edit, remove, favorite, relation, file, and activity contracts. Associations use canonical Records and the registered RecordRelations type, and graph expansion emits only nodes inside the current DataRole, location, tenant, SaaS, and direct-conversation participation envelope; edges require both authorized endpoints. Connected automations retain approval-required state instead of silently performing material mutations. The complete routed native-Contact and six-entity journey, golden/disposable procedure and graph policy check, and desktop/mobile Chromium experience passed against a disposable tenant. RelateEverything is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Let a user open any important record and understand the surrounding people, companies, work, conversations, documents, and history without duplicating those records into a separate graph-shaped application.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Person** | Identity, contact details, consent, role, and relationship context. |
| **Company** | Organization identity, owner, segment, status, and commercial context. |
| **Relationship** | Typed source/target connection, direction, dates, status, and provenance. |
| **Work record** | Projects, tasks, events, opportunities, or custom operational records. |
| **Conversation/file** | Native communication or document linked into business context. |
| **Relationship event** | Created, changed, ended, inferred, or confirmed history. |
**Relationship rule:** Use typed relationships between canonical records. Do not copy a contact, file, or project merely to make it visible from another page.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Global search and recent context.
- Person and company 360-degree views.
- Relationship explorer with typed filters.
- Project/work page with people, files, tasks, and conversations.
- Relationship quality and duplicate-review queue.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Authorize both the relationship and each target record before displaying connected context.
- Protect sensitive fields even when the containing record is visible.
- Apply location and DataRole filters during graph traversal and AI retrieval.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Record created/imported → match candidates → confirm or create identity.
- Relationship change → append history and refresh relevant views.
- Connected trigger → create follow-up work on the correct source record.
- Merge review → authorized deterministic merge with preserved provenance.
- Important context change → inbox attention for affected owner.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Ingest contacts, calendars, files, messages, and CRM records with stable external IDs.
- Deduplicate through a reviewable matching process.
- Expose related context through authenticated APIs without returning inaccessible nodes.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use relationship traversal to select relevant permitted context, then let AI summarize or suggest connections. AI-inferred relationships should remain distinguishable from confirmed facts.
- Filter accessible nodes before context assembly.
- Store provenance and confidence for inferred links.
- Require review before consequential inferred relationships drive mutations.
- Evaluate retrieval for both relevance and permission leakage.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Create people, companies, projects, tasks, files, and typed relationships, then build a company page that shows only the connected context the current user may see.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Traverse mixed permitted/restricted relationships without leaking names or counts.
- Test duplicate and directional relationship rules.
- Preserve provenance during merge and unlink operations.
- Verify connected-record automations target the canonical record.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Choose a small vocabulary of relationship types first. A consistent graph is more useful than hundreds of near-duplicate labels.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/relateeverything.html).
---
title: "How to build CustomerPortal"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/customerportal.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build CustomerPortal
Build a branded customer workspace that shares selected requests, files, messages, approvals, and milestones while protecting internal work.
## In brief
- **Pattern:** customer collaboration portal
- **First release:** One secure shared request
- **Build surface:** Developer Console
**CustomerPortal** follows the **customer collaboration portal** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/customerportal.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
CustomerPortal is now implemented and live-proven as a production Build This solution. Six managed record families use 54 database-backed dynamic/custom fields for customer requests, milestones, approvals, shared updates, private internal operations, and visibility reviews across five professional responsive pages. Explicit customer and internal-staff User Types have separate reviewed page menus and assignment policies; DataRoles plus shared/internal Locations enforce record visibility. External users completed create, view, edit, remove, 25-pixel favorite, registered relationship, Favorites, and customer-page workflows while direct detail, lists, search, graph nodes/edges, fields, and internal pages all concealed private records. The routed API/SQL isolation journey and desktop/mobile Chromium proof passed against a disposable tenant. CustomerPortal is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Give customers a simple, branded place to collaborate on their own work while internal teams keep private fields, notes, assignments, AI guidance, and operating detail in the same governed application.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Customer** | Organization, account state, owner, branding, and commercial relationship. |
| **Portal member** | Verified identity, customer membership, status, and permitted role. |
| **Request** | Type, customer, submitter, status, owner, SLA, and shared summary. |
| **Comment/message** | Content, author, visibility, attachments, and related request. |
| **File** | Document metadata, owner, visibility, review state, and relationship. |
| **Approval/milestone** | Requested decision, due date, status, evidence, and shared update. |
**Relationship rule:** Derive customer membership server-side and explicitly mark shareable content; never trust a customer-supplied account or request identifier as authorization proof.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Branded customer home with their requests and milestones.
- New-request and document-upload forms.
- Shared request timeline with messages and files.
- Customer approval workspace.
- Internal sidecar with private notes, assignment, workflows, and AI guidance.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Create a customer user type using existing authorization—not a new authentication system.
- Require verified membership in the request's customer account.
- Enforce shared/private visibility server-side for fields, comments, files, and events.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Invitation → verified membership → portal access.
- Customer request → validation → internal queue and SLA.
- Internal status update → deliberately published customer update.
- File upload → scan/validation → shared or internal review state.
- Customer approval → immutable decision evidence → next internal action.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Use the same login/session APIs and custom domain/branding capabilities.
- Connect notifications and approved scheduling, billing, storage, or support systems.
- Keep storage/provider tokens and internal identifiers out of portal output.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use AI for internal summarization, routing, and prepared responses over permitted context. Expose AI output to customers only through an explicitly designed and tested customer-visible experience.
- Separate internal-only and customer-visible context before retrieval.
- Prepare replies or status summaries as reviewable suggestions.
- Never allow private notes or inaccessible files into customer output.
- Record the approved content version that was published.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Invite one customer member, let them submit a request and file, route it internally, publish one status update, and capture one customer approval.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Test cross-customer identifier substitution on every route.
- Test private fields, comments, files, counts, and search results for leakage.
- Test revoked membership and expired invitation behavior.
- Trace every externally visible update to its author/approval.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Design from the customer's smallest useful journey first; the richer internal sidecar can grow without making the portal harder to use.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/customerportal.html).
---
title: "How to build CompanyIQ"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/companyiq.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build CompanyIQ
Build permission-aware document search and scheduled intelligence that connects knowledge to customers, risks, and opportunities.
## In brief
- **Pattern:** company knowledge and opportunity intelligence
- **First release:** Search plus one evidence-backed insight
- **Build surface:** Developer Console
**CompanyIQ** follows the **company knowledge and opportunity intelligence** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/companyiq.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
CompanyIQ is now implemented and live-proven as a production Build This solution. Five managed record families use 40 database-backed dynamic/custom fields for knowledge sources, business context, evidence-backed insights, cited evidence, and governed suggested actions across five professional responsive pages. Current secured typed APIs and reviewed stored procedures provide create, view, edit, safe remove, gray/gold 25-pixel favorites, registered relationships, files, activity, ingestion, and cited semantic search. Retrieval applies the represented user's tenant, DataRole, location, record, field, and CanAiReadRecords envelope before context assembly and preserves text plus visual evidence through the canonical 256, 1024, and 1998-dimensional multimodal lanes. Golden/disposable database policy, the routed lifecycle/favorite/relation/AI journey, and desktop/mobile Chromium proof passed. CompanyIQ is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Make company knowledge searchable with source evidence, then combine permitted documents and business records to surface a small number of reviewable risks or opportunities.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **AI source** | Canonical document/source identity, origin, ownership, permission metadata, and processing state. |
| **Document element/chunk** | Extracted text and visual structure with source position and metadata. |
| **Business record** | Customer, contact, sale, project, support, meeting, or custom source context. |
| **Insight** | Typed opportunity/risk, summary, confidence, owner, state, and expiry. |
| **Insight evidence** | Exact source chunks and business records supporting or contradicting the insight. |
| **Suggested action** | Reviewable task, opportunity, follow-up, or external action. |
**Relationship rule:** Reuse the canonical AI source → element → chunk → visual index → embedding pipeline and relate evidence back to canonical business records; do not create a parallel knowledge-table family.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Natural-language knowledge search with citations.
- Source/document detail with processing state.
- Customer 360 with related knowledge and history.
- Scheduled insight queue.
- Evidence review and approved-action workspace.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Apply source, record, field, DataRole, and location access before retrieval.
- Preserve document ownership and effective access when chunking and embedding.
- Require normal action permissions even when AI discovered the opportunity.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Source accepted → extract text and visual evidence → chunk → embed → index.
- Source or permission change → bounded reprocessing and access refresh.
- Scheduled account scan → insight with evidence and expiry.
- Human review → approve, dismiss, or correct insight.
- Approved action → native opportunity/task/workflow with source link.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Accept approved files and source connectors through bounded ingestion.
- Use the current multimodal embedding profile for text, image, and mixed evidence.
- Send approved opportunities into the native CRM/work system, not directly into database tables.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use secured multimodal retrieval so tables, diagrams, screenshots, and text can all remain evidence. Current vector lanes are 256, 1024, and 1998 dimensions; any manual MRL truncation must be L2-normalized consistently for storage and query.
- Preserve source location and visual evidence during extraction.
- Filter authorized candidates before assembling model context.
- Return evidence links and distinguish contradiction or uncertainty.
- Evaluate retrieval and insight quality against fixed permission-aware fixtures.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Ingest a small approved document set, answer one cited question, relate evidence to one customer, and run one reviewable opportunity scan that can create a task.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Test text, table, diagram, and screenshot retrieval.
- Prove revoked source access disappears from search and AI context.
- Verify embedding dimensions, normalization, and query/storage parity.
- Trace each insight to source/version and business evidence.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Prove trustworthy search before proactive intelligence. Insight quality cannot exceed retrieval quality and permission correctness.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/companyiq.html).
---
title: "How to build OpsAtlas"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/opsatlas.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build OpsAtlas
Build an owned, revisioned operating-memory product with permission-aware answers, evidence, review work, knowledge gaps, and governed actions.
## In brief
- **Pattern:** AI operational knowledge core
- **First release:** One maintained collection and cited answer
- **Build surface:** Developer Console
**OpsAtlas** follows the **AI operational knowledge core** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/opsatlas.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
OpsAtlas is now implemented and live-proven as a production Build This solution. Five managed record families use 35 database-backed dynamic/custom fields for operating domains, approved source registers, review programs, gap triage, and governed workflow links across five professional responsive pages. The native Knowledge Core provides owned draft creation, immutable revisions, explicit publish/archive/restore, collections, owner assignment, due reviews, questions, feedback, gap analytics, favorites, relations, files, and activity through current typed APIs and reviewed stored procedures. Published AI-eligible knowledge enters the canonical multimodal source pipeline; cited answers apply tenant, DataRole, location, field, record, and CanAiReadRecords scope before context and preserve visual evidence across the 256, 1024, and 1998-dimensional lanes. The routed author-to-publish/favorite/relation/ingest/answer journey, database policy, and desktop/mobile Chromium proof passed. OpsAtlas is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Turn a small set of approved operating procedures into maintained Knowledge Core records that employees and agents can search in context, verify against exact revisions, and use to start the right governed workflow.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Knowledge item** | Canonical title, body, type, authority, lifecycle, AI/support eligibility, and RecordId. |
| **Immutable revision** | Versioned content, change note, author, publication state, and content hash. |
| **Collection** | Reader journey, audience, default owner, review policy, and parent collection. |
| **Owner/review cycle** | Accountable user or DataRole, due date, Task, outcome, and next review. |
| **Question/feedback** | Missing answer, repeated occurrence, assignment, response, rating, and source context. |
| **Citation/evidence** | Exact item, revision, freshness, source/page/visual identity, and permitted related records. |
**Relationship rule:** Relate each procedure to the products, departments, locations, assets, cases, work orders, conversations, files, and workflows it governs; a relation improves context but never grants access.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Knowledge home with collections, search, freshness, and health.
- Item editor/view with ownership, revisions, publication, relations, and citations.
- Review queue connected to Tasks and Universal Inbox.
- Question, feedback, and knowledge-gap workspace.
- Employee/agent answer surface with source and freshness evidence.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Separate read, edit, publish, manage ownership/reviews, link, ask/answer, and support-learning capabilities.
- Apply tenant, DataRole, location, record, field, and `CanAiReadRecords` checks before retrieval or model context.
- Keep customer-safe and internal-only collections distinct and recheck disclosure at response time.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Draft → review/edit → permission-checked publish → immutable revision citation.
- Review due → Task/Inbox attention → confirm current, revise/republish, reassign, or archive.
- No authoritative answer → Knowledge Question → assignment/answer → optional GeneratedCandidate Draft.
- Usage/negative feedback/repeated gaps → secured health signal and owner follow-up.
- Resolved support case → structured sanitization → owner/review Task → Draft candidate; authorized publication only.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Ingest only approved current sources with explicit audience, owner, authority, review policy, and AI eligibility.
- Preserve files, PDF pages, images, tables, diagrams, and screenshots through the canonical multimodal AI pipeline.
- Expose read-only diagnostics separately from knowledge; route any material action through existing workflow/GoClaw approval contracts.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use Knowledge Core as the governed source layer for secured semantic and multimodal retrieval. Only Published, AI-eligible revisions enter normal RAG, and every answer retains exact source/revision/freshness evidence.
- Index approved revisions through the existing source, element, chunk, visual-index, and embedding chain.
- Filter by the represented user's full security envelope before context assembly.
- Prefer current Official/Verified evidence and surface conflicts, staleness, or missing authority.
- Create a question or escalation instead of fabricating an unsupported answer.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Create one customer-safe collection with an owner and 30-day review policy, publish three approved procedures, relate one to a business record, answer one question with an exact citation, and turn one missing answer into a reviewable Knowledge Question.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Prove Draft, archived, stale-policy, internal-only, other-tenant, other-location, and AI-denied material cannot enter a normal answer.
- Prove citations resolve to the exact immutable revision and source evidence.
- Prove duplicate review cycles/questions/support candidates remain idempotent.
- Change source activity or permissions and verify stale context or revoked access fails closed.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Start with one operating area whose owner will actually review it. A small current corpus with accountable citations is more useful than a large unowned document dump.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/opsatlas.html).
---
title: "How to build Headless SaaS DB"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/headless-saas-db.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build Headless SaaS DB
Build a managed, permission-aware SaaS backend for custom web, mobile, desktop, or existing frontends.
## In brief
- **Pattern:** API-first SaaS backend
- **First release:** One authenticated end-to-end resource
- **Build surface:** Developer Console
**Headless SaaS DB** follows the **API-first SaaS backend** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/headless-saas-db.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
Headless SaaS DB is now implemented and live-proven as a production Build This solution. Five managed record families use 35 database-backed dynamic/custom fields for domain objects, API client profiles, inbound contracts, webhook policies, and correlation evidence across five professional responsive pages. The exported server-side TypeScript client covers schema/search and complete record, favorite, relation, inbound-endpoint, webhook, delivery-retry, and correlation-log operations. A working React administration client keeps the live app credential and physical routing inside its same-origin BFF while showing accurate 25-pixel gray/gold favorite stars. The routed public-API journey proved create, read, update, remove cleanup, favorite/unfavorite, relation add/remove, read-only denial, webhook create/rotate, safe correlation evidence, and absence of SQL/provider authority; current golden and disposable procedure policy plus desktop/mobile Chromium proof passed. It is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Let a custom frontend use BuildWithHQ records, relationships, workflows, files, AI, and integrations without exposing SQL, provider secrets, tenant infrastructure, or authorization decisions to the client.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Domain record** | The customer, project, transaction, content, or custom object your product owns. |
| **Relationship** | Reviewed connections between domain records. |
| **API client/profile** | App-bound client metadata and external credential-source reference. |
| **Inbound endpoint** | Declared event contract, authentication, validation, and processing policy. |
| **Webhook subscription** | Allowed event, target, signing state, delivery policy, and status. |
| **Operation evidence** | Correlation ID, actor/client, bounded outcome, and safe failure detail. |
**Relationship rule:** Treat client-supplied identifiers as query targets only; derive the builder account, SaaS app, tenant account, user, role, and location from verified server identity.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Your custom customer-facing frontend.
- Internal admin/operations views for the same records.
- Developer Console API-client and integration setup.
- Webhook delivery and correlation-log diagnostics.
- Optional BuildWithHQ pages for back-office workflows.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Use app-bound credentials or user sessions appropriate to the caller.
- Enforce tenant, record, field, DataRole, and location scope in the backend.
- Give every client only the operations and fields its use case requires.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Authenticated request → identity/context derivation → validation → typed service.
- Typed service → reviewed stored procedure → bounded response.
- State change → workflow/event → signed webhook delivery.
- Inbound event → authenticate → validate → deduplicate → queue/process.
- Failure → safe problem response and correlation evidence.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Start from the generated OpenAPI contract and current REST routes.
- Use externally referenced credentials; never persist raw bearer values in profiles or frontend code.
- Put specialized libraries/services in an appliance with declared endpoints and egress.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Call secured AI capabilities through the API or declared tools. The browser sends the user's request; the server performs authorization, retrieval, context assembly, and governed action handling.
- Keep model/provider credentials server-side.
- Apply the same data permissions used by normal record APIs.
- Return bounded evidence and correlation identifiers.
- Represent material AI mutations as authorized action contracts.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** From a small custom frontend, authenticate, list and create one permitted record type, invoke one workflow, receive one signed webhook, and trace the whole exchange by correlation ID.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Generate/validate clients against the current OpenAPI document.
- Test cross-app, cross-tenant, field, role, and location denials.
- Test duplicate requests/events and webhook retries.
- Confirm no secrets, SQL details, or internal routes appear in output/logs.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Build one complete API resource before designing a broad generic backend. Its auth, errors, idempotency, events, and observability become the pattern for the rest.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/headless-saas-db.html).
---
title: "How to build CaseFlow"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/caseflow.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build CaseFlow
Build controlled case stages, intake, work queues, documents, decisions, approvals, SLAs, and replayable history.
## In brief
- **Pattern:** case and process management
- **First release:** One intake-to-decision path
- **Build surface:** Developer Console
**CaseFlow** follows the **case and process management** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/caseflow.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
CaseFlow is now implemented and live-proven as a production Build This solution. Seven managed record families use 53 database-backed dynamic/custom fields for cases, parties, stage history, tasks, evidence, decisions, and approvals across five professional responsive workspaces. Current secured typed APIs and reviewed stored procedures provide complete lifecycle, gray/gold 25-pixel favorites, registered relationships, files, and activity. The server-owned CaseFlow transition boundary fixes creation at Intake, serializes adjacent stages, rejects direct, stale, invalid, and out-of-order changes, and requires an approved Checklists & Signoffs run before Decision or Closed. Five role policies and two locations preserve permission scope. The routed intake-to-decision API journey and desktop/mobile Chromium proof passed. CaseFlow is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Move a case through a defined process while accumulating participants, documents, messages, work, decisions, and approvals—with every transition explainable and properly authorized.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Case** | Type, subject, customer, owner, stage, priority, SLA, and current version. |
| **Participant** | Person/organization role, effective dates, and contact relationship. |
| **Stage transition** | From/to stage, actor, time, reason, expected version, and evidence. |
| **Task/checklist** | Required work, owner, due date, completion, and stage dependency. |
| **Document/communication** | Native file or conversation related to the case with visibility. |
| **Decision/approval** | Question, recommendation, authority, outcome, reason, and evidence. |
**Relationship rule:** Make the case the coordination record while participants, files, conversations, and business records remain canonical and permissioned in their native models.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Role-aware case queue with SLA and stage filters.
- Structured intake form.
- Case 360 with timeline, parties, files, messages, and tasks.
- Reviewer checklist and evidence workspace.
- Decision/approval and audit replay views.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Scope case visibility by type, role, customer, geography/location, and sensitivity.
- Protect sensitive fields/documents independently from case access.
- Define who may transition, decide, approve, reopen, and export.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Intake → validate required identity/data → triage.
- Stage transition → expected-state and required-check validation.
- Missing document → request and timer; receipt → review task.
- SLA threshold → escalation and ownership event.
- Recommendation → authorized decision/approval → immutable transition evidence.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Accept forms, messages, documents, and authenticated API submissions.
- Use managed notifications and signature/identity connectors where required.
- Export only bounded authorized case data through reviewed APIs.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use AI to summarize long histories, identify missing information, and prepare next actions. It must cite permitted case evidence and cannot make regulated or material decisions unless an explicit policy and authorized action allow it.
- Assemble context from the case and accessible related records.
- Distinguish facts, missing data, and recommendations.
- Bind suggestions to the case/activity version so changed evidence makes them stale.
- Capture reviewer corrections as evaluation evidence.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Submit one case type, route it to triage, request and receive one document, complete a review checklist, and record one authorized decision.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Test invalid and out-of-order stage transitions.
- Test SLA clocks, pause rules, and escalation fixtures.
- Prove sensitive evidence is absent for restricted reviewers.
- Replay the case timeline into the same explainable final state.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Model the happy path and three important exceptions first. Case systems become unmanageable when every exception is encoded as a new status.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/caseflow.html).
---
title: "How to build AssetOps"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/assetops.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build AssetOps
Build customer, location, asset, inspection, work-order, technician, parts, and service-history operations.
## In brief
- **Pattern:** asset and field operations
- **First release:** One inspection-to-work-order loop
- **Build surface:** Developer Console
**AssetOps** follows the **asset and field operations** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/assetops.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
AssetOps is now implemented and live-proven as a production Build This solution. Five product-specific record families use 47 database-backed dynamic/custom fields for sites, assets, inspection findings, parts usage, and service history across five professional responsive workspaces. Native Contacts own customers and technicians; native Work Orders own maintenance state and assignments; native Checklists & Signoffs own versioned inspections and proof; native Files own photos and documents. Current secured typed APIs and reviewed stored procedures provide complete lifecycle, 25-pixel gray/gold favorites, relationships, activity, valid work-order transitions, optimistic concurrency, exact proof-location matching, and server-enforced location and assignee scope. The routed customer-to-asset-to-inspection-to-completed-work journey and desktop/mobile Chromium proof passed. AssetOps is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Connect each physical asset to its customer/site hierarchy, current condition, inspections, service work, technicians, parts, documents, photos, and complete maintenance history.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Customer/location** | Customer, property, facility, room, route, and location hierarchy. |
| **Asset** | Type, serial, parent asset, site, warranty, state, and service attributes. |
| **Inspection** | Template/version, inspector, readings, answers, photos, result, and time. |
| **Issue** | Finding, severity, source inspection, status, and required response. |
| **Work order** | Asset, issue, priority, assignee, schedule, state, labor, and resolution. |
| **Part/service evidence** | Parts, measurements, manuals, images, invoices, notes, and historical events. |
**Relationship rule:** Use parent/child and typed relationships for customer → site → asset/component, then attach inspections, issues, work, people, files, and invoices to their canonical records.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Operations dashboard and work queue.
- Customer/site/asset 360-degree detail.
- Mobile-friendly inspection and issue capture.
- Dispatcher schedule and technician assignment.
- Work-order execution with parts, photos, signature, and closeout.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Scope technicians and managers by assigned work, team, customer, and location.
- Protect customer, warranty, invoice, and sensitive site fields separately.
- Define who may inspect, dispatch, close, reopen, or approve chargeable work.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Scheduled/due asset → inspection task.
- Failed reading/check → issue → work-order rule.
- Work order → assignment, schedule, SLA, and customer notification.
- Technician closeout → required evidence validation → asset history.
- Recurring condition → maintenance suggestion for human review.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Use mobile forms/files for readings, signatures, photos, and offline-tolerant field capture where supported.
- Connect approved inventory, accounting, calendar, mapping, or messaging providers.
- Ingest device/telemetry events through authenticated, deduplicated endpoints or an appliance service.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use permitted service history, readings, manuals, images, and inspection evidence to summarize condition and prepare maintenance work. Do not let a prediction silently change asset state or schedule chargeable service.
- Preserve visual inspection evidence and source metadata.
- Relate the model's evidence to the exact asset and service event.
- Express recommendations with confidence and maintenance horizon.
- Require approval for work creation/scheduling according to policy.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Create one customer, site, and asset; complete a mobile inspection; turn one failed check into a work order; assign and close it with photo evidence.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Test location and assignment restrictions in list, search, and detail views.
- Test duplicate telemetry and inspection submissions.
- Require closeout evidence and valid state transitions.
- Trace each maintenance suggestion to readings and history.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Start with one asset class and one inspection template. Generalize only after the field workflow works end to end.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/assetops.html).
---
title: "How to build ActiveWorkplace"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/activeworkplace.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build ActiveWorkplace
Build a Wave-like shared coordination surface around any permitted Record and its secured recursive relationship graph.
## In brief
- **Pattern:** recursive work coordination
- **First release:** Pin one Record and coordinate its graph
- **Build surface:** Developer Console
**ActiveWorkplace** follows the **recursive work coordination** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/activeworkplace.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
ActiveWorkplace is now implemented and live-proven as a production Build This solution. Two managed record families use 16 database-backed dynamic/custom fields for pinnable work items and reusable graph playbooks across four professional responsive workspaces. The native first-class ActiveWorkplace module owns idempotent pinning, bounded cyclic graphs, participants and responsibilities, deterministic Pulse, retained activity and snapshots, and read-only playback; Universal Inbox remains the attention layer. Current secured typed APIs and reviewed stored procedures provide complete record lifecycle, 25-pixel gray/gold favorites, registered relationships, stale-write protection, and current tenant, DataRole, location, and AI-read enforcement. The routed pin-to-playback journey, golden/disposable procedure policy, cleanup, and desktop/mobile Chromium proof passed. ActiveWorkplace is not yet a Template Library listing; exact-version packaging remains later work.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Let a team pin an existing business Record, see the permitted connected work around it, assign explicit responsibilities, notice inactivity, and replay retained history without copying the underlying business data or creating a second permission system.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Root Record** | The existing permitted customer, case, project, asset, opportunity, conversation, or other canonical Record. |
| **ActiveWorkplace** | Coordination name, owner, Pulse, bounded graph policy, stale timing, and current version. |
| **Participant assignment** | A User, GoClaw, or Workflow plus responsibility, watch/action settings, and approval policy. |
| **Workplace event** | Immutable comments, participant changes, Pulse transitions, graph changes, and correlation evidence. |
| **Graph snapshot** | A bounded identity-and-topology snapshot used for playback; it does not copy business payloads. |
| **Inbox attention** | The de-duplicated At Risk or Stale item that points back to the root and workplace. |
**Relationship rule:** Keep every business item in its native module. Reuse typed Record Relations for the Active Graph and store only bounded identities/topology in workplace snapshots.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- My/All ActiveWorkplaces list with search and Pulse filters.
- Pin-record picker over currently visible Records.
- Workplace detail with root, Pulse, graph, participants, and activity.
- Participant/responsibility editor for eligible Users, GoClaws, and Workflows.
- Read-only Graph Playback with a clear historical banner and Return to current action.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Require current read/edit permission to pin and manage the root.
- Reauthorize every graph node by tenant, SaaS app, DataRole, and location before display.
- Require independent AI-read access for a GoClaw backing user; assignment never grants it.
- Intersect playback identities with current authorization and remove all historical mutation paths.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Visible Record -> pin once -> initial graph snapshot and owner participant.
- Meaningful update or related Record change -> refresh activity/Pulse and retained state.
- Waiting expiry or inactivity -> Active/At Risk/Stale transition -> create or touch Inbox attention.
- Eligible participant assignment -> responsibility recorded without changing its authority.
- Resolved -> retain history and prevent accidental active duplicate behavior according to policy.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Use Universal Inbox for human attention rather than creating a separate alert queue.
- Use existing GoClaw events/suggestions/actions for AI participation and material approval.
- Use normal workflow definitions and APIs for deterministic business actions.
- Keep ActiveWorkplace tenant-local; involve AI Orchestration only when an AI job actually runs.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
AI is optional. A GoClaw can monitor, research, summarize, and prepare work only through its represented user's current AI-read envelope. Record Pulse itself is deterministic and does not need a model.
- Start with a non-acting GoClaw watcher and a clear responsibility.
- Retrieve only currently authorized graph records and fields.
- Represent any material next step as the existing governed suggestion/action contract.
- Keep human approval for customer-facing or material work until policy and harness evidence support more authority.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Pin one case or opportunity, show a cyclic three-record graph, add one person and one read-only GoClaw, let inactivity create one Inbox item, and replay the workplace with every write control removed.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Prove a hidden-location graph branch and its edges are omitted.
- Prove a GoClaw without independent CanAiReadRecords cannot be assigned.
- Evaluate Pulse twice and prove the second run does not duplicate Inbox attention.
- Change current location access and prove playback cannot restore the old view.
- Retry pinning and prove the same active root workplace is returned.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
The feature is strongest when the graph is meaningful and bounded. Begin with one work type, a small relation vocabulary, and a Pulse rule the team already understands.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/activeworkplace.html).
---
title: "How to build FieldService"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/fieldservice.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build FieldService
Build customer, service-area, scheduling, job, estimate, approval, evidence, and follow-up operations for field teams.
## In brief
- **Pattern:** field-service management
- **First release:** One request-to-closeout service call
- **Build surface:** Developer Console
**FieldService** follows the **field-service management** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/fieldservice.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
FieldService is now implemented and live-proven as a production Build This solution refreshed from the existing reference implementation. Template-owned Change Requests use 12 database-backed dynamic/custom fields while native Contacts own customers and team profiles, native Work Orders own jobs and legal transitions, native Calendar owns scheduling, native Files and Checklists & Signoffs own evidence and approval, and native Conversations and Universal Inbox own communications and attention. Six professional responsive workspaces support dispatcher, customer, technician, schedule, evidence, manager approval, and follow-up work. Current secured typed APIs and reviewed stored procedures provide complete lifecycle, 25-pixel gray/gold favorites, registered relationships, optimistic concurrency, server-enforced location and assignee scope, and read-only governed AI/estimator boundaries. The routed request-to-approved-closeout journey, golden/disposable procedure policy, template bundle checks, and desktop/mobile Chromium proof passed. Tenant billing remains explicitly deferred and this is not yet a Template Library listing.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Move one customer request through triage, scheduling, assignment, field execution, evidence, authorization, closeout, and follow-up while keeping dispatchers, technicians, managers, and customers inside their own experience and data scope.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Customer/service location** | Contact, address, service area, location scope, equipment, and history. |
| **Service request** | Intake channel, issue, priority, customer, site, and triage state. |
| **Work order** | Job scope, schedule, assigned user, status, version, labor, and completion. |
| **Estimate/approval** | Line items, amount, authority, decision, reason, and evidence. |
| **Calendar/resource** | Appointment, duration, technician/resource assignment, and conflicts. |
| **Service evidence** | Notes, readings, images, files, signature, communications, and follow-up. |
**Relationship rule:** Keep Contacts, Calendar, Work Orders, files, conversations, payments, and Inbox attention canonical in their first-class modules, linked to the service request and customer graph.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Dispatcher queue and service-area calendar.
- Customer, site, and equipment history.
- Technician work-order workspace with mobile evidence capture.
- Estimate and manager/customer approval page.
- Closeout evidence and follow-up workspace.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Create User Types for dispatcher, technician, manager, and customer experiences.
- Scope work by DataRole, assigned technician/team, customer, and Location.
- Protect pricing, customer, and site fields independently.
- Require explicit authority for assignment, estimate approval, closeout, and reopen.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Inbound request -> triage -> service-area and priority routing.
- Approved scope -> schedule and assign technician/resource.
- Field arrival -> checklist, readings, notes, photos, and status transitions.
- Estimate -> authorized approval with retained evidence.
- Closeout -> customer notice, evidence, and scheduled follow-up.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Receive email, SMS, forms, and authenticated API requests through the Universal Inbox.
- Use Calendar, mapping, inventory, accounting, and messaging connectors as needed.
- Put specialized estimating or device logic behind a declared appliance endpoint.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use permitted service history, manuals, text, images, and readings to answer field questions and prepare summaries or estimates. Keep chargeable, customer-facing, and state-changing work inside explicit approval and action contracts.
- Retrieve only the technician's permitted customer, asset, and job evidence.
- Cite the exact source record, file, page, image, or reading.
- Bind suggestions to the current work-order/activity version.
- Require configured approval before creating an estimate, message, charge, or material state transition.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Accept one service request, schedule and assign it, complete a field checklist with photo evidence, approve an estimate, close the work order, and schedule customer follow-up.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Test cross-location, unassigned-technician, customer, and field-level denials.
- Test schedule conflicts and stale work-order versions.
- Prove duplicate requests and follow-up retries are idempotent.
- Trace request, work, approval, evidence, and follow-up by correlation ID.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Start with one trade, one service-call type, and one closeout checklist. Generalize only after dispatch-to-follow-up works end to end.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/fieldservice.html).
---
title: "How to build Reservations"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/reservations.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build Reservations
Build bookable products, capacity, assignments, calendars, tenant-controlled payments, and pre/post-service follow-up.
## In brief
- **Pattern:** reservations and booking operations
- **First release:** One book-to-follow-up path
- **Build surface:** Developer Console
**Reservations** follows the **reservations and booking operations** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/reservations.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
Reservations is now implemented and live-proven as a production Build This solution over the current first-class Reservations module. One managed Reservation Policies record family uses 10 database-backed dynamic/custom fields, while native Reservation Products, Reservations, Contacts, Calendar, Files, Favorites, Record Graph, Record Activity, tenant payment controls, and durable pre/post-service follow-ups retain their current secured typed APIs and reviewed stored procedures. Five professional responsive workspaces cover booking, operations, product/capacity setup, customers, and payment/follow-up. The routed journey proved product setup/update/deactivation, authenticated booking, idempotent replay, server-locked capacity, party limits, assignment, Contact and Calendar creation, free-booking payment state, paid-booking payment-method enforcement, stale-write rejection, follow-up update, favorite, relation, file evidence, check-in/completion, audited cancellation, and cleanup. Golden/disposable procedure policy and desktop/mobile Chromium proof passed. Anonymous checkout and Template Library packaging remain intentionally separate.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Let an authenticated customer or operator select a bookable product, protect capacity, create or reuse the Contact, place the service on the Calendar, assign an eligible tenant user or resource, coordinate tenant-controlled payment, and deliver scheduled follow-up.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Bookable product** | Duration, buffers, price/currency, capacity, party limits, defaults, and follow-up timing. |
| **Reservation** | Contact, product, start/end, party size, status, assignment, location, version, and notes. |
| **Capacity/resource** | Availability, hold, location, eligible resource/user, and conflict state. |
| **Calendar event** | Reservation-linked schedule entry and assigned participants/resources. |
| **Payment intent** | Tenant payment policy, provider reference, amount, currency, and synchronized state. |
| **Follow-up work** | Before/after-service time, template, channel, delivery state, attempts, and evidence. |
**Relationship rule:** Keep Contacts, Calendar, payment orchestration, communications, tenant users, Locations, and resources canonical, with the Reservation as the lifecycle record that links them.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Bookable-product setup.
- Availability and authenticated booking flow.
- Reservation list and detail workspace.
- Calendar, assignment, and capacity view.
- Payment state and pre/post-service follow-up queue.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Separate operator, assigned provider, manager, and customer User Type experiences.
- Scope reservations by DataRole, Location, assignment, and Contact relationship.
- Protect payment references, internal notes, and customer fields independently.
- Require explicit permissions for product setup, assignment, cancellation, payment actions, and override.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Select product/time -> validate availability -> protect capacity -> create reservation.
- Create or reuse Contact -> assign eligible user/resource -> create Calendar event.
- Free booking -> confirm; paid booking -> tenant-controlled charge intent and synchronized state.
- Before-service due time -> queue and send configured reminder/follow-up.
- Completion/cancellation -> release capacity and schedule or cancel after-service communication.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Use tenant-owned payment configuration and provider orchestration; keep credentials and provider calls server-side.
- Use Calendar and communications workers for durable schedules and delivery evidence.
- Use authenticated inbound endpoints and signed webhooks for external channel/provider state.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
Use AI only where it improves permitted scheduling, customer-service, or follow-up work. It may summarize context or draft a message, but it cannot invent availability, confirm a charge, or bypass reservation state rules.
- Retrieve only permitted Contact, Reservation, product, and communication context.
- Treat capacity and payment state as deterministic service results, not model output.
- Bind drafts and suggestions to the current reservation/activity version.
- Require policy or human approval for customer-facing or material actions.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Configure one paid service, book it for an existing or new Contact, protect one capacity slot, assign one tenant user, create its Calendar event and charge intent, then deliver one before- and one after-service follow-up.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Test cross-tenant, DataRole, Location, assignment, and Contact denials.
- Test concurrent capacity claims, buffers, party limits, and stale reservation versions.
- Prove booking, charge-intent, provider-state, and follow-up retries are idempotent.
- Trace reservation, calendar, payment reference, and communication evidence by correlation ID.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Make availability and capacity deterministic before polishing the booking screen. A beautiful form cannot repair an overbooked schedule.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/reservations.html).
---
title: "How to build Support Tickets"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/support-tickets.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build Support Tickets
Turn selected inbound email addresses into durable support-ticket queues inside Universal Inbox.
## In brief
- **Pattern:** technical support ticketing
- **First release:** One mailbox-to-resolution support path
- **Build surface:** Developer Console
**Support Tickets** follows the **technical support ticketing** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/support-tickets.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
Support Tickets is now implemented and live-proven as a production Build This solution over native Conversations and Universal Inbox. One managed Support Playbooks record family uses 14 database-backed dynamic/custom fields across five professional responsive workspaces for command, ticket conversation, customer context, mailbox/service-level configuration, and knowledge-backed resolution. Current secured typed APIs and reviewed stored procedures provide mailbox-owner ticketing configuration, atomic stable ticket allocation, assignment, priority, customer reply, explicit resolution, automatic reopen on later inbound mail, and Inbox ActivityVersion stale-context protection. Support playbooks provide complete create, view, edit, safe remove, 25-pixel gray/gold favorite, registered relationship, file, and activity lifecycle. The routed mailbox-to-resolution journey, golden/disposable procedure policy, cleanup, and desktop/mobile Chromium proof passed. This is not yet a Template Library listing.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Let an account owner enable ticketing on a selected email address, give each new conversation a stable human-readable ticket number, and move the request through assignment and explicit resolution states without creating a second message or permission system.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Email channel** | The existing inbound ChannelConnection plus the Ticketing switch, short prefix, and next-number allocator. |
| **Conversation** | The canonical email thread and message history; replies remain on the same source conversation. |
| **Inbox ticket** | The attention record, stable mailbox-scoped key, assignee, priority, SLA, resolution state, and ActivityVersion. |
| **Customer context** | The permitted Contact, company, subscription, product, or service records related to the conversation. |
| **Resolution evidence** | Notes, files, approvals, sent messages, status changes, and correlation/audit history. |
| **Knowledge evidence** | Permission-aware articles, manuals, policies, and prior records used to prepare an answer. |
**Relationship rule:** Keep Conversations and Messages canonical. Universal Inbox contributes ticket identity, attention, routing, ownership, SLA, and resolution state, while related customer and product records remain in their native modules.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Support-ticket queue with owner, priority, SLA, channel, and resolution filters.
- Ticket conversation workspace with message history and related customer context.
- Account-owner email settings with a per-address Ticketing checkbox and prefix.
- Knowledge/evidence viewer for grounded support research.
- Resolution, reopen, escalation, and service-level reporting views.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Allow only an active tenant account owner to configure mailbox ticketing.
- Derive tenant, SaaS application, user, DataRole, and Location scope from verified server identity.
- Authorize the source Conversation and related records on every read; a ticket number grants no access.
- Require action-specific permission and configured approval for customer-facing or material operations.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Ticket-enabled inbound email -> canonical Conversation -> atomically allocated ticket key -> Inbox attention.
- Reply on the same thread -> retain ticket key -> increment ActivityVersion -> invalidate stale preparation.
- Assignment or response -> New/Open/Waiting Customer/Waiting Internal/Snoozed state.
- Resolved or Closed ticket receives a new reply -> reopen and return to attention.
- SLA threshold or exception -> escalate through the same Inbox ownership and audit path.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Use the existing inbound-email connector and delivery worker; do not expose provider credentials to the browser.
- Use Contacts, subscriptions, products, files, and custom records as related context rather than copying them into ticket tables.
- Use Knowledge Core and secured vector retrieval for evidence-backed suggestions.
- Use GoClaw suggestion and action contracts when AI prepares or executes governed work.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
AI may classify, summarize, research permitted evidence, and draft a reply. It must apply CanAiReadRecords, tenant, DataRole, and Location security before context assembly, preserve the ActivityVersion stale-context fence, and never send or mutate material state outside the configured approval policy.
- Load the current ticket and only currently permitted related records.
- Retrieve source-cited knowledge through the secured AI/vector path.
- Bind every draft or proposed action to the current Inbox ActivityVersion.
- Route customer-facing or material actions through the canonical suggestion, approval, and execution contracts.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Enable Ticketing for one support email address, receive a new thread, allocate one stable key, assign and reply to it, resolve it, then prove a later reply reopens the same ticket without changing its number.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Prove a non-owner cannot read or change mailbox ticketing configuration.
- Send concurrent first messages and prove ticket numbers are unique and monotonically allocated per mailbox.
- Retry ingestion and prove the same Conversation keeps one ticket key.
- Resolve a ticket, receive a reply, and prove it reopens with a higher ActivityVersion.
- Disable Ticketing and prove existing tickets retain identity while new conversations remain ordinary Inbox items.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Enable ticketing only on addresses that represent a support promise. Keep general mailboxes conversational so the team's Inbox stays useful instead of turning every email into a case.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/support-tickets.html).
---
title: "How to build Checkoff & Approval"
section: "How-To Do This"
canonical_url: "https://support.buildwithhq.com/how-to-do-this/checkoff-approval.html"
reviewed_at: "2026-09-14"
authority: Official
---
# How to build Checkoff & Approval
Build reusable checklists whose items can require notes, documents, photos, and governed approval before work is accepted.
## In brief
- **Pattern:** checklists, evidence, and signoff
- **First release:** One evidence-backed checklist
- **Build surface:** Developer Console
**Checkoff & Approval** follows the **checklists, evidence, and signoff** pattern. This guide turns the [product concept](https://buildwithhq.com/buildthis/checkoff-approval.html) into a buildable first release using BuildWithHQ records, pages, workflows, permissions, integrations, AI, and exact-version packaging.
Note
Checkoff & Approval is now implemented and live-proven as a production Build This solution over native Checklists & Signoffs, Files, Workflow, Universal Inbox, Favorites, Record Graph, and Record Activity. One managed Verification Programs record family uses 12 database-backed dynamic/custom fields across five professional responsive workspaces for command, template editing, attached work, evidence review, and signoff/revision. Current secured typed APIs and reviewed stored procedures provide template list/read/create/revision-fenced update/deactivation, exact run snapshots, bounded item revisions, verified document/photo evidence, idempotent submission, Inbox ActivityVersion fencing, reject/revise/resubmit, and human approval. The public Developer API recipe now includes the full template lifecycle. The routed evidence-to-approval journey, golden/disposable procedure and developer-grant policy, cleanup, OpenAPI/SDK checks, and desktop/mobile Chromium proof passed. This is not yet a Template Library listing.
[Open SaaS Apps](https://dev.buildwithhq.com/apps) [First-app guide](../getting-started/your-first-app.html) [Page editor](../builder-guide/page-editor.html) [Workflows](../builder-guide/workflows.html)
## Define the outcome before the screens
Attach a bounded checklist to a record, ticket, project, or work order; let authorized users complete its items with required proof; and submit the finished run through the existing Workflow approval system without creating a parallel approval engine.
Note
Treat every name in this blueprint as a starting point. Use the language of the industry you are serving, but preserve clear ownership, status, permission, and audit boundaries.
## 1. Create the smallest useful data model
In the Developer Console, create or select the SaaS app and model these as application record types and relationships. The page editor composes React components and validated data bindings, which call the platform APIs for these records.
| Record type | What it holds |
| --- | --- |
| **Checklist template** | Reusable name, version, item order, proof requirements, and activation state. |
| **Checklist item template** | Instruction, required state, proof policy, order, and optional responsible role. |
| **Checklist run** | The versioned checklist attached to one authorized source record, current status, owner, and revision. |
| **Run item** | Completion state, actor, completion time, note, and optimistic-concurrency revision. |
| **Evidence** | A native File relationship identifying the document or photo supplied as proof for one item. |
| **Signoff** | The existing Workflow run, approval step, Inbox attention, decision, comment, and audit evidence. |
**Relationship rule:** Keep Files, Workflow approvals, Inbox attention, users, DataRoles, Locations, and the attached business record canonical. The checklist stores only its template/run lifecycle and explicit links to that evidence and approval work.
## 2. Build the working screens
Start with task-oriented pages instead of a page for every table. The first navigation should contain:
- Checklist-template editor with ordered items and proof rules.
- Attached checklist list on records, tickets, projects, and work orders.
- Run workspace with up to 1,000 bounded items, progress, notes, documents, and photos.
- Submit-for-signoff and reviewer decision panel.
- Rejection, revision, proof replacement, resubmission, and approval history.
Use [Puck](../builder-guide/page-editor.html) for the normal visual experience. Keep stable block identity when using [Monaco advanced JSON mode](../builder-guide/raw-json-mode.html) so future template upgrades can merge safely.
## 3. Set access before adding automation
- Authorize the attached source record before listing or opening its checklists.
- Apply tenant, DataRole, Location, record, and action permissions to every run, item, and evidence operation.
- Require current file access for proof downloads; an evidence link never grants access to a File.
- Use the existing Workflow decision permissions and Inbox ActivityVersion fence for signoff.
User Type menus organize page links for each audience. An entitled published page may also be requested directly when it is absent from the menu. Menus do not grant the records or actions on those pages. The server derives the account, SaaS app, user, DataRole, and location scope from the verified identity on every data request.
## 4. Add the core workflows
- Create or attach run -> snapshot exact template version and ordered items.
- Complete item -> validate required note/file/photo proof -> record actor, revision, and evidence links.
- All required items complete -> submit once -> create or reuse one Workflow approval and Inbox item.
- Reject -> return to editable state -> replace proof or edit items -> resubmit with current revision.
- Approve -> lock the accepted result while preserving the complete activity, file, and Workflow evidence chain.
Make each mutation a narrow, reviewed action with explicit inputs, allowed states, correlation evidence, and an idempotent retry policy where retries are possible. Pages, integrations, and agents all reach the same application service and stored-procedure contract through supported APIs.
## 5. Connect channels and outside systems
- Use native Files and attachment upload APIs for documents and photos.
- Use Workflow for approval decisions and Universal Inbox for reviewer attention.
- Use Record Relations to attach runs to any authorized business record without copying that record.
- Use background workers for scanning, thumbnails, delivery, or other asynchronous evidence processing.
Use [managed connections](../builder-guide/external-api-connections.html), [OAuth connectors](../api/connectors-and-oauth.html), [inbound endpoints](../api/inbound-endpoints.html), and [webhooks](../api/webhooks.html). Put specialized SDKs or private processing behind a declared [appliance endpoint](../builder-guide/appliances-and-endpoints.html); never expose provider credentials to a page.
## 6. Add AI as a governed layer
AI may summarize a permitted checklist or highlight missing evidence, but it cannot mark work complete, manufacture proof, approve signoff, or read attached records/files beyond the represented user's normal and AI-specific permissions.
- Assemble context only from the authorized run, source record, and readable evidence.
- Keep completion and proof validation deterministic.
- Bind suggestions to the current run revision and Inbox ActivityVersion.
- Route any material follow-up through the normal suggestion, permission, and approval contracts.
AI and retrieval must apply `CanAiReadRecords`, tenant, DataRole, field, and location security before context is assembled. Planning and research remain read-only. Customer-facing or material changes use an action suggestion and the configured policy or human approval before execution.
## 7. Ship one complete vertical slice
**Recommended first release:** Attach one checklist to a work order, complete its required items with a note and photo, submit it for Workflow signoff, reject it once, replace the proof, resubmit, and approve it while preserving one coherent audit chain.
1. Create the minimum record types and seed representative, non-production data.
2. Build the primary list/queue and one complete detail page.
3. Implement one state-changing workflow and its denial/error states.
4. Add the narrowest useful integration or inbound channel.
5. Add one permission-aware AI read or suggestion only if it improves that workflow.
6. Test as an owner, a normal operator, a restricted user, and an unauthorized user.
## 8. Validate, package, and operate
- Test another tenant, DataRole, Location, source-record denial, and unauthorized file download.
- Test two users completing the same item and stale run revisions.
- Test duplicate submit/approve/reject requests for idempotency.
- Test reject, edit, proof replacement, resubmit, approve, and activity history.
- Test empty, typical, and 1,000-item runs without returning unbounded unrelated records.
Publish a reviewed exact version as a [template](../builder-guide/templates-forking.html) when the pattern is reusable. If you sell it, complete the [marketplace preparation](../selling/preparing-a-template.html) checks. Template installs are version snapshots; upstream edits must never silently mutate a tenant copy.
**Built-in data path:** React components and data bindings call authenticated APIs; typed application services execute reviewed stored procedures and return bounded results with audit and correlation evidence.
Tip
Make proof requirements explicit per item. A photo or document should demonstrate a specific completed step, not become an unstructured dumping ground attached to the whole job.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/how-to-do-this/checkoff-approval.html).
---
# Section: First Class Modules
---
title: "Connected work"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/connected-work.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Connected work
Find records you can read, explore their visible connections, and open their native pages to work on them.
## In brief
- **Runtime key:** module.connected-work
- **Experience:** Read-only record discovery
- **Security:** Server-enforced
Use Connected work when a task crosses several modules and you need to follow the records that connect it. This is a read-only discovery view; create, edit, remove, and relationship management stay on the native record pages.
[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.connected-work. 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
Find records you can read, explore their visible connections, and open their native pages to work on them.
## Key capabilities
- Search by saved record title with Search records, then use Load more records for additional results.
- Choose a result to see its visible related work. Explore follows a connected record without copying it.
- Open native record and Open record use persisted page associations. When several destinations are available, choose the page you want; no destination is guessed.
- A display-limit notice means the shown connections are bounded, not the complete relationship count. Explore a connected record to continue.
- If loading fails, Try again repeats the requested search or connection lookup under your current access.
## Common uses
- Follow a meeting to its related work order or file.
- Find the native page for a record discovered through another module.
- Review visible relationships before opening the owning record.
## How it connects
Connected work reads the existing secured record search and Record Graph. It links to the original records and their persisted native pages; it does not create a second record store or a new mutation route.
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
Every search and connection read uses the verified user, tenant organization, and SaaS app. Personal ownership, DataRole, location, private-record, and action rules remain enforced by the secured services. A visible relationship or page link never grants access to its target.
- 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. Add the validated `module.connected-work` block to an active, entitled published page.
2. Reuse that page in the UserType menus that need it; each menu may use its own label for the same page.
3. Verify native page associations for the records your users need. Test no destination, one destination, and several destinations without inventing a page key.
4. Test empty results, loading, retry, additional search results, bounded connections, and denied records as representative users.
## Build a professional Connected work dashboard
These six registry-backed presentation blocks let a designer turn the secured Connected work 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/record-graph`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/connected-work-sample.json`, its executable authenticated page at `pages/first-class/connected-work-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 Connected work projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Visible relations, connected records, types, and recent links |
| `entity-list` | `core.entity-list` | Highly connected records inside the current scope |
| `progress-list` | `core.progress-list` | Distribution by relation type or owning module |
| `series-chart` | `core.series-chart` | Relationship creation and removal |
| `data-grid` | `core.presentation-grid` | One bounded edge page with both endpoints authorized |
| `timeline` | `core.timeline` | Create, update, remove, source loss, and graph traversal 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": "connected-work-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Connected work",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "relations",
"label": "Visible relations",
"value": 3218,
"format": "number",
"tone": "neutral"
},
{
"key": "records",
"label": "Connected records",
"value": 1407,
"format": "number",
"tone": "primary"
},
{
"key": "types",
"label": "Relation types",
"value": 18,
"format": "number",
"tone": "success"
},
{
"key": "new",
"label": "Added this week",
"value": 96,
"format": "number",
"tone": "warning"
}
]
},
"children": []
},
{
"_id": "connected-work-recent",
"_type": "core.entity-list",
"props": {
"title": "Highly connected records",
"hasMore": true,
"items": [
{
"id": "connected-work-sample-1",
"recordId": "connected-work-record-1",
"primary": "Acme Field Services",
"secondary": "Contact - 34 visible relations",
"status": {
"key": "connected",
"label": "Connected",
"tone": "success"
},
"trailing": "34 links"
},
{
"id": "connected-work-sample-2",
"recordId": "connected-work-record-2",
"primary": "Acme Field Services - Follow-up",
"secondary": "Contact - 34 visible relations - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "connected-work-sample-3",
"recordId": "connected-work-record-3",
"primary": "Acme Field Services - West region",
"secondary": "Contact - 34 visible relations - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "connected-work-sample-4",
"recordId": "connected-work-record-4",
"primary": "Acme Field Services - Customer response",
"secondary": "Contact - 34 visible relations - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "connected-work-sample-5",
"recordId": "connected-work-record-5",
"primary": "Acme Field Services - Regional operations review with a deliberately long title",
"secondary": "Contact - 34 visible relations - 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": "connected-work-sample-6",
"recordId": "connected-work-record-6",
"primary": "Acme Field Services - Completed preview",
"secondary": "Contact - 34 visible relations - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "connected-work-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Relations by type",
"items": [
{
"key": "customer",
"label": "Customer of",
"value": 1210,
"maximum": 3218,
"displayValue": "1210",
"tone": "primary",
"status": {
"key": "customer",
"label": "Customer of",
"tone": "primary"
}
},
{
"key": "attached",
"label": "Attached to",
"value": 1084,
"maximum": 3218,
"displayValue": "1084",
"tone": "success",
"status": {
"key": "attached",
"label": "Attached to",
"tone": "success"
}
},
{
"key": "depends",
"label": "Depends on",
"value": 924,
"maximum": 3218,
"displayValue": "924",
"tone": "warning",
"status": {
"key": "depends",
"label": "Depends on",
"tone": "warning"
}
}
]
},
"children": []
},
{
"_id": "connected-work-trend",
"_type": "core.series-chart",
"props": {
"title": "Relationship 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": "Removed",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "connected-work-table",
"_type": "core.presentation-grid",
"props": {
"title": "Visible relationships",
"columns": [
{
"key": "from",
"label": "From",
"type": "text",
"align": "left"
},
{
"key": "relation",
"label": "Relationship",
"type": "text",
"align": "left"
},
{
"key": "to",
"label": "To",
"type": "text",
"align": "left"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "created",
"label": "Created",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "connected-work-row-1",
"recordId": "connected-work-record-1",
"cells": {
"from": "Acme Field Services",
"relation": "Customer of",
"to": "WO-1048",
"status": {
"key": "active",
"label": "Active",
"tone": "success"
},
"created": "2026-09-04T11:11:00Z"
}
},
{
"id": "connected-work-row-2",
"recordId": "connected-work-record-2",
"cells": {
"from": "Acme Field Services - Follow-up",
"relation": "Customer of",
"to": "WO-1048",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"created": "2026-09-04T15:42:00Z"
}
},
{
"id": "connected-work-row-3",
"recordId": "connected-work-record-3",
"cells": {
"from": "Acme Field Services - West region",
"relation": "Customer of",
"to": "WO-1048",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"created": "2026-09-04T12:18:00Z"
}
},
{
"id": "connected-work-row-4",
"recordId": "connected-work-record-4",
"cells": {
"from": "Acme Field Services - Customer response",
"relation": "Customer of",
"to": "WO-1048",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"created": "2026-09-03T21:07:00Z"
}
},
{
"id": "connected-work-row-5",
"recordId": "connected-work-record-5",
"cells": {
"from": "Acme Field Services - Regional operations review with a deliberately long title",
"relation": "Customer of - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"to": "WO-1048",
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"created": "2026-09-03T16:31:00Z"
}
},
{
"id": "connected-work-row-6",
"recordId": "connected-work-record-6",
"cells": {
"from": "Acme Field Services - Completed preview",
"relation": "Customer of",
"to": "WO-1048",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"created": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 3218,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "connected-work-timeline",
"_type": "core.timeline",
"props": {
"title": "Relationship activity",
"hasMore": true,
"items": [
{
"id": "connected-work-event-1",
"recordId": "connected-work-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Relationship created",
"description": "Acme Field Services was related to WO-1048.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "connected-work-event-2",
"recordId": "connected-work-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Relationship created - Follow-up",
"description": "Acme Field Services was related to WO-1048. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "connected-work-event-3",
"recordId": "connected-work-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Relationship created - West region",
"description": "Acme Field Services was related to WO-1048. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "connected-work-event-4",
"recordId": "connected-work-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Relationship created - Customer response",
"description": "Acme Field Services was related to WO-1048. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "connected-work-event-5",
"recordId": "connected-work-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Relationship created - Regional operations review with a deliberately long title",
"description": "Acme Field Services was related to WO-1048. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "connected-work-event-6",
"recordId": "connected-work-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Relationship created - Completed preview",
"description": "Acme Field Services was related to WO-1048. 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 ConnectedWorkPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface ConnectedWorkPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: ConnectedWorkPresentationData | 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 ConnectedWorkPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: ConnectedWorkPresentationProps) {
if (error) return
The Connected work presentation could not be loaded.
;
if (loading || !data) return
Loading Connected work presentation...
;
return (
Connected work
Connected work
Fictional connected-record design data; the live page searches visible records and opens their native owners.
);
}
```
### 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/record-graph`, 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": "connected-work-live",
"_type": "module.connected-work",
"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
Use the native module for lifecycle actions. A short connection list is not an all-records report, and menu placement never substitutes for data permissions.
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/connected-work.html).
---
title: "Knowledge finder"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/knowledge-finder.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Knowledge finder
Search accessible Knowledge Core articles, filter publication state, preview current text, and open the native article page.
## In brief
- **Runtime key:** module.knowledge-finder
- **Experience:** Read-only article discovery
- **Security:** Server-enforced
Use Knowledge finder for everyday article lookup. This read-only view does not create, edit, publish, archive, or remove articles; Open article takes you to the native Knowledge Core page for those separately authorized actions.
[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.knowledge-finder. 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
Search accessible Knowledge Core articles, filter publication state, preview current text, and open the native article page.
## Key capabilities
- Enter a search and choose All accessible articles, Published, Draft, or Archived in Publication state, then select Search articles.
- Use Previous article page and Next article page to browse the applied search and publication-state filter.
- Preview an article to read its current publication state, authority level, revision, summary, and article text.
- Draft and archived previews explicitly say they are unavailable as published citations. Selecting a filter never grants access to those articles.
- Open article resolves the record's persisted native page choices. If no destination is available, use the displayed unavailable-state guidance rather than guessing a route.
- Try again retries the failed search page or article preview using current permissions.
## Common uses
- Find an operating procedure by its title or search text.
- Review an accessible draft before opening Knowledge Core.
- Check an article's current revision and publication state.
## How it connects
Knowledge finder uses the existing secured Knowledge Core list and detail services. It displays the current article text and opens the same canonical record in its owning page; it does not generate AI answers or queue indexing.
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 verified user, tenant organization, SaaS app, and persisted page identity determine the current request. Knowledge Core applies its record, Personal ownership, DataRole, location, private-record, and action-specific permissions. Search terms, publication filters, menu labels, and client-supplied IDs are query targets, never authorization proof.
- 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. Add the validated `module.knowledge-finder` block to an active, entitled published page.
2. Reuse the page across UserType menus with the labels each audience needs.
3. Ensure the intended native Knowledge Core pages are associated with their records, then test the Open article choices.
4. Test empty, loading, retry, page navigation, preview, and denied access with representative users, including the distinction between readable drafts and published citations.
## Build a professional Knowledge finder dashboard
These six registry-backed presentation blocks let a designer turn the secured Knowledge finder 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/knowledge-articles`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/knowledge-finder-sample.json`, its executable authenticated page at `pages/first-class/knowledge-finder-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 Knowledge finder projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Published, draft, due-review, stale, and gap counts |
| `entity-list` | `core.entity-list` | Important articles, owners, freshness, and use |
| `progress-list` | `core.progress-list` | Distribution by freshness, lifecycle, category, or owner |
| `series-chart` | `core.series-chart` | Draft, publish, review, use, and gap activity |
| `data-grid` | `core.presentation-grid` | One bounded article page with lifecycle evidence |
| `timeline` | `core.timeline` | Revision, publication, review, retrieval, feedback, gap, and archive |
### 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": "knowledge-finder-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Knowledge finder",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "published",
"label": "Published",
"value": 684,
"format": "number",
"tone": "success"
},
{
"key": "review",
"label": "Review due",
"value": 31,
"format": "number",
"tone": "warning"
},
{
"key": "draft",
"label": "Drafts",
"value": 24,
"format": "number",
"tone": "neutral"
},
{
"key": "gaps",
"label": "Open gaps",
"value": 12,
"format": "number",
"tone": "danger"
}
]
},
"children": []
},
{
"_id": "knowledge-finder-recent",
"_type": "core.entity-list",
"props": {
"title": "Knowledge needing attention",
"hasMore": true,
"items": [
{
"id": "knowledge-finder-sample-1",
"recordId": "knowledge-finder-record-1",
"primary": "Escalation playbook",
"secondary": "Support operations - owner: Maya Chen",
"status": {
"key": "review-due",
"label": "Review due",
"tone": "warning"
},
"trailing": "14 uses"
},
{
"id": "knowledge-finder-sample-2",
"recordId": "knowledge-finder-record-2",
"primary": "Escalation playbook - Follow-up",
"secondary": "Support operations - owner: Maya Chen - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "knowledge-finder-sample-3",
"recordId": "knowledge-finder-record-3",
"primary": "Escalation playbook - West region",
"secondary": "Support operations - owner: Maya Chen - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "knowledge-finder-sample-4",
"recordId": "knowledge-finder-record-4",
"primary": "Escalation playbook - Customer response",
"secondary": "Support operations - owner: Maya Chen - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "knowledge-finder-sample-5",
"recordId": "knowledge-finder-record-5",
"primary": "Escalation playbook - Regional operations review with a deliberately long title",
"secondary": "Support operations - owner: Maya Chen - 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": "knowledge-finder-sample-6",
"recordId": "knowledge-finder-record-6",
"primary": "Escalation playbook - Completed preview",
"secondary": "Support operations - owner: Maya Chen - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "knowledge-finder-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Knowledge freshness",
"items": [
{
"key": "current",
"label": "Current",
"value": 611,
"maximum": 684,
"displayValue": "611",
"tone": "success",
"status": {
"key": "current",
"label": "Current",
"tone": "success"
}
},
{
"key": "due",
"label": "Due soon",
"value": 42,
"maximum": 684,
"displayValue": "42",
"tone": "warning",
"status": {
"key": "due",
"label": "Due soon",
"tone": "warning"
}
},
{
"key": "stale",
"label": "Stale",
"value": 31,
"maximum": 684,
"displayValue": "31",
"tone": "danger",
"status": {
"key": "stale",
"label": "Stale",
"tone": "danger"
}
}
]
},
"children": []
},
{
"_id": "knowledge-finder-trend",
"_type": "core.series-chart",
"props": {
"title": "Knowledge lifecycle",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Published",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Reviewed",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "knowledge-finder-table",
"_type": "core.presentation-grid",
"props": {
"title": "Authorized knowledge",
"columns": [
{
"key": "article",
"label": "Article",
"type": "text",
"align": "left"
},
{
"key": "owner",
"label": "Owner",
"type": "text",
"align": "left"
},
{
"key": "freshness",
"label": "Freshness",
"type": "status",
"align": "left"
},
{
"key": "uses",
"label": "Uses",
"type": "number",
"align": "right"
},
{
"key": "review",
"label": "Next review",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "knowledge-finder-row-1",
"recordId": "knowledge-finder-record-1",
"cells": {
"article": "Escalation playbook",
"owner": "Maya Chen",
"freshness": {
"key": "due",
"label": "Review due",
"tone": "warning"
},
"uses": 14,
"review": "2026-09-10T00:00:00Z"
}
},
{
"id": "knowledge-finder-row-2",
"recordId": "knowledge-finder-record-2",
"cells": {
"article": "Escalation playbook - Follow-up",
"owner": "Maya Chen",
"freshness": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"uses": 15,
"review": "2026-09-04T15:42:00Z"
}
},
{
"id": "knowledge-finder-row-3",
"recordId": "knowledge-finder-record-3",
"cells": {
"article": "Escalation playbook - West region",
"owner": "Maya Chen",
"freshness": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"uses": 16,
"review": "2026-09-04T12:18:00Z"
}
},
{
"id": "knowledge-finder-row-4",
"recordId": "knowledge-finder-record-4",
"cells": {
"article": "Escalation playbook - Customer response",
"owner": "Maya Chen",
"freshness": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"uses": 17,
"review": "2026-09-03T21:07:00Z"
}
},
{
"id": "knowledge-finder-row-5",
"recordId": "knowledge-finder-record-5",
"cells": {
"article": "Escalation playbook - Regional operations review with a deliberately long title",
"owner": "Maya Chen - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"freshness": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"uses": 18,
"review": "2026-09-03T16:31:00Z"
}
},
{
"id": "knowledge-finder-row-6",
"recordId": "knowledge-finder-record-6",
"cells": {
"article": "Escalation playbook - Completed preview",
"owner": "Maya Chen",
"freshness": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"uses": 19,
"review": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 684,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "knowledge-finder-timeline",
"_type": "core.timeline",
"props": {
"title": "Knowledge activity",
"hasMore": true,
"items": [
{
"id": "knowledge-finder-event-1",
"recordId": "knowledge-finder-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Review requested",
"description": "Escalation playbook entered its scheduled review window.",
"actor": "Knowledge Core",
"tone": "warning"
},
{
"id": "knowledge-finder-event-2",
"recordId": "knowledge-finder-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Review requested - Follow-up",
"description": "Escalation playbook entered its scheduled review window. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "knowledge-finder-event-3",
"recordId": "knowledge-finder-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Review requested - West region",
"description": "Escalation playbook entered its scheduled review window. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "knowledge-finder-event-4",
"recordId": "knowledge-finder-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Review requested - Customer response",
"description": "Escalation playbook entered its scheduled review window. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "knowledge-finder-event-5",
"recordId": "knowledge-finder-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Review requested - Regional operations review with a deliberately long title",
"description": "Escalation playbook entered its scheduled review window. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "knowledge-finder-event-6",
"recordId": "knowledge-finder-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Review requested - Completed preview",
"description": "Escalation playbook entered its scheduled review window. 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 KnowledgeFinderPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface KnowledgeFinderPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: KnowledgeFinderPresentationData | 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 KnowledgeFinderPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: KnowledgeFinderPresentationProps) {
if (error) return
The Knowledge finder presentation could not be loaded.
;
if (loading || !data) return
Loading Knowledge finder presentation...
;
return (
Knowledge finder
Knowledge finder
Fictional article-discovery design data; the live page searches accessible articles and opens their native owner.
);
}
```
### 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/knowledge-articles`, 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": "knowledge-finder-live",
"_type": "module.knowledge-finder",
"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
Keep article lifecycle work in Knowledge Core. This search page is not an AI answer, and an accessible draft is not automatically eligible for a published citation.
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/knowledge-finder.html).
---
title: "Universal Inbox"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/universal-inbox.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Universal Inbox
Collects configured external email, optional support tickets, SMS, web-form submissions, and other inbound communications into one secured attention layer where unsorted work stays visible and routing rules can classify, prioritize, assign, and queue it for action.
## In brief
- **Runtime key:** core.universal-inbox
- **Experience:** Inbound work & triage
- **Security:** Server-enforced
Use Universal Inbox as the front door for communication and work arriving from outside the organization. It gives users one place to see both unclassified incoming items and communication already sorted into a queue, owner, priority, or workflow.
[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 core.universal-inbox. 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
Collects configured external email, optional support tickets, SMS, web-form submissions, and other inbound communications into one secured attention layer where unsorted work stays visible and routing rules can classify, prioritize, assign, and queue it for action.
## Key capabilities
- Receive configured inbound email and SMS through native Conversations and messages.
- Let an account owner enable Ticketing per inbound email address and assign stable mailbox-scoped ticket numbers.
- Use New, Open, WaitingCustomer, WaitingInternal, Snoozed, Resolved, and Closed as one controlled ticket-resolution lifecycle.
- Receive web-form submissions and other authenticated inbound source events without losing their native source record.
- Keep unmatched or unsorted items visible for human triage instead of dropping them into a hidden channel silo.
- Apply deterministic routing rules and permitted AI classification to assign queues, owners, tags, priorities, and due/SLA attention.
- Open the full native source context, prepare replies or actions, approve governed suggestions, snooze, reassign, and complete work.
- Increment `ActivityVersion` when new source activity arrives so stale drafts or actions cannot execute.
## Optional support tickets per email address
Ticketing is an email-channel setting, not a second inbox. An account owner opens Email ticketing setup, checks **Ticketing** for the intended inbound address, and chooses a 2–12 character letter/number prefix. New conversations on that address receive a permanent key such as `SUP-000123`. Other mailboxes remain ordinary Inbox channels.
The ticket reuses the Inbox queue, assignment, priority, SLA, activity, approval, and status controls. Resolving or closing it changes the Inbox attention state; it does not rewrite the native Conversation. A later inbound reply retains the ticket key, reopens attention, and makes preparation against the older `ActivityVersion` stale.
## Common uses
- One intake queue for support, sales, service, operations, and customer requests.
- A technical-support mailbox with durable ticket identity and resolution state.
- Sorting unknown inbound communication before it reaches a team or person.
- Shared ownership, SLA, escalation, and GoClaw-assisted preparation across external channels.
## How it connects
Email and SMS normalize into native Conversations; web forms remain native submissions; other inbound endpoints keep their canonical source records. Inbox items point to those sources as the attention layer. Sorting changes queue/ownership/attention state—it does not move or copy the underlying conversation, form, or business record.
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
An item is visible only when the current user can access its source record and effective queue scope. Routing never grants source access. GoClaw preparation, approval, and execution reapply the user's record/DataRole/location envelope, AI-read gate, action permission, and current ActivityVersion.
- The authenticated service derives the SaaS app, tenant account, user, DataRole, and location scope; browser identifiers are never authorization proof.
- For the standard experience, the tenant shell already renders `UniversalInboxBell` in the top bar and opens the complete `UniversalInbox` panel. No custom React is required. Builders may also place the registered `core.universal-inbox` block on a published page and launch it from a User Type menu.
- List, search, detail, relation, activity, favorite, and write operations reapply their required server-side permissions.
## Add it to an app
1. For a standard BuildWithHQ app, write no React. Confirm the authorized user can see the built-in Inbox bell in the tenant top bar; it opens the native Inbox panel.
2. If users also need a permanent workspace, create a shared page with PageKey `Inbox`, add the registered `core.universal-inbox` block, save, validate, and publish it.
3. For every intended User Type, choose **Add existing** on the Inbox page, order it in that menu, and choose **Save menu**. The resulting link opens `/?page=Inbox`; menu placement adds navigation without granting page or Inbox data authority.
4. Connect each supported external channel through its reviewed email, SMS, web-form, or inbound-endpoint contract.
5. For a support mailbox, open Email ticketing setup as the tenant account owner, check Ticketing, and select a unique operational prefix.
6. Define the default Unsorted/Triage queue so unmatched communication remains visible and owned operationally.
7. Add routing rules for source, recipient/line, form, intent, customer, location, priority, or other permitted context.
8. Configure queues, assignment, SLA/escalation, and the existing permissions for the people who will triage or act.
9. Enable GoClaw preparation only for permitted users/queues, then test the top-bar panel and menu page plus new tickets, same-thread replies, reopen after resolution, ordinary mailboxes, duplicate delivery, late events, and stale-activity cases end to end.
10. Use the optional React examples below only when maintaining a custom tenant runtime, replacing the application shell, or building a headless same-origin presentation.
## React is optional
The standard tenant application already mounts the Inbox bell and native panel in its shell. Channel setup, ticketing, routing, queues, permissions, and GoClaw policy are configuration work; they do not require a React component. Use one of the following integrations only when you want the Inbox somewhere other than the standard top-bar panel.
| Experience | What to add |
| --- | --- |
| Normal BuildWithHQ app | Nothing. Use the built-in top-bar Inbox. |
| Menu-launched Inbox page | Add the registered `core.universal-inbox` block to a published page and add that PageKey to each intended User Type menu. |
| Custom tenant shell | Mount `UniversalInboxBell` and open `UniversalInbox` in your dialog. |
| Headless presentation | Inject the official authenticated client into your own markup. |
### Menu-launched embedded Inbox page
Create a shared page named **Inbox**. In Puck, select **Universal Inbox** from BuildWithHQ components. Or switch to Monaco advanced JSON and paste this complete page document. Save the draft, validate it, and publish it.
```
[
{
"_id": "universal-inbox-page",
"_type": "core.universal-inbox",
"props": {},
"children": []
}
]
```
Back in the selected User Type, choose **Add existing** on the Inbox page and then **Save menu**. The saved menu item has this shape. Use the Design studio rather than writing `SaasAppMenus` directly because the authoring API applies version and route checks.
```
{
"id": "inbox",
"label": "Inbox",
"targetType": "Page",
"pageKey": "Inbox",
"sortOrder": 0
}
```
The menu opens `/?page=Inbox`. The runtime bootstrap verifies that this User Type may open that PageKey, returns the authorized component contract, and then the block loads only Inbox rows visible to the signed-in user. The top-bar bell remains available; both presentations share the same tenant-session client and server authorization.
### Custom tenant-runtime page component
The registered page renderer is already bundled in BuildWithHQ. If you maintain a custom tenant-runtime fork, this self-contained equivalent mounts the complete native queue, ticketing settings, detail, activity, workflow approval, and human-state controls. The native component imports its own styles.
```
import {
UniversalInbox,
createUniversalInboxHttpClient,
} from "@buildwithhq/universal-inbox";
import { getTenantAccessToken, tenantSessionFetch } from "./api";
import { ensureCsrfToken } from "./webSecurity";
const inboxClient = createUniversalInboxHttpClient({
accessToken: getTenantAccessToken,
csrfToken: ensureCsrfToken,
fetchImplementation: tenantSessionFetch,
});
interface DedicatedInboxPageProps {
readonly initialInboxItemId?: string;
}
export function DedicatedInboxPage({
initialInboxItemId,
}: DedicatedInboxPageProps) {
return (
);
}
```
### Custom tenant-shell top bar and dialog
Use this only when replacing the standard shell. Do not mount a second bell alongside the one that is already present.
```
import { useEffect, useState } from "react";
import {
UniversalInbox,
UniversalInboxBell,
createUniversalInboxHttpClient,
} from "@buildwithhq/universal-inbox";
import { getTenantAccessToken, tenantSessionFetch } from "./api";
import { ensureCsrfToken } from "./webSecurity";
const inboxClient = createUniversalInboxHttpClient({
accessToken: getTenantAccessToken,
csrfToken: ensureCsrfToken,
fetchImplementation: tenantSessionFetch,
});
export function InboxTopBarControl() {
const [open, setOpen] = useState(false);
const [initialInboxItemId, setInitialInboxItemId] = useState();
useEffect(() => {
if (!open) return undefined;
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") setOpen(false);
};
window.addEventListener("keydown", closeOnEscape);
return () => window.removeEventListener("keydown", closeOnEscape);
}, [open]);
return (
<>
{
setInitialInboxItemId(inboxItemId);
setOpen(true);
}}
/>
{open && (
{
if (event.target === event.currentTarget) setOpen(false);
}}
>
);
}
export function HeadlessInboxApp({
accessToken,
csrfToken,
authenticatedFetch,
}: HeadlessInboxAuth) {
const client = useMemo(
() => createUniversalInboxHttpClient({
accessToken,
csrfToken,
...(authenticatedFetch ? { fetchImplementation: authenticatedFetch } : {}),
}),
[accessToken, authenticatedFetch, csrfToken],
);
return ;
}
```
### Exact browser and SQL path
| Purpose | HTTP route | Reviewed procedure |
| --- | --- | --- |
| Top-bar counts | `GET /api/inbox/badge` | `sp_GetInboxBadgeSecured` |
| Queue | `GET /api/inbox/items` | `sp_Inbox_GetQueueSecured` |
| Detail and activity | `GET /api/inbox/items/{inboxItemId}` | `sp_Inbox_GetItemDetailSecured` |
| Read/status/priority/assignment | `PUT /api/inbox/items/{inboxItemId}/human-state` | `sp_Inbox_MarkRead` / `sp_Inbox_SetStatusSecured` |
| Email ticketing setup | `GET /api/inbox/email-channels/ticketing`
`PUT /api/inbox/email-channels/{channelConnectionId}/ticketing` | `sp_Inbox_GetEmailTicketingSettingsSecured` / `sp_Inbox_SetEmailTicketingSecured` |
These identifiers select an already authorized target; none of them establish tenant, user, DataRole, location, queue, or source-record authority. See the [React component catalog](../builder-guide/react-component-catalog.html) for generated starters and the [headless guide](../builder-guide/headless-application-kit.html) for the surrounding authentication and proxy boundary.
## Build a professional Universal Inbox dashboard
These six registry-backed presentation blocks let a designer turn the secured Universal Inbox 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/inbox`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/universal-inbox-sample.json`, its executable authenticated page at `pages/first-class/universal-inbox-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 Universal Inbox projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Open, unsorted, SLA-risk, assigned, and resolved counts |
| `entity-list` | `core.entity-list` | Newest authorized items requiring attention |
| `progress-list` | `core.progress-list` | Distribution by queue, channel, priority, or ticket state |
| `series-chart` | `core.series-chart` | Received, assigned, replied-to, and resolved work |
| `data-grid` | `core.presentation-grid` | One bounded page of display-ready Inbox and ticket state |
| `timeline` | `core.timeline` | Delivery, triage, assignment, reply, escalation, resolution, and reopen 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": "universal-inbox-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Inbox operations",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "open",
"label": "Open items",
"value": 238,
"format": "number",
"tone": "primary"
},
{
"key": "unsorted",
"label": "Needs triage",
"value": 31,
"format": "number",
"tone": "warning"
},
{
"key": "sla",
"label": "SLA at risk",
"value": 8,
"format": "number",
"tone": "danger"
},
{
"key": "resolved",
"label": "Resolved today",
"value": 74,
"format": "number",
"tone": "success"
}
]
},
"children": []
},
{
"_id": "universal-inbox-recent",
"_type": "core.entity-list",
"props": {
"title": "Items needing attention",
"hasMore": true,
"items": [
{
"id": "universal-inbox-sample-1",
"recordId": "universal-inbox-record-1",
"primary": "SUP-000482 - Login fails after update",
"secondary": "Email - Priority support queue",
"status": {
"key": "needs-response",
"label": "Needs response",
"tone": "warning"
},
"trailing": "12 min"
},
{
"id": "universal-inbox-sample-2",
"recordId": "universal-inbox-record-2",
"primary": "SUP-000482 - Login fails after update - Follow-up",
"secondary": "Email - Priority support queue - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "universal-inbox-sample-3",
"recordId": "universal-inbox-record-3",
"primary": "SUP-000482 - Login fails after update - West region",
"secondary": "Email - Priority support queue - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "universal-inbox-sample-4",
"recordId": "universal-inbox-record-4",
"primary": "SUP-000482 - Login fails after update - Customer response",
"secondary": "Email - Priority support queue - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "universal-inbox-sample-5",
"recordId": "universal-inbox-record-5",
"primary": "SUP-000482 - Login fails after update - Regional operations review with a deliberately long title",
"secondary": "Email - Priority support queue - 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": "universal-inbox-sample-6",
"recordId": "universal-inbox-record-6",
"primary": "SUP-000482 - Login fails after update - Completed preview",
"secondary": "Email - Priority support queue - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "universal-inbox-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Open work by queue",
"items": [
{
"key": "support",
"label": "Technical support",
"value": 112,
"maximum": 216,
"displayValue": "112",
"tone": "primary",
"status": {
"key": "support",
"label": "Technical support",
"tone": "primary"
}
},
{
"key": "success",
"label": "Customer success",
"value": 73,
"maximum": 216,
"displayValue": "73",
"tone": "success",
"status": {
"key": "success",
"label": "Customer success",
"tone": "success"
}
},
{
"key": "triage",
"label": "Unsorted",
"value": 31,
"maximum": 216,
"displayValue": "31",
"tone": "warning",
"status": {
"key": "triage",
"label": "Unsorted",
"tone": "warning"
}
}
]
},
"children": []
},
{
"_id": "universal-inbox-trend",
"_type": "core.series-chart",
"props": {
"title": "Inbox throughput",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Received",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Resolved",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "universal-inbox-table",
"_type": "core.presentation-grid",
"props": {
"title": "Authorized inbox items",
"columns": [
{
"key": "subject",
"label": "Subject",
"type": "text",
"align": "left"
},
{
"key": "channel",
"label": "Channel",
"type": "text",
"align": "left"
},
{
"key": "queue",
"label": "Queue",
"type": "text",
"align": "left"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "updated",
"label": "Updated",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "universal-inbox-row-1",
"recordId": "universal-inbox-record-1",
"cells": {
"subject": "SUP-000482 - Login fails after update",
"channel": "Email",
"queue": "Technical support",
"status": {
"key": "needs-response",
"label": "Needs response",
"tone": "warning"
},
"updated": "2026-09-04T17:48:00Z"
}
},
{
"id": "universal-inbox-row-2",
"recordId": "universal-inbox-record-2",
"cells": {
"subject": "SUP-000482 - Login fails after update - Follow-up",
"channel": "Email",
"queue": "Technical support",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"updated": "2026-09-04T15:42:00Z"
}
},
{
"id": "universal-inbox-row-3",
"recordId": "universal-inbox-record-3",
"cells": {
"subject": "SUP-000482 - Login fails after update - West region",
"channel": "Email",
"queue": "Technical support",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"updated": "2026-09-04T12:18:00Z"
}
},
{
"id": "universal-inbox-row-4",
"recordId": "universal-inbox-record-4",
"cells": {
"subject": "SUP-000482 - Login fails after update - Customer response",
"channel": "Email",
"queue": "Technical support",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"updated": "2026-09-03T21:07:00Z"
}
},
{
"id": "universal-inbox-row-5",
"recordId": "universal-inbox-record-5",
"cells": {
"subject": "SUP-000482 - Login fails after update - Regional operations review with a deliberately long title",
"channel": "Email - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"queue": "Technical support",
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"updated": "2026-09-03T16:31:00Z"
}
},
{
"id": "universal-inbox-row-6",
"recordId": "universal-inbox-record-6",
"cells": {
"subject": "SUP-000482 - Login fails after update - Completed preview",
"channel": "Email",
"queue": "Technical support",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"updated": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 238,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "universal-inbox-timeline",
"_type": "core.timeline",
"props": {
"title": "Inbox and ticket activity",
"hasMore": true,
"items": [
{
"id": "universal-inbox-event-1",
"recordId": "universal-inbox-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Ticket reply received",
"description": "The customer added diagnostic details to SUP-000482.",
"actor": "Maya Chen",
"tone": "primary"
},
{
"id": "universal-inbox-event-2",
"recordId": "universal-inbox-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Ticket reply received - Follow-up",
"description": "The customer added diagnostic details to SUP-000482. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "universal-inbox-event-3",
"recordId": "universal-inbox-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Ticket reply received - West region",
"description": "The customer added diagnostic details to SUP-000482. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "universal-inbox-event-4",
"recordId": "universal-inbox-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Ticket reply received - Customer response",
"description": "The customer added diagnostic details to SUP-000482. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "universal-inbox-event-5",
"recordId": "universal-inbox-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Ticket reply received - Regional operations review with a deliberately long title",
"description": "The customer added diagnostic details to SUP-000482. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "universal-inbox-event-6",
"recordId": "universal-inbox-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Ticket reply received - Completed preview",
"description": "The customer added diagnostic details to SUP-000482. 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 UniversalInboxPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface UniversalInboxPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: UniversalInboxPresentationData | 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 UniversalInboxPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: UniversalInboxPresentationProps) {
if (error) return
The Universal Inbox presentation could not be loaded.
;
if (loading || !data) return
Loading Universal Inbox presentation...
;
return (
Universal Inbox
Inbox operations
Inbound attention, support tickets, queues, SLA state, ownership, and resolution activity.
);
}
```
### 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/inbox`, 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": "universal-inbox-live",
"_type": "core.universal-inbox",
"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
Create an explicit Unsorted or Triage queue and monitor it. A routing rule should improve ownership, but no outside message should disappear merely because it did not match a rule.
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/universal-inbox.html).
---
title: "Checklists & Signoffs"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/checklists-signoffs.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Checklists & Signoffs
Attaches reusable, versioned checklists to authorized records and lets each item require notes, documents, or photos before the completed run enters the existing Workflow signoff path.
## In brief
- **Runtime key:** module.checklists-signoffs
- **Experience:** Evidence-backed work acceptance
- **Security:** Server-enforced
Use Checklists & Signoffs when work must be performed step by step and accepted with reviewable evidence. A run can attach to a ticket, project, work order, or other universal record without copying that record or inventing another approval system.
[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.checklists-signoffs. 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
Attaches reusable, versioned checklists to authorized records and lets each item require notes, documents, or photos before the completed run enters the existing Workflow signoff path.
## Key capabilities
- Create reusable versioned templates with ordered required or optional items.
- Attach a template snapshot to any authorized universal record.
- Track up to 1,000 ordered run items with completion actor, time, note, and revision.
- Require one or more native File documents or photos as proof for selected items.
- Calculate bounded progress and refuse submission while required work or evidence is missing.
- Submit exactly one current run to the existing Workflow and Universal Inbox approval path.
- Support rejection, editing, proof replacement, resubmission, approval, and replayable history.
- Reject stale revisions and make duplicate completion, submission, and decision requests idempotent.
## Common uses
- Work-order inspection and closeout proof.
- Project stage-gate and quality checklists.
- Support-ticket diagnostic or escalation checkoffs.
- Safety, compliance, onboarding, handoff, and document-review signoffs.
## How it connects
Runs attach through universal Record relationships. Evidence remains a native File, decisions remain Workflow approval records, reviewer attention remains in Universal Inbox, and activity/audit records preserve the end-to-end chain.
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
Every list, detail, item update, evidence link/download, submission, and decision derives tenant, user, DataRole, Location, and source-record scope from server identity. Evidence links do not grant File access, and Workflow permissions—not checklist UI state—control signoff authority.
- 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. Add `module.checklists-signoffs` to the source-record pages and menus where users perform or review work.
2. Create a versioned template with ordered items, required/optional state, proof type, and responsible role.
3. Grant source-record, checklist action, Workflow approval, File, DataRole, and Location permissions independently.
4. Attach a run to a representative ticket, project, work order, or custom record and complete it with required notes/documents/photos.
5. Test cross-tenant and cross-location denial, stale and duplicate requests, unauthorized file access, reject/edit/replace/resubmit/approve, failure recovery, and a 1,000-item boundary run before publishing.
## Copy/paste the checklist page
The normal choice is the native block. It already provides template creation, record-attached runs, item updates, document/photo proof, submission, and signoff status. Paste this page document into Raw JSON mode:
```
{
"blocks": [
{
"_id": "work-checklists-signoffs",
"_type": "module.checklists-signoffs",
"props": {},
"children": []
}
]
}
```
With empty props, the page loads a bounded first page of checklist runs visible to the signed-in user. On a contextual record page, set `targetRecordId` through the page editor's validated binding to pre-filter and prefill that record. The value remains a query target; the server independently authorizes it.
### Custom unstyled React feed
For custom presentation inside `apps/tenant-runtime/src`, retain the official authenticated API helper and SDK parser and replace only the markup. This read view does not infer edit or approval authority in React:
```
import { useEffect, useState } from "react";
import type {
ChecklistRunDetail,
ChecklistRunList,
ChecklistRunStatus,
} from "@buildwithhq/module-sdk";
import { getChecklistRun, listChecklistRuns } from "./api";
type ChecklistFeedProps = {
readonly targetRecordId?: string;
};
const statuses: readonly (ChecklistRunStatus | "")[] = [
"", "Draft", "InProgress", "ReadyForSignoff",
"Approved", "Rejected", "Cancelled",
];
export function CustomChecklistFeed({ targetRecordId = "" }: ChecklistFeedProps) {
const [status, setStatus] = useState("");
const [feed, setFeed] = useState(null);
const [selected, setSelected] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const request = new AbortController();
setSelected(null);
setError(null);
listChecklistRuns(targetRecordId, status, request.signal)
.then(setFeed)
.catch((caught: unknown) => {
if (!(caught instanceof DOMException && caught.name === "AbortError")) {
setError(caught instanceof Error ? caught.message : "Checklists could not be loaded.");
}
});
return () => request.abort();
}, [status, targetRecordId]);
const open = async (checklistRunId: string) => {
setError(null);
try {
setSelected(await getChecklistRun(checklistRunId));
} catch (caught) {
setError(caught instanceof Error ? caught.message : "The checklist could not be loaded.");
}
};
return (
First-class module
Checklists & Signoffs
{error &&
{error}
}
{!feed && !error &&
Loading checklists...
}
{feed &&
Showing {feed.items.length} of {feed.totalRecords} visible checklist runs
))}
)}
);
}
```
### Copy/paste write lifecycle
These helpers show the correct order for a proof-backed item: create a versioned template, attach a run to an authorized Record, upload bytes through Files, attach that File as proof, update with the newest item revision, then reload and submit with one stable request ID.
```
import type { ChecklistRunItem } from "@buildwithhq/module-sdk";
import {
attachChecklistProof,
createChecklistTemplate,
getChecklistRun,
startChecklistRun,
submitChecklistForSignoff,
updateChecklistItem,
uploadFile,
} from "./api";
export const createCloseoutTemplate = () => createChecklistTemplate({
name: "Work-order closeout",
description: "Completion requires a site photo.",
items: [{
itemKey: "completion-photo",
title: "Photograph the completed work",
instructions: "Show the finished work and surrounding area.",
responseType: "Checkbox",
isRequired: true,
evidenceRequirement: "Photo",
minimumEvidenceCount: 1,
sortOrder: 0,
}],
});
export const startCloseout = (targetRecordId: string, checklistTemplateId: string) =>
startChecklistRun({ targetRecordId, checklistTemplateId, title: "Closeout" });
export async function completeItemWithPhoto(
item: ChecklistRunItem,
photo: File,
caption: string,
locationId?: string,
) {
const uploaded = await uploadFile(photo, { description: caption, locationId });
if (!uploaded.recordId) throw new Error("The Files API did not return a RecordId.");
const attached = await attachChecklistProof(item.checklistRunItemId, {
fileRecordId: uploaded.recordId,
proofKind: "Photo",
caption,
});
return updateChecklistItem(item.checklistRunItemId, {
expectedRevision: attached.itemRevision ?? item.revision,
status: "Completed",
responseText: "Completed with attached photo evidence.",
});
}
export async function requestSignoff(checklistRunId: string, requestId: string) {
const current = await getChecklistRun(checklistRunId);
return submitChecklistForSignoff(checklistRunId, {
expectedRevision: current.revision,
requestId,
});
}
```
Create the `requestId` once with `crypto.randomUUID()` and keep that same value when retrying one logical submission. Generate a new ID only for a genuinely new submit attempt after the run has been edited. The current SDK list helper deliberately retrieves at most the first 100 authorized runs; use the module's bounded server pagination contract when building a larger administrative catalog. Approval and rejection happen through the existing Universal Inbox Workflow approval contract, not by writing checklist status directly.
### Optional starter styling
Import this beside the custom component or replace it entirely. Presentation does not change the authenticated data binding or server authorization:
```
.custom-checklists { width: 100%; max-width: none; }
.custom-checklists > header {
align-items: end;
display: flex;
gap: 1rem;
justify-content: space-between;
}
.custom-checklists__runs {
display: grid;
gap: .75rem;
list-style: none;
margin: 1rem 0;
padding: 0;
}
.custom-checklists__runs > li {
background: var(--surface, #fff);
border: 1px solid var(--line, #d8dee8);
border-radius: .75rem;
}
.custom-checklists__runs button {
align-items: center;
background: transparent;
border: 0;
color: inherit;
display: flex;
gap: 1rem;
justify-content: space-between;
padding: 1rem;
text-align: left;
width: 100%;
}
.custom-checklists__runs span { display: grid; gap: .25rem; }
.custom-checklists__detail {
background: var(--surface-muted, #f7f9fc);
border-radius: .75rem;
padding: 1rem;
}
@media (max-width: 640px) {
.custom-checklists > header,
.custom-checklists__runs button { align-items: stretch; flex-direction: column; }
}
```
### Exact lifecycle and data path
| Purpose | Method and route | Important contract |
| --- | --- | --- |
| List templates | `GET /api/modules/checklists-signoffs/templates` | Active templates visible in the current tenant |
| Create template | `POST /api/modules/checklists-signoffs/templates` | 1–1,000 unique ordered items |
| List runs | `GET /api/modules/checklists-signoffs/runs` | Filter by target/status; maximum 200 rows per page |
| Open run | `GET /api/modules/checklists-signoffs/runs/{checklistRunId}` | Authorized snapshot, items, revisions, and visible proof |
| Attach run | `POST /api/modules/checklists-signoffs/runs` | Target Record must be inside the current create/edit envelope |
| Update item | `PUT /api/modules/checklists-signoffs/items/{checklistRunItemId}` | `expectedRevision` rejects stale edits |
| Upload proof bytes | `POST /api/modules/files/upload` | Files validates size, signature, MIME and storage policy |
| Attach proof | `POST /api/modules/checklists-signoffs/items/{checklistRunItemId}/proof` | File and checklist must share an authorized scope |
| Remove proof | `DELETE /api/modules/checklists-signoffs/proof/{checklistItemEvidenceId}` | Allowed only while the run is editable |
| Request signoff | `POST /api/modules/checklists-signoffs/runs/{checklistRunId}/submit` | Revision-fenced and idempotent by `requestId` |
| Approve/reject | `POST /api/inbox/items/{inboxItemId}/workflow-approval` | Current Inbox activity and Workflow permission are rechecked |
The API derives SaaS, organization, user, DataRoles and locations from verified server identity. Completing an item does not bypass its proof requirement; attaching proof does not grant permission to download that File; and submitting a run does not grant approval authority. The database returns the bounded authorized checklist projection that the component renders.
See the [complete React component catalog](../builder-guide/react-component-catalog.html) for generated styled and unstyled block exports, or the [headless application guide](../builder-guide/headless-application-kit.html) when the React application lives outside the BuildWithHQ tenant shell.
## Build a professional Checklists & Signoffs dashboard
These six registry-backed presentation blocks let a designer turn the secured Checklists & Signoffs 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/checklists-signoffs`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/checklists-signoffs-sample.json`, its executable authenticated page at `pages/first-class/checklists-signoffs-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 Checklists & Signoffs projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Visible runs, completion, signoff queue, and exceptions |
| `entity-list` | `core.entity-list` | Recent authorized runs and attached business records |
| `progress-list` | `core.progress-list` | Run distribution or item completion |
| `series-chart` | `core.series-chart` | Started, completed, submitted, and approved over time |
| `data-grid` | `core.presentation-grid` | One bounded page of runs with progress and status |
| `timeline` | `core.timeline` | Completion, evidence, submission, rejection, and approval 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": "checklists-signoffs-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Checklist operations",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "visible",
"label": "Visible runs",
"value": 84,
"format": "number",
"tone": "neutral"
},
{
"key": "awaiting",
"label": "Awaiting signoff",
"value": 9,
"format": "number",
"tone": "warning"
},
{
"key": "complete",
"label": "Items complete",
"value": 72,
"format": "percent",
"tone": "success"
},
{
"key": "attention",
"label": "Need attention",
"value": 4,
"format": "number",
"tone": "danger"
}
]
},
"children": []
},
{
"_id": "checklists-signoffs-recent",
"_type": "core.entity-list",
"props": {
"title": "Recent checklist runs",
"hasMore": true,
"items": [
{
"id": "checklists-signoffs-sample-1",
"recordId": "checklists-signoffs-record-1",
"primary": "North Wing closeout",
"secondary": "WO-1048 - 18 of 20 items",
"status": {
"key": "ready-for-signoff",
"label": "Ready for signoff",
"tone": "warning"
},
"trailing": "2 proofs"
},
{
"id": "checklists-signoffs-sample-2",
"recordId": "checklists-signoffs-record-2",
"primary": "North Wing closeout - Follow-up",
"secondary": "WO-1048 - 18 of 20 items - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "checklists-signoffs-sample-3",
"recordId": "checklists-signoffs-record-3",
"primary": "North Wing closeout - West region",
"secondary": "WO-1048 - 18 of 20 items - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "checklists-signoffs-sample-4",
"recordId": "checklists-signoffs-record-4",
"primary": "North Wing closeout - Customer response",
"secondary": "WO-1048 - 18 of 20 items - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "checklists-signoffs-sample-5",
"recordId": "checklists-signoffs-record-5",
"primary": "North Wing closeout - Regional operations review with a deliberately long title",
"secondary": "WO-1048 - 18 of 20 items - 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": "checklists-signoffs-sample-6",
"recordId": "checklists-signoffs-record-6",
"primary": "North Wing closeout - Completed preview",
"secondary": "WO-1048 - 18 of 20 items - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "checklists-signoffs-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Runs by status",
"items": [
{
"key": "draft",
"label": "Draft",
"value": 18,
"maximum": 84,
"displayValue": "18",
"tone": "neutral",
"status": {
"key": "draft",
"label": "Draft",
"tone": "neutral"
}
},
{
"key": "active",
"label": "In progress",
"value": 41,
"maximum": 84,
"displayValue": "41",
"tone": "primary",
"status": {
"key": "active",
"label": "In progress",
"tone": "primary"
}
},
{
"key": "approved",
"label": "Approved",
"value": 25,
"maximum": 84,
"displayValue": "25",
"tone": "success",
"status": {
"key": "approved",
"label": "Approved",
"tone": "success"
}
}
]
},
"children": []
},
{
"_id": "checklists-signoffs-trend",
"_type": "core.series-chart",
"props": {
"title": "Checklist throughput",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Runs started",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Runs approved",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "checklists-signoffs-table",
"_type": "core.presentation-grid",
"props": {
"title": "Visible checklist runs",
"columns": [
{
"key": "checklist",
"label": "Checklist",
"type": "text",
"align": "left"
},
{
"key": "target",
"label": "Attached record",
"type": "text",
"align": "left"
},
{
"key": "progress",
"label": "Progress",
"type": "number",
"align": "right"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "updated",
"label": "Updated",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "checklists-signoffs-row-1",
"recordId": "checklists-signoffs-record-1",
"cells": {
"checklist": "North Wing closeout",
"target": "WO-1048",
"progress": 90,
"status": {
"key": "ready",
"label": "Ready for signoff",
"tone": "warning"
},
"updated": "2026-09-04T17:45:00Z"
}
},
{
"id": "checklists-signoffs-row-2",
"recordId": "checklists-signoffs-record-2",
"cells": {
"checklist": "North Wing closeout - Follow-up",
"target": "WO-1048",
"progress": 91,
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"updated": "2026-09-04T15:42:00Z"
}
},
{
"id": "checklists-signoffs-row-3",
"recordId": "checklists-signoffs-record-3",
"cells": {
"checklist": "North Wing closeout - West region",
"target": "WO-1048",
"progress": 92,
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"updated": "2026-09-04T12:18:00Z"
}
},
{
"id": "checklists-signoffs-row-4",
"recordId": "checklists-signoffs-record-4",
"cells": {
"checklist": "North Wing closeout - Customer response",
"target": "WO-1048",
"progress": 93,
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"updated": "2026-09-03T21:07:00Z"
}
},
{
"id": "checklists-signoffs-row-5",
"recordId": "checklists-signoffs-record-5",
"cells": {
"checklist": "North Wing closeout - Regional operations review with a deliberately long title",
"target": "WO-1048 - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"progress": 94,
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"updated": "2026-09-03T16:31:00Z"
}
},
{
"id": "checklists-signoffs-row-6",
"recordId": "checklists-signoffs-record-6",
"cells": {
"checklist": "North Wing closeout - Completed preview",
"target": "WO-1048",
"progress": 95,
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"updated": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 84,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "checklists-signoffs-timeline",
"_type": "core.timeline",
"props": {
"title": "Checklist activity",
"hasMore": true,
"items": [
{
"id": "checklists-signoffs-event-1",
"recordId": "checklists-signoffs-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Evidence attached",
"description": "A closeout photo was attached to item 18.",
"actor": "Jordan Lee",
"tone": "success"
},
{
"id": "checklists-signoffs-event-2",
"recordId": "checklists-signoffs-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Evidence attached - Follow-up",
"description": "A closeout photo was attached to item 18. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "checklists-signoffs-event-3",
"recordId": "checklists-signoffs-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Evidence attached - West region",
"description": "A closeout photo was attached to item 18. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "checklists-signoffs-event-4",
"recordId": "checklists-signoffs-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Evidence attached - Customer response",
"description": "A closeout photo was attached to item 18. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "checklists-signoffs-event-5",
"recordId": "checklists-signoffs-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Evidence attached - Regional operations review with a deliberately long title",
"description": "A closeout photo was attached to item 18. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "checklists-signoffs-event-6",
"recordId": "checklists-signoffs-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Evidence attached - Completed preview",
"description": "A closeout photo was attached to item 18. 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 ChecklistsSignoffsPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface ChecklistsSignoffsPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: ChecklistsSignoffsPresentationData | 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 ChecklistsSignoffsPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: ChecklistsSignoffsPresentationProps) {
if (error) return
The Checklists & Signoffs presentation could not be loaded.
;
if (loading || !data) return
Loading Checklists & Signoffs presentation...
;
return (
Checklists & Signoffs
Checklist operations
Completion, evidence, signoff readiness, visible runs, and recent decisions.
);
}
```
### 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/checklists-signoffs`, 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": "checklists-signoffs-live",
"_type": "module.checklists-signoffs",
"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
Use concise items with explicit proof rules and keep the normal checklist below the hard 1,000-item boundary. Split genuinely different phases into related runs instead of making one checklist behave like an unbounded database table.
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/checklists-signoffs.html).
---
title: "GoClaw"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/goclaw.html"
reviewed_at: "2026-09-14"
authority: Official
---
# GoClaw
Adds a dedicated virtual assistant to a user account that researches, summarizes, classifies, drafts, and proposes work without ever receiving more access or action authority than that user.
## In brief
- **Runtime key:** module.goclaw
- **Experience:** Permission-bound AI assistance
- **Security:** Server-enforced
Use GoClaw as the virtual assistant attached to each enabled user account. It helps that person work their permitted Inbox items and business records, but it is not a privileged service account and cannot step outside the user's existing authorization boundary.
[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.goclaw. 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
Adds a dedicated virtual assistant to a user account that researches, summarizes, classifies, drafts, and proposes work without ever receiving more access or action authority than that user.
## Key capabilities
- Read only records, fields, locations, and Inbox context the represented user may read—and only when AI access is also allowed.
- Research, classify, summarize, draft replies, and prepare structured next actions.
- Create suggestion batches whose evidence, risk, confidence, required permission, and inputs can be reviewed.
- Route material or customer-facing actions through configured policy or human approval before deterministic execution.
- Respect action budgets, leases, retries, stale-context fences, and idempotent execution.
- Record attribution, approval/refusal, tool/action evidence, results, failures, ratings, and correlation history.
**Surface:** Per-user account assistant. GoClaw is attached to the represented user and remains inside that user's tenant, DataRole, location, record, field, action, and AI-read boundaries.
## Common uses
- Triage and response preparation for permitted Universal Inbox work.
- Account, case, project, support, or operations research and follow-up suggestions.
- A personal daily assistant that reduces preparation work while the user retains decision authority.
## How it connects
GoClaw claims authorized Inbox work, prepares `AiActionSuggestionBatches` and `AiActionSuggestions`, and—after current permission/policy/approval checks—creates `AiActionExecutions` for deterministic executors. It uses native records and the secured AI retrieval plane rather than maintaining a shadow copy of the user's data.
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
GoClaw operates on behalf of one user. Its effective access is the intersection of that user's tenant, DataRole, record, field, location, and action permissions with AI-specific `CanAiReadRecords`, secure-field exclusions, tool policy, quotas, approval policy, and the current source/Inbox activity version. The result can be narrower than the user's normal access, never wider.
- 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. Enable the assistant for the intended user account and bind it to that user—do not create a wider shared service identity.
2. Select the Inbox queues/channels and record contexts it may prepare, while preserving all normal user and AI-read checks.
3. Configure reviewed action templates, required permissions, authority/approval policy, quotas, and exact behavior/harness versions.
4. Begin with research and drafts; add deterministic actions one at a time with safe error, retry, and idempotency behavior.
5. Test restricted records, secure fields, another location, stale Inbox activity, refused approval, quota exhaustion, and duplicate execution before widening use.
## Copy/paste the native review surface
GoClaw remains an account-level tenant capability, and the same secured approval queue used by the top bar is now also registered as the real `module.goclaw` page block. Use the block in a native Builder page, or use the same reviewed React client integration in a custom/headless shell:
```
import {
GoClawApprovalQueue,
createApprovalQueueHttpClient,
} from "@buildwithhq/universal-inbox";
import { getTenantAccessToken, tenantSessionFetch } from "./api";
import { ensureCsrfToken } from "./webSecurity";
const goClawReviewClient = createApprovalQueueHttpClient({
accessToken: getTenantAccessToken,
csrfToken: ensureCsrfToken,
fetchImplementation: tenantSessionFetch,
});
type GoClawReviewSurfaceProps = {
readonly initialInboxItemId?: string;
readonly onClose?: () => void;
readonly onOpenInbox?: (inboxItemId: string) => void;
};
export function GoClawReviewSurface(props: GoClawReviewSurfaceProps) {
return ;
}
```
Place this at the account shell level, such as an **AI review** top-bar action or a protected route. The package includes its production component CSS and provides the complete review, edit, approve, reject, take-over, retry, stale-context, and source-Inbox behavior.
### Custom unstyled approval queue
If you own the presentation, keep the official `ApprovalQueueClient` and render only the secured rows it returns. This bounded starter loads at most 100 review batches and makes stale lineage visible:
```
import { useEffect, useState } from "react";
import type {
ApprovalQueueClient,
ApprovalQueueItem,
} from "@buildwithhq/universal-inbox";
type UnstyledGoClawQueueProps = {
readonly client: ApprovalQueueClient;
readonly onSelect: (item: ApprovalQueueItem) => void;
};
const title = (item: ApprovalQueueItem) =>
item.subject?.trim() || item.summary?.trim() || item.sourceType;
export function UnstyledGoClawQueue({
client,
onSelect,
}: UnstyledGoClawQueueProps) {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const request = new AbortController();
setLoading(true);
setError(null);
client.list(100, request.signal)
.then(setItems)
.catch((failure: unknown) => {
if (!(failure instanceof DOMException && failure.name === "AbortError")) {
setItems([]);
setError(failure instanceof Error ? failure.message : "Approval work is unavailable.");
}
})
.finally(() => {
if (!request.signal.aborted) setLoading(false);
});
return () => request.abort();
}, [client]);
return (
GoClaw
Approval work
{loading &&
Loading approval work...
}
{error &&
{error}
}
{!loading && !error && items.length === 0 &&
No secured approval work is available.
}
{items.map((item) => (
))}
);
}
```
### Review and disposition helpers
A custom detail view can edit only the reviewed draft/action inputs and then call the typed client. It never supplies approver, tenant, role, location, or database identity:
```
import type {
ApprovalQueueClient,
ApprovalQueueItem,
} from "@buildwithhq/universal-inbox";
type ReviewedActionEdit = {
readonly aiActionSuggestionId: string;
readonly suggestedInput: Record;
};
type ReviewedApproval = {
readonly reviewNotes?: string;
readonly editedDraftResponse?: Record;
readonly actionEdits?: readonly ReviewedActionEdit[];
};
export async function approveReviewedWork(
client: ApprovalQueueClient,
item: ApprovalQueueItem,
reviewed: ReviewedApproval = {},
) {
if (item.isStale || item.activityVersion !== item.basedOnActivityVersion) {
throw new Error("Inbox activity changed. Re-run GoClaw before approving.");
}
return client.approve(item.suggestionBatchId, reviewed);
}
export const rejectPreparedWork = (
client: ApprovalQueueClient,
item: ApprovalQueueItem,
notes?: string,
) => client.reject(item.suggestionBatchId, notes);
export const takeOverPreparedWork = (
client: ApprovalQueueClient,
item: ApprovalQueueItem,
notes?: string,
) => client.takeOver(item.suggestionBatchId, notes);
export const requestFreshPreparation = (
client: ApprovalQueueClient,
item: ApprovalQueueItem,
notes?: string,
) => client.retry(item.suggestionBatchId, notes);
```
The browser check is helpful feedback, but the server is authoritative: it rechecks current source access, `CanAiReadRecords`, `inbox.approve`, every proposed action's permission, reviewed action schemas, and `ActivityVersion` inside the transaction. After an ambiguous network failure, reload the queue before repeating a decision.
### Optional starter styling
Use this only with the custom queue. The native component already imports its own production styles:
```
.custom-goclaw { width: 100%; max-width: none; }
.custom-goclaw > header { margin-bottom: 1rem; }
.custom-goclaw > header > * { margin-block: .25rem; }
.custom-goclaw ol {
display: grid;
gap: .75rem;
list-style: none;
margin: 0;
padding: 0;
}
.custom-goclaw li {
border: 1px solid var(--line, #d8dee8);
border-radius: .75rem;
}
.custom-goclaw li[data-stale="true"] {
border-color: var(--warning, #b86b00);
}
.custom-goclaw button {
background: var(--surface, #fff);
border: 0;
border-radius: inherit;
color: inherit;
display: grid;
gap: .35rem;
padding: 1rem;
text-align: left;
width: 100%;
}
.custom-goclaw button:hover,
.custom-goclaw button:focus-visible {
background: var(--surface-muted, #f7f9fc);
}
```
### Exact browser and worker path
| Purpose | Method and route | Important contract |
| --- | --- | --- |
| List review work | `GET /api/goclaw/approvals?top=100` | Returns at most 500 currently authorized batches; the native UI requests 100 |
| Approve reviewed work | `POST /api/goclaw/approvals/{suggestionBatchId}/approve` | Optional review notes, draft edit, and at most 20 reviewed action-input edits |
| Reject proposal | `POST /api/goclaw/approvals/{suggestionBatchId}/reject` | Dismisses pending suggestions and records the human decision |
| Human take-over | `POST /api/goclaw/approvals/{suggestionBatchId}/take-over` | Returns ownership to the current authorized reviewer |
| Prepare again | `POST /api/goclaw/approvals/{suggestionBatchId}/retry` | Invalidates the old batch and queues a fresh fenced planning pass |
| Open source context | `GET /api/inbox/items/{inboxItemId}` | Loads the secured native source projection through Universal Inbox |
| Set authority policy | `PUT /api/goclaw/agents/{goClawAgentId}/authority` | Human account owner with `inbox.approve`; ResearchOnly, ApprovalRequired, or PolicyAuthorizedLowRisk |
The browser never creates arbitrary actions or writes directly to GoClaw tables. Inbound work first becomes a Universal Inbox item. A fenced worker claim loads authorized context, uses the centrally routed model capability, and persists canonical `AiActionSuggestionBatches` plus `AiActionSuggestions`. Approval creates `AiActionExecutions`; deterministic workers lease `GoClawActionQueue` work, recheck lineage and permission, and record the result. Outbound communication still flows through the communications worker.
The current deterministic action templates are `SendConversationReply`, `CreateFollowup`, `LinkRecords`, `PostMessage`, and `UpdateRecordStatus`. Add a new action only with a reviewed input schema, required permission, deterministic executor, idempotency/recovery behavior, and tests.
GoClaw work and its audit lineage live in the routed SaaS database; secured retrieval uses that SaaS's vector database, while the one central Platform Orchestration database resolves model endpoints and aggregates health. The client selects none of those databases.
See [GoClaw in depth](../builder-guide/goclaw-in-depth.html) for the authority model, the [React component catalog](../builder-guide/react-component-catalog.html) for the generated native and headless starters, or the [headless application guide](../builder-guide/headless-application-kit.html) when the React application lives outside the BuildWithHQ tenant shell.
## Build a professional GoClaw dashboard
These six registry-backed presentation blocks let a designer turn the secured GoClaw 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/goclaw/approvals`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/goclaw-sample.json`, its executable authenticated page at `pages/first-class/goclaw-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 GoClaw projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Review backlog, stale work, decisions, and execution success |
| `entity-list` | `core.entity-list` | Current suggestion batches with source and risk |
| `progress-list` | `core.progress-list` | Distribution by risk, state, or action outcome |
| `series-chart` | `core.series-chart` | Prepared, approved, rejected, and executed work over time |
| `data-grid` | `core.presentation-grid` | One bounded page of reviewable batches |
| `timeline` | `core.timeline` | Preparation, human disposition, retry, execution, and failure evidence |
### 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": "goclaw-metrics",
"_type": "core.metric-strip",
"props": {
"title": "GoClaw review center",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "review",
"label": "Awaiting review",
"value": 17,
"format": "number",
"tone": "warning"
},
{
"key": "stale",
"label": "Stale batches",
"value": 2,
"format": "number",
"tone": "danger"
},
{
"key": "approved",
"label": "Approved today",
"value": 26,
"format": "number",
"tone": "success"
},
{
"key": "success",
"label": "Execution success",
"value": 97.4,
"format": "percent",
"tone": "primary"
}
]
},
"children": []
},
{
"_id": "goclaw-recent",
"_type": "core.entity-list",
"props": {
"title": "Prepared work",
"hasMore": true,
"items": [
{
"id": "goclaw-sample-1",
"recordId": "goclaw-record-1",
"primary": "Draft reply for SUP-000482",
"secondary": "Universal Inbox - low risk",
"status": {
"key": "approval-required",
"label": "Approval required",
"tone": "warning"
},
"trailing": "3 actions"
},
{
"id": "goclaw-sample-2",
"recordId": "goclaw-record-2",
"primary": "Draft reply for SUP-000482 - Follow-up",
"secondary": "Universal Inbox - low risk - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "goclaw-sample-3",
"recordId": "goclaw-record-3",
"primary": "Draft reply for SUP-000482 - West region",
"secondary": "Universal Inbox - low risk - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "goclaw-sample-4",
"recordId": "goclaw-record-4",
"primary": "Draft reply for SUP-000482 - Customer response",
"secondary": "Universal Inbox - low risk - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "goclaw-sample-5",
"recordId": "goclaw-record-5",
"primary": "Draft reply for SUP-000482 - Regional operations review with a deliberately long title",
"secondary": "Universal Inbox - low risk - 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": "goclaw-sample-6",
"recordId": "goclaw-record-6",
"primary": "Draft reply for SUP-000482 - Completed preview",
"secondary": "Universal Inbox - low risk - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "goclaw-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Review queue by risk",
"items": [
{
"key": "low",
"label": "Low risk",
"value": 11,
"maximum": 17,
"displayValue": "11",
"tone": "success",
"status": {
"key": "low",
"label": "Low risk",
"tone": "success"
}
},
{
"key": "medium",
"label": "Medium risk",
"value": 5,
"maximum": 17,
"displayValue": "5",
"tone": "warning",
"status": {
"key": "medium",
"label": "Medium risk",
"tone": "warning"
}
},
{
"key": "high",
"label": "High risk",
"value": 1,
"maximum": 17,
"displayValue": "1",
"tone": "danger",
"status": {
"key": "high",
"label": "High risk",
"tone": "danger"
}
}
]
},
"children": []
},
{
"_id": "goclaw-trend",
"_type": "core.series-chart",
"props": {
"title": "Preparation and execution",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Prepared",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Executed",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "goclaw-table",
"_type": "core.presentation-grid",
"props": {
"title": "Authorized suggestion batches",
"columns": [
{
"key": "work",
"label": "Prepared work",
"type": "text",
"align": "left"
},
{
"key": "source",
"label": "Source",
"type": "text",
"align": "left"
},
{
"key": "risk",
"label": "Risk",
"type": "status",
"align": "left"
},
{
"key": "status",
"label": "State",
"type": "status",
"align": "left"
},
{
"key": "updated",
"label": "Updated",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "goclaw-row-1",
"recordId": "goclaw-record-1",
"cells": {
"work": "Draft reply for SUP-000482",
"source": "Universal Inbox",
"risk": {
"key": "low",
"label": "Low",
"tone": "success"
},
"status": {
"key": "pending",
"label": "Awaiting review",
"tone": "warning"
},
"updated": "2026-09-04T17:42:00Z"
}
},
{
"id": "goclaw-row-2",
"recordId": "goclaw-record-2",
"cells": {
"work": "Draft reply for SUP-000482 - Follow-up",
"source": "Universal Inbox",
"risk": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"updated": "2026-09-04T15:42:00Z"
}
},
{
"id": "goclaw-row-3",
"recordId": "goclaw-record-3",
"cells": {
"work": "Draft reply for SUP-000482 - West region",
"source": "Universal Inbox",
"risk": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"updated": "2026-09-04T12:18:00Z"
}
},
{
"id": "goclaw-row-4",
"recordId": "goclaw-record-4",
"cells": {
"work": "Draft reply for SUP-000482 - Customer response",
"source": "Universal Inbox",
"risk": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"updated": "2026-09-03T21:07:00Z"
}
},
{
"id": "goclaw-row-5",
"recordId": "goclaw-record-5",
"cells": {
"work": "Draft reply for SUP-000482 - Regional operations review with a deliberately long title",
"source": "Universal Inbox - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"risk": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"updated": "2026-09-03T16:31:00Z"
}
},
{
"id": "goclaw-row-6",
"recordId": "goclaw-record-6",
"cells": {
"work": "Draft reply for SUP-000482 - Completed preview",
"source": "Universal Inbox",
"risk": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"updated": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 17,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "goclaw-timeline",
"_type": "core.timeline",
"props": {
"title": "GoClaw decisions and outcomes",
"hasMore": true,
"items": [
{
"id": "goclaw-event-1",
"recordId": "goclaw-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Suggestion batch prepared",
"description": "Evidence and three proposed actions are ready for review.",
"actor": "GoClaw",
"tone": "primary"
},
{
"id": "goclaw-event-2",
"recordId": "goclaw-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Suggestion batch prepared - Follow-up",
"description": "Evidence and three proposed actions are ready for review. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "goclaw-event-3",
"recordId": "goclaw-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Suggestion batch prepared - West region",
"description": "Evidence and three proposed actions are ready for review. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "goclaw-event-4",
"recordId": "goclaw-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Suggestion batch prepared - Customer response",
"description": "Evidence and three proposed actions are ready for review. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "goclaw-event-5",
"recordId": "goclaw-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Suggestion batch prepared - Regional operations review with a deliberately long title",
"description": "Evidence and three proposed actions are ready for review. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "goclaw-event-6",
"recordId": "goclaw-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Suggestion batch prepared - Completed preview",
"description": "Evidence and three proposed actions are ready for review. 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 GoClawPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface GoClawPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: GoClawPresentationData | 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 GoClawPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: GoClawPresentationProps) {
if (error) return
The GoClaw presentation could not be loaded.
;
if (loading || !data) return
Loading GoClaw presentation...
;
return (
GoClaw
GoClaw review center
Prepared work, risk, approval state, execution outcomes, and stale-context visibility.
);
}
```
### 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/goclaw/approvals`, 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": "goclaw-live",
"_type": "module.goclaw",
"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 each assistant with read-only preparation and one narrow approved action. Expand only after acceptance, edit, refusal, and outcome evidence show the behavior is trustworthy.
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/goclaw.html).
---
title: "AI Insights"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/ai-insights.html"
reviewed_at: "2026-09-14"
authority: Official
---
# AI Insights
Runs permission-aware AI analysis in the background each day to create prioritized, evidence-backed insights and governed next actions from meaningful changes in authorized business records.
## In brief
- **Runtime key:** module.ai-insights
- **Experience:** Proactive daily findings
- **Security:** Server-enforced
Use AI Insights when the system should look for important changes without waiting for a user to ask. The daily background cycle turns permitted record and knowledge context into a small, prioritized insight feed for the people entitled to see and act on it.
[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.ai-insights. 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
Runs permission-aware AI analysis in the background each day to create prioritized, evidence-backed insights and governed next actions from meaningful changes in authorized business records.
## Key capabilities
- Run a durable daily background cycle for configured insight types and source scopes.
- Steer each nightly run with the app's starter directives and the organization's own "look for" directives; the organization owner turns AI Insights on in Account admin > AI Insights.
- Read only AI-eligible records and evidence through tenant, DataRole, field, location, and `CanAiReadRecords` controls.
- Create structured insights with source record, type/category, title, summary, recommended action, priority, confidence, severity, evidence, model/profile provenance, and lifecycle dates.
- Use source version/context fingerprints and current-state checks to avoid repeatedly presenting stale or duplicate findings.
- Deliver each insight only to users who can currently access its source record; losing source access removes the insight from their secured feed.
- Let each user view, pin, snooze, hide, dismiss, or advance an insight without changing another user's personal feed state.
- Turn a recommended next step into a governed action that still requires the current user's action permission and configured policy or approval.
## Common uses
- Daily account risk, opportunity, renewal, follow-up, and customer-health findings.
- Operational exceptions, overdue work, recurring issues, quality changes, and material KPI movement.
- Proactive company-knowledge, case, service, sales, support, or asset insights with evidence links.
## How it connects
The background cycle writes canonical `AiInsights` linked to their source Records and stores supporting evidence separately. The secured Insights feed presents those findings with per-user state. Recommended work may create a governed insight action, workflow, Inbox item, or GoClaw suggestion, but the insight itself is a finding—not authorization to mutate business data.
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
Insight generation applies the AI-read envelope before model context is assembled. Insight visibility is inherited from the source record and rechecked at read time. Evidence, recommendations, and actions cannot reveal or operate on records, fields, locations, or tools outside the current user's permissions; material execution uses action-specific permission and policy/approval.
- 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. Choose one decision users should make from a daily finding and define its source modules, eligible states, evidence, freshness window, and materiality threshold.
2. Select the exact users/roles and AI-readable record scope; normal record access alone does not imply AI eligibility.
3. Configure the daily schedule, model/harness profile, priority/confidence/severity rules, expiry, deduplication/current-context policy, and safe failure behavior.
4. Define the user-facing insight lifecycle and any optional next action as a reviewed, permission-keyed contract with the required approval policy.
5. Test no-change days, changed source context, duplicates, expired findings, revoked source access, restricted fields/locations, model failure/retry, and proposed-action denial.
## Copy/paste the feed
The normal choice is the native block: it already includes the secured list, filters, bounded pagination, detail panel, evidence, and per-user view, pin, snooze, and hide state. Paste this page document into Raw JSON mode:
```
{
"blocks": [
{
"_id": "ai-insights-feed",
"_type": "module.ai-insights",
"props": {},
"children": []
}
]
}
```
The page shell supplies the page heading. The module expands to the available center column; page and shell layout remain yours.
### Custom unstyled React feed
For custom presentation inside `apps/tenant-runtime/src`, keep the official authenticated API helper and SDK contract parser, then replace only the markup. This starter deliberately does not make authorization decisions in React:
```
import { useEffect, useState } from "react";
import type { AiInsightList } from "@buildwithhq/module-sdk";
import { listAiInsights } from "./api";
export function CustomAiInsightsFeed() {
const [feed, setFeed] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const request = new AbortController();
listAiInsights({ pageNumber: 1, pageSize: 50 }, request.signal)
.then(setFeed)
.catch((caught: unknown) => {
if (!(caught instanceof DOMException && caught.name === "AbortError")) {
setError(caught instanceof Error ? caught.message : "AI Insights could not be loaded.");
}
});
return () => request.abort();
}, []);
if (error) return
{error}
;
if (!feed) return
Loading AI Insights...
;
if (!feed.items.length) return
No insights match this secured view.
;
return (
First-class module
AI Insights
{feed.unviewedCount} new / {feed.totalRecords} visible
))}
);
}
```
### Optional starter styling
Drop this next to the unstyled component, import it, and replace the design tokens with your own. Removing this CSS does not change data binding or security.
```
.custom-insights { width: 100%; max-width: none; }
.custom-insights > header {
align-items: end;
display: flex;
gap: 1rem;
justify-content: space-between;
}
.custom-insights ol {
display: grid;
gap: .75rem;
list-style: none;
margin: 1rem 0 0;
padding: 0;
}
.custom-insights li {
background: var(--surface, #fff);
border: 1px solid var(--line, #d8dee8);
border-radius: .75rem;
padding: 1rem;
}
.custom-insights li[data-unviewed="true"] {
border-left: .3rem solid var(--accent, #3157d5);
}
.custom-insights h2 { margin: .25rem 0 .5rem; }
@media (max-width: 640px) {
.custom-insights > header { align-items: start; flex-direction: column; }
}
```
### Exact data path
| Purpose | Method and route | Bound |
| --- | --- | --- |
| Feed | `GET /api/modules/ai-insights` | Maximum 200 rows per page |
| Insight and evidence | `GET /api/modules/ai-insights/{insightId}` | Maximum 25 display-safe evidence rows |
| Personal state | `PUT /api/modules/ai-insights/{insightId}/state` | View, pin, snooze, or hide for the signed-in user |
All three routes derive identity on the server and reapply tenant, SaaS app, active-user, DataRole, location, record-read, and `CanAiReadRecords` checks. The detail route omits raw evidence JSON and action payloads. A recommendation is display-only until a separate Workflow or GoClaw action contract authorizes it.
See the [complete React component catalog](../builder-guide/react-component-catalog.html) for the generated styled and unstyled block exports, or the [headless application guide](../builder-guide/headless-application-kit.html) when the React application lives outside the BuildWithHQ tenant shell.
## Build a professional AI Insights dashboard
These six registry-backed presentation blocks let a designer turn the secured AI Insights 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/ai-insights`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/ai-insights-sample.json`, its executable authenticated page at `pages/first-class/ai-insights-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 AI Insights projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Visible, unviewed, pinned, severe, and expiring findings |
| `entity-list` | `core.entity-list` | Highest-priority authorized findings and source records |
| `progress-list` | `core.progress-list` | Distribution by state, severity, or category |
| `series-chart` | `core.series-chart` | Generated, viewed, acted-on, and dismissed findings |
| `data-grid` | `core.presentation-grid` | One bounded page with confidence and source context |
| `timeline` | `core.timeline` | Generation, view, pin, snooze, action, dismissal, and expiry |
### 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": "ai-insights-metrics",
"_type": "core.metric-strip",
"props": {
"title": "AI insight center",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "visible",
"label": "Visible insights",
"value": 126,
"format": "number",
"tone": "neutral"
},
{
"key": "unviewed",
"label": "Unviewed",
"value": 18,
"format": "number",
"tone": "warning"
},
{
"key": "pinned",
"label": "Pinned",
"value": 7,
"format": "number",
"tone": "primary"
},
{
"key": "high",
"label": "High severity",
"value": 5,
"format": "number",
"tone": "danger"
}
]
},
"children": []
},
{
"_id": "ai-insights-recent",
"_type": "core.entity-list",
"props": {
"title": "Priority insights",
"hasMore": true,
"items": [
{
"id": "ai-insights-sample-1",
"recordId": "ai-insights-record-1",
"primary": "Renewal risk increased",
"secondary": "Northstar account - 91% confidence",
"status": {
"key": "high-severity",
"label": "High severity",
"tone": "danger"
},
"trailing": "91%"
},
{
"id": "ai-insights-sample-2",
"recordId": "ai-insights-record-2",
"primary": "Renewal risk increased - Follow-up",
"secondary": "Northstar account - 91% confidence - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "ai-insights-sample-3",
"recordId": "ai-insights-record-3",
"primary": "Renewal risk increased - West region",
"secondary": "Northstar account - 91% confidence - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "ai-insights-sample-4",
"recordId": "ai-insights-record-4",
"primary": "Renewal risk increased - Customer response",
"secondary": "Northstar account - 91% confidence - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "ai-insights-sample-5",
"recordId": "ai-insights-record-5",
"primary": "Renewal risk increased - Regional operations review with a deliberately long title",
"secondary": "Northstar account - 91% confidence - 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": "ai-insights-sample-6",
"recordId": "ai-insights-record-6",
"primary": "Renewal risk increased - Completed preview",
"secondary": "Northstar account - 91% confidence - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "ai-insights-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Insights by state",
"items": [
{
"key": "open",
"label": "Open",
"value": 62,
"maximum": 126,
"displayValue": "62",
"tone": "warning",
"status": {
"key": "open",
"label": "Open",
"tone": "warning"
}
},
{
"key": "progress",
"label": "In progress",
"value": 29,
"maximum": 126,
"displayValue": "29",
"tone": "primary",
"status": {
"key": "progress",
"label": "In progress",
"tone": "primary"
}
},
{
"key": "acted",
"label": "Acted on",
"value": 35,
"maximum": 126,
"displayValue": "35",
"tone": "success",
"status": {
"key": "acted",
"label": "Acted on",
"tone": "success"
}
}
]
},
"children": []
},
{
"_id": "ai-insights-trend",
"_type": "core.series-chart",
"props": {
"title": "Insight outcomes",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Generated",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Acted on",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "ai-insights-table",
"_type": "core.presentation-grid",
"props": {
"title": "Authorized insights",
"columns": [
{
"key": "insight",
"label": "Insight",
"type": "text",
"align": "left"
},
{
"key": "source",
"label": "Source",
"type": "text",
"align": "left"
},
{
"key": "confidence",
"label": "Confidence",
"type": "number",
"align": "right"
},
{
"key": "severity",
"label": "Severity",
"type": "status",
"align": "left"
},
{
"key": "created",
"label": "Created",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "ai-insights-row-1",
"recordId": "ai-insights-record-1",
"cells": {
"insight": "Renewal risk increased",
"source": "Northstar account",
"confidence": 91,
"severity": {
"key": "high",
"label": "High",
"tone": "danger"
},
"created": "2026-09-04T16:30:00Z"
}
},
{
"id": "ai-insights-row-2",
"recordId": "ai-insights-record-2",
"cells": {
"insight": "Renewal risk increased - Follow-up",
"source": "Northstar account",
"confidence": 92,
"severity": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"created": "2026-09-04T15:42:00Z"
}
},
{
"id": "ai-insights-row-3",
"recordId": "ai-insights-record-3",
"cells": {
"insight": "Renewal risk increased - West region",
"source": "Northstar account",
"confidence": 93,
"severity": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"created": "2026-09-04T12:18:00Z"
}
},
{
"id": "ai-insights-row-4",
"recordId": "ai-insights-record-4",
"cells": {
"insight": "Renewal risk increased - Customer response",
"source": "Northstar account",
"confidence": 94,
"severity": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"created": "2026-09-03T21:07:00Z"
}
},
{
"id": "ai-insights-row-5",
"recordId": "ai-insights-record-5",
"cells": {
"insight": "Renewal risk increased - Regional operations review with a deliberately long title",
"source": "Northstar account - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"confidence": 95,
"severity": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"created": "2026-09-03T16:31:00Z"
}
},
{
"id": "ai-insights-row-6",
"recordId": "ai-insights-record-6",
"cells": {
"insight": "Renewal risk increased - Completed preview",
"source": "Northstar account",
"confidence": 96,
"severity": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"created": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 126,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "ai-insights-timeline",
"_type": "core.timeline",
"props": {
"title": "Insight activity",
"hasMore": true,
"items": [
{
"id": "ai-insights-event-1",
"recordId": "ai-insights-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Insight generated",
"description": "A new evidence-backed renewal finding is available.",
"actor": "AI Insights",
"tone": "primary"
},
{
"id": "ai-insights-event-2",
"recordId": "ai-insights-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Insight generated - Follow-up",
"description": "A new evidence-backed renewal finding is available. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "ai-insights-event-3",
"recordId": "ai-insights-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Insight generated - West region",
"description": "A new evidence-backed renewal finding is available. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "ai-insights-event-4",
"recordId": "ai-insights-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Insight generated - Customer response",
"description": "A new evidence-backed renewal finding is available. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "ai-insights-event-5",
"recordId": "ai-insights-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Insight generated - Regional operations review with a deliberately long title",
"description": "A new evidence-backed renewal finding is available. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "ai-insights-event-6",
"recordId": "ai-insights-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Insight generated - Completed preview",
"description": "A new evidence-backed renewal finding is available. 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 AiInsightsPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface AiInsightsPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: AiInsightsPresentationData | 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 AiInsightsPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: AiInsightsPresentationProps) {
if (error) return
The AI Insights presentation could not be loaded.
;
if (loading || !data) return
Loading AI Insights presentation...
;
return (
AI Insights
AI insight center
Prioritized findings, severity, confidence, source context, and governed user actions.
);
}
```
### 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/ai-insights`, 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": "ai-insights-live",
"_type": "module.ai-insights",
"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
A daily run should produce a short list worth reading, not summarize every record. Start with one high-value insight type, measure dismissals and acted-on outcomes, and tune thresholds before adding more.
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/ai-insights.html).
---
title: "Contacts"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/contacts.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Contacts
Provides a secured contact directory with configurable contact types, search, create/detail experiences, custom fields, relationships, activity, and favorites.
## In brief
- **Runtime key:** module.contacts
- **Experience:** People & organizations
- **Security:** Server-enforced
Use Contacts as the shared people-and-organization foundation for CRM, customer service, projects, field work, portals, sales, and other relationship-driven applications.
[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.contacts. 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
Provides a secured contact directory with configurable contact types, search, create/detail experiences, custom fields, relationships, activity, and favorites.
## Key capabilities
- Search and browse only the contacts visible to the current user.
- Create contacts with name, company, email, phone, and configurable type fields.
- Show secured contact detail, custom fields, related records, activity, and favorite state.
- Reuse the same contact identity across other modules instead of creating duplicate people records.
## Common uses
- Customer and prospect directories.
- Members, vendors, partners, or project stakeholders.
- The person/company context behind conversations, events, work orders, files, and cases.
## How it connects
Contacts commonly anchor Conversations, Calendar attendees, Files, Work Orders, CRM records, and portal membership. Typed relationships preserve the role a contact plays without flattening every relationship into the contact row.
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
Contact lists and details apply SaaS, tenant, DataRole, location, record, and field rules. Create and edit controls appear only when secured responses grant the corresponding capability; hidden or encrypted dynamic fields are not 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.contacts` 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.
### Explicit scope for create helpers
The native create helpers below require the user's current choice of Personal or one eligible DataRole. In a tenant-runtime component, render `RecordCreateScopeSelect allowAll` from `./RecordCreateScopeSelect` and read its submitted form with `readRecordCreateScope`. Pass that choice to the helper; do not infer it from the page, menu, role count, or a default location. When active locations exist, explicitly choose an authorized location or All when permitted; Personal has no location. For a shared-only helper, validate the returned choice is shared and has a DataRole before calling it. The typed choice is a request, and SQL rechecks current authority. Child actions keep their reviewed parent scope. Ordinary edits preserve the loaded scope and revision; moving scope is a separate explicit action.
## Copy/paste the Contacts page
**Pagination:** In the standard Contacts directory, users can choose 10, 25, 50, 100, or 200 rows per page. Set `props.pageSize` to an integer from 1 through 200 in the page block JSON to choose its initial size (default 25); a custom initial size is also added to the selector. A size change restarts at page one and keeps the search text. The choice is local to the mounted page, not a saved user preference. Multi-type composite directories retain their existing combined-list behavior and do not show this selector.
API callers control `pageNumber` and `pageSize` (1–200), browsing up to 5,000 authorized matches for the current filters. Refine the search to find other records. Fast rows are the default (`includeTotal=false`); follow `hasMore` for navigation and do not present an inexact list total as the full directory size. The separate secured count endpoint returns the full authorized total, even above 5,000. See [progressive loading and exact counts](../api/progressive-loading-and-counts.html) for the executable SDK example, matching filters and cache boundaries. This browsing policy supersedes the earlier full-directory traversal behavior.
The native block is the fastest complete starting point. It supplies the secured directory, search, create, detail, custom-field display, relationship/activity counts, favorites, and normal server pagination. Paste this page document into Raw JSON mode:
```
{
"blocks": [
{
"_id": "contacts-directory",
"_type": "module.contacts",
"props": {
"title": "Customers and partners",
"singular": "contact",
"typeLabel": "Relationship",
"contactTypes": ["Customer", "Prospect", "Partner"]
},
"children": []
}
]
}
```
`title`, `singular`, and `typeLabel` change presentation only. `contactTypes` accepts at most eight values and is a bounded convenience for small segmented directories: the native renderer requests the first 100 authorized rows for each configured type, merges duplicates, and sorts the result. For a large directory, omit `contactTypes` and use the native server-paginated list.
### Custom unstyled React directory
For custom presentation inside `apps/tenant-runtime/src`, keep the official authenticated API helpers and SDK parsers and replace only the markup. This example searches and pages on the server; it never downloads an entire directory and then filters it in the browser:
```
import { FormEvent, useEffect, useState } from "react";
import { contactDynamicValue, type ContactDetail, type ContactList } from "@buildwithhq/module-sdk";
import { getContact, listContacts } from "./api";
const emptyContacts: ContactList = {
contractVersion: 1,
pageNumber: 1,
pageSize: 100,
totalRecords: 0,
totalPages: 0,
totalIsExact: false,
hasMore: false,
items: [],
};
type UnstyledContactsDirectoryProps = {
contactType?: string;
};
export function UnstyledContactsDirectory({
contactType = "",
}: UnstyledContactsDirectoryProps) {
const [draftSearch, setDraftSearch] = useState("");
const [search, setSearch] = useState("");
const [pageNumber, setPageNumber] = useState(1);
const [result, setResult] = useState(emptyContacts);
const [selected, setSelected] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
listContacts(search, controller.signal, contactType, pageNumber, 100)
.then(setResult)
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error ? caught.message : "Contacts could not be loaded.");
}
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [contactType, pageNumber, search]);
function submitSearch(event: FormEvent) {
event.preventDefault();
setPageNumber(1);
setSearch(draftSearch.trim());
}
async function openContact(recordId: string) {
setError(null);
try {
setSelected(await getContact(recordId));
} catch (caught) {
setError(caught instanceof Error ? caught.message : "The contact could not be loaded.");
}
}
return (
Contacts
Showing {result.items.length} on this page; {result.totalIsExact ? result.totalRecords : `at least ${result.totalRecords}`} authorized contacts found.
{error &&
{error}
}
{!loading && result.items.length === 0 &&
No visible contacts match this search.
}
{result.items.map((contact) => (
))}
{selected && (
{selected.title}
{selected.email || "No email"} · {selected.phone || selected.mobilePhone || "No phone"}
{selected.relationCount} related records · {selected.activityCount} activity items
{selected.dynamicFields.map((field) => (
{field.fieldLabel}
{contactDynamicValue(field)}
))}
)}
);
}
```
### Replacement-safe create and update
Create accepts native fields, an optional authorized location, and at most 200 typed dynamic-field values. Update is intentionally replacement-shaped: omitted native fields become empty. Always rebuild the request from the current secured detail, apply the user's changes, and send the current `updatedUtc`. A stale edit returns `409`; reload before offering a deliberate retry.
```
import type {
ContactDetail,
ContactDynamicField,
ContactDynamicFieldInput,
ContactWrite,
} from "@buildwithhq/module-sdk";
import { createContact, setContactFavorite, updateContact } from "./api";
import type { RecordCreateScope } from "./RecordCreateScopeSelect";
const dynamicInput = (field: ContactDynamicField): ContactDynamicFieldInput => ({
fieldKey: field.fieldKey,
valueText: field.valueText ?? undefined,
valueInt: field.valueInt ?? undefined,
valueDecimal: field.valueDecimal ?? undefined,
valueDateTime: field.valueDateTime ?? undefined,
valueBool: field.valueBool ?? undefined,
valueGuid: field.valueGuid ?? undefined,
valueJson: field.valueJson ?? undefined,
});
export const contactReplacement = (current: ContactDetail): ContactWrite => ({
expectedUpdatedUtc: current.updatedUtc,
locationId: current.locationId ?? undefined,
contactType: current.contactType ?? "",
firstName: current.firstName ?? "",
lastName: current.lastName ?? "",
companyName: current.companyName ?? "",
email: current.email ?? "",
phone: current.phone ?? "",
mobilePhone: current.mobilePhone ?? "",
address1: current.address1 ?? "",
address2: current.address2 ?? "",
city: current.city ?? "",
stateProvince: current.stateProvince ?? "",
postalCode: current.postalCode ?? "",
country: current.country ?? "",
notes: current.notes ?? "",
dynamicFields: current.dynamicFields.map(dynamicInput),
});
// Pass the user's current choice from the secured scope selector.
export async function addCustomer(chosenScope: RecordCreateScope) {
return createContact({
...chosenScope,
contactType: "Customer",
firstName: "Ada",
lastName: "Lovelace",
companyName: "Analytical Engines",
email: "ada@example.test",
dynamicFields: [{ fieldKey: "customerTier", valueText: "Gold" }],
});
}
export async function saveContact(
current: ContactDetail,
changes: Omit,
) {
return updateContact(current.recordId, {
...contactReplacement(current),
...changes,
expectedUpdatedUtc: current.updatedUtc,
});
}
export async function toggleContactFavorite(current: ContactDetail) {
return setContactFavorite(current.recordId, !current.isFavorite);
}
```
### Optional styling
The unstyled component uses semantic HTML and can inherit a product's own design system. This small layer makes a two-column directory without changing its data behavior:
```
.custom-contacts { width: 100%; max-width: none; }
.custom-contacts form { display: flex; gap: .75rem; align-items: end; }
.custom-contacts form label { flex: 1; }
.custom-contacts input { box-sizing: border-box; width: 100%; }
.custom-contacts ul { list-style: none; margin: 1rem 0; padding: 0; }
.custom-contacts li + li { border-top: 1px solid var(--line, #d8dee8); }
.custom-contacts li > button {
align-items: start;
background: transparent;
border: 0;
display: grid;
gap: .25rem;
padding: .85rem 0;
text-align: left;
width: 100%;
}
.custom-contacts nav { display: flex; gap: 1rem; align-items: center; }
@media (min-width: 800px) {
.custom-contacts { display: grid; grid-template-columns: minmax(18rem, 1fr) minmax(22rem, 2fr); gap: 1.5rem; }
}
```
### Exact lifecycle and data path
| Purpose | Method and route | Contract behavior |
| --- | --- | --- |
| Secured list/search | `GET /api/modules/contacts?search=&contactType=&locationId=&pageNumber=1&pageSize=100` | Search first, then browse up to 5,000 matches; 1–200 rows per page; use `hasMore` for Next |
| Full authorized match count | `GET /api/modules/contacts/count?search=&contactType=&locationId=` | Same filters and security; no 5,000 cap on the total |
| Secured detail | `GET /api/modules/contacts/{recordId}` | Native and visible dynamic fields, relation/activity counts, and personal favorite state |
| Create | `POST /api/modules/contacts` | Requires create permission, an authorized location, and at least one name/company/email/phone value |
| Replace/update | `PUT /api/modules/contacts/{recordId}` | Requires edit permission and the exact `expectedUpdatedUtc`; stale state is 409 |
| Personal favorite | `PUT /api/modules/contacts/{recordId}/favorite` | Current signed-in user's favorite state only |
These routes derive SaaS, account, user, DataRole, and effective location scope from the authenticated server identity. The browser supplies search, filter, page, location, and record targets only. The service executes `sp_Contacts_ListSecured`, `sp_Contacts_GetSecured`, `sp_Contacts_CreateSecured`, `sp_Contacts_UpdateSecured`, and `sp_Contacts_SetFavoriteSecured`; React does not trim a broader SQL result after retrieval.
Validation failures return `400`, permission failures return `403` where disclosure is safe, stale replacements return `409`, and an absent or inaccessible detail target returns the same `404 contacts_not_found` envelope so it cannot be used as a cross-tenant existence oracle.
See the [complete React component catalog](../builder-guide/react-component-catalog.html) for all generated page blocks and operations, or the [headless application guide](../builder-guide/headless-application-kit.html) when the React application lives outside the BuildWithHQ tenant shell.
## Build a professional Contacts dashboard
These six registry-backed presentation blocks let a designer turn the secured Contacts 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/contacts`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/contacts-sample.json`, its executable authenticated page at `pages/first-class/contacts-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 Contacts projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Visible contacts, new records, companies, and favorites |
| `entity-list` | `core.entity-list` | Recently touched people and organizations |
| `progress-list` | `core.progress-list` | Distribution by contact type, location, or lifecycle |
| `series-chart` | `core.series-chart` | Contact creation and updates over time |
| `data-grid` | `core.presentation-grid` | One server-paged directory with only display fields |
| `timeline` | `core.timeline` | Creation, edits, relationships, favorites, and related work |
### 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": "contacts-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Contact overview",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "visible",
"label": "Visible contacts",
"value": 1842,
"format": "number",
"tone": "neutral"
},
{
"key": "new",
"label": "New this month",
"value": 86,
"format": "number",
"tone": "success"
},
{
"key": "companies",
"label": "Companies",
"value": 412,
"format": "number",
"tone": "primary"
},
{
"key": "favorites",
"label": "Your favorites",
"value": 23,
"format": "number",
"tone": "warning"
}
]
},
"children": []
},
{
"_id": "contacts-recent",
"_type": "core.entity-list",
"props": {
"title": "Recently updated contacts",
"hasMore": true,
"items": [
{
"id": "contacts-sample-1",
"recordId": "contacts-record-1",
"primary": "Maya Chen",
"secondary": "Acme Field Services - Customer",
"status": {
"key": "active",
"label": "Active",
"tone": "success"
},
"trailing": "Reno"
},
{
"id": "contacts-sample-2",
"recordId": "contacts-record-2",
"primary": "Maya Chen - Follow-up",
"secondary": "Acme Field Services - Customer - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "contacts-sample-3",
"recordId": "contacts-record-3",
"primary": "Maya Chen - West region",
"secondary": "Acme Field Services - Customer - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "contacts-sample-4",
"recordId": "contacts-record-4",
"primary": "Maya Chen - Customer response",
"secondary": "Acme Field Services - Customer - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "contacts-sample-5",
"recordId": "contacts-record-5",
"primary": "Maya Chen - Regional operations review with a deliberately long title",
"secondary": "Acme Field Services - Customer - 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": "contacts-sample-6",
"recordId": "contacts-record-6",
"primary": "Maya Chen - Completed preview",
"secondary": "Acme Field Services - Customer - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "contacts-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Contacts by type",
"items": [
{
"key": "customer",
"label": "Customers",
"value": 1104,
"maximum": 1842,
"displayValue": "1104",
"tone": "primary",
"status": {
"key": "customer",
"label": "Customers",
"tone": "primary"
}
},
{
"key": "prospect",
"label": "Prospects",
"value": 521,
"maximum": 1842,
"displayValue": "521",
"tone": "warning",
"status": {
"key": "prospect",
"label": "Prospects",
"tone": "warning"
}
},
{
"key": "partner",
"label": "Partners",
"value": 217,
"maximum": 1842,
"displayValue": "217",
"tone": "success",
"status": {
"key": "partner",
"label": "Partners",
"tone": "success"
}
}
]
},
"children": []
},
{
"_id": "contacts-trend",
"_type": "core.series-chart",
"props": {
"title": "Contact growth",
"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": "contacts-table",
"_type": "core.presentation-grid",
"props": {
"title": "Contact directory",
"columns": [
{
"key": "name",
"label": "Name",
"type": "text",
"align": "left"
},
{
"key": "company",
"label": "Company",
"type": "text",
"align": "left"
},
{
"key": "type",
"label": "Type",
"type": "status",
"align": "left"
},
{
"key": "location",
"label": "Location",
"type": "text",
"align": "left"
},
{
"key": "updated",
"label": "Updated",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "contacts-row-1",
"recordId": "contacts-record-1",
"cells": {
"name": "Maya Chen",
"company": "Acme Field Services",
"type": {
"key": "customer",
"label": "Customer",
"tone": "primary"
},
"location": "Reno",
"updated": "2026-09-04T15:12:00Z"
}
},
{
"id": "contacts-row-2",
"recordId": "contacts-record-2",
"cells": {
"name": "Maya Chen - Follow-up",
"company": "Acme Field Services",
"type": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"location": "Reno",
"updated": "2026-09-04T15:42:00Z"
}
},
{
"id": "contacts-row-3",
"recordId": "contacts-record-3",
"cells": {
"name": "Maya Chen - West region",
"company": "Acme Field Services",
"type": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"location": "Reno",
"updated": "2026-09-04T12:18:00Z"
}
},
{
"id": "contacts-row-4",
"recordId": "contacts-record-4",
"cells": {
"name": "Maya Chen - Customer response",
"company": "Acme Field Services",
"type": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"location": "Reno",
"updated": "2026-09-03T21:07:00Z"
}
},
{
"id": "contacts-row-5",
"recordId": "contacts-record-5",
"cells": {
"name": "Maya Chen - Regional operations review with a deliberately long title",
"company": "Acme Field Services - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"type": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"location": "Reno",
"updated": "2026-09-03T16:31:00Z"
}
},
{
"id": "contacts-row-6",
"recordId": "contacts-record-6",
"cells": {
"name": "Maya Chen - Completed preview",
"company": "Acme Field Services",
"type": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"location": "Reno",
"updated": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 1842,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "contacts-timeline",
"_type": "core.timeline",
"props": {
"title": "Contact activity",
"hasMore": true,
"items": [
{
"id": "contacts-event-1",
"recordId": "contacts-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Contact updated",
"description": "Phone and company details were refreshed.",
"actor": "Sam Rivera",
"tone": "neutral"
},
{
"id": "contacts-event-2",
"recordId": "contacts-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Contact updated - Follow-up",
"description": "Phone and company details were refreshed. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "contacts-event-3",
"recordId": "contacts-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Contact updated - West region",
"description": "Phone and company details were refreshed. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "contacts-event-4",
"recordId": "contacts-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Contact updated - Customer response",
"description": "Phone and company details were refreshed. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "contacts-event-5",
"recordId": "contacts-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Contact updated - Regional operations review with a deliberately long title",
"description": "Phone and company details were refreshed. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "contacts-event-6",
"recordId": "contacts-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Contact updated - Completed preview",
"description": "Phone and company details were refreshed. 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 ContactsPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface ContactsPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: ContactsPresentationData | 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 ContactsPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: ContactsPresentationProps) {
if (error) return
The Contacts presentation could not be loaded.
;
if (loading || !data) return
Loading Contacts presentation...
;
return (
Contacts
Contact overview
People and companies, type distribution, locations, engagement, and recent changes.
);
}
```
### 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/contacts`, 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": "contacts-live",
"_type": "module.contacts",
"props": {
"experience": "directory",
"title": "Contact directory",
"pageSize": 25
},
"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
Define a small contact-type vocabulary and decide whether organizations are contacts, related company records, or both before importing data.
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/contacts.html).
---
title: "Work Orders"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/work-orders.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Work Orders
Tracks work requests through a secured lifecycle with number, customer, location, summary, priority, status, assignments, related records, and service evidence.
## In brief
- **Runtime key:** module.work-orders
- **Experience:** Operational work
- **Security:** Server-enforced
Use Work Orders when an app needs accountable operational work that can be created, prioritized, assigned, moved through approved states, and connected to customers or other records.
[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.work-orders. 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
Tracks work requests through a secured lifecycle with number, customer, location, summary, priority, status, assignments, related records, and service evidence.
## Key capabilities
- Search and filter work orders by text and status.
- Create work with summary, description, priority, and optional customer context.
- Move work through the supported status lifecycle with optimistic concurrency.
- Assign team members, designate primary responsibility, and expose related-record counts.
- Let the tenant choose whether Work Order payment collection is disabled, optional, required, manual, at creation, or required before completion.
- Show secured payment history and queue idempotent charges against protected, consented payment methods without exposing card or provider secrets.
## Common uses
- Field service and maintenance dispatch.
- Facilities, asset, repair, installation, or inspection follow-up.
- Internal service requests and operational task queues that need a stronger lifecycle than a simple task.
## How it connects
Work Orders compose naturally with Contacts, Calendar, Files, Conversations, Notifications, Record Relations, Record Activity, and the shared tenant payment ledger. Charges remain linked payment records rather than columns or raw processor data copied into Work Orders.
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 service limits rows by SaaS app, tenant, DataRole, and location and separately checks create, transition, assignment, and payment authority. The account owner manages payment policy and external connector references. Collection requires the existing `CanBillRecords` capability plus edit and record scope; customer and assignee identifiers are query/action targets, not proof of access.
- 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.work-orders` 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.
### Explicit scope for create helpers
The native create helpers below require the user's current choice of Personal or one eligible DataRole. In a tenant-runtime component, render `RecordCreateScopeSelect allowAll` from `./RecordCreateScopeSelect` and read its submitted form with `readRecordCreateScope`. Pass that choice to the helper; do not infer it from the page, menu, role count, or a default location. When active locations exist, explicitly choose an authorized location or All when permitted; Personal has no location. For a shared-only helper, validate the returned choice is shared and has a DataRole before calling it. The typed choice is a request, and SQL rechecks current authority. Child actions keep their reviewed parent scope. Ordinary edits preserve the loaded scope and revision; moving scope is a separate explicit action.
## Copy/paste the Work Orders page
The native block is the complete starting point: secured search and status filters, bounded server pagination, create/detail views, status transitions, assignments, related-record counts, and the permission-aware payment panel. Paste this page document into Raw JSON mode:
```
{
"blocks": [
{
"_id": "operations-work-orders",
"_type": "module.work-orders",
"props": {},
"children": []
}
]
}
```
The native list asks SQL for only 100 authorized rows at a time. Its next-page control follows `hasMore`; it labels `totalRecords` as “at least” while `totalIsExact` is false rather than running a tenant-wide count for every page.
### Custom unstyled React work queue
For custom presentation inside `apps/tenant-runtime/src`, retain the authenticated API helper and SDK parser and replace only the markup. Search, status, location, ordering, and pagination stay in the stored procedure; React receives the page it should display:
```
import { FormEvent, useEffect, useState } from "react";
import {
workOrderStatuses,
type WorkOrderDetail,
type WorkOrderList,
type WorkOrderStatus,
} from "@buildwithhq/module-sdk";
import { getWorkOrder, listWorkOrders } from "./api";
const emptyWorkOrders: WorkOrderList = {
contractVersion: 1,
pageNumber: 1,
pageSize: 50,
totalRecords: 0,
totalPages: 0,
totalIsExact: true,
hasMore: false,
canCreate: false,
canBill: false,
items: [],
};
type UnstyledWorkOrderQueueProps = {
locationId?: string;
};
export function UnstyledWorkOrderQueue({ locationId = "" }: UnstyledWorkOrderQueueProps) {
const [draftSearch, setDraftSearch] = useState("");
const [search, setSearch] = useState("");
const [status, setStatus] = useState("");
const [pageNumber, setPageNumber] = useState(1);
const [result, setResult] = useState(emptyWorkOrders);
const [selected, setSelected] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
listWorkOrders(search, status, controller.signal, pageNumber, 50, locationId)
.then(setResult)
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error ? caught.message : "Work orders could not be loaded.");
}
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [locationId, pageNumber, search, status]);
function submitSearch(event: FormEvent) {
event.preventDefault();
setPageNumber(1);
setSearch(draftSearch.trim());
}
async function openWorkOrder(recordId: string) {
setError(null);
try {
setSelected(await getWorkOrder(recordId));
} catch (caught) {
setError(caught instanceof Error ? caught.message : "The work order could not be loaded.");
}
}
const totalLabel = result.totalIsExact
? `${result.totalRecords} matching work orders`
: `At least ${result.totalRecords} matching work orders`;
return (
)}
);
}
```
### Replacement-safe work and lifecycle helpers
Create can use a server-generated number. Update replaces the editable work-order fields, so rebuild it from the latest secured detail and include its `updatedUtc`. Status and assignment are separate commands, and each command advances that timestamp. Re-read the detail after every mutation before issuing another command.
```
import type {
WorkOrderAssignmentRole,
WorkOrderDetail,
WorkOrderStatus,
WorkOrderWrite,
WorkOrderUpdate,
} from "@buildwithhq/module-sdk";
import {
assignWorkOrder,
createWorkOrder,
setWorkOrderStatus,
updateWorkOrder,
} from "./api";
import type { RecordCreateScope } from "./RecordCreateScopeSelect";
export const workOrderReplacement = (current: WorkOrderDetail): WorkOrderUpdate => ({
expectedUpdatedUtc: current.updatedUtc,
locationId: current.locationId ?? null,
customerRecordId: current.customerRecordId ?? null,
summary: current.summary,
description: current.description ?? "",
priority: current.priority,
scheduledStartUtc: current.scheduledStartUtc ?? null,
scheduledEndUtc: current.scheduledEndUtc ?? null,
});
export async function createRepairWork(chosenScope: RecordCreateScope, customerRecordId?: string) {
return createWorkOrder({
...chosenScope,
customerRecordId: customerRecordId ?? null,
summary: "Inspect and repair rooftop unit",
description: "Confirm fault, record readings, repair, and attach closeout evidence.",
priority: "High",
});
}
export async function saveWorkOrder(
current: WorkOrderDetail,
changes: Partial>,
) {
const request: WorkOrderUpdate = {
...workOrderReplacement(current),
...changes,
expectedUpdatedUtc: current.updatedUtc,
};
return updateWorkOrder(current.recordId, request);
}
export async function moveWorkOrder(current: WorkOrderDetail, status: WorkOrderStatus) {
return setWorkOrderStatus(current.recordId, {
expectedUpdatedUtc: current.updatedUtc,
status,
});
}
export async function setWorkOrderAssignee(
current: WorkOrderDetail,
assignedUserId: string,
assignmentRole: WorkOrderAssignmentRole,
isPrimary = false,
isActive = true,
) {
return assignWorkOrder(current.recordId, {
expectedUpdatedUtc: current.updatedUtc,
assignedUserId,
assignmentRole,
isPrimary,
isActive,
});
}
```
Choose `customerRecordId` from secured Contacts results and `assignedUserId` from a server-scoped active-user picker. They are action targets, not proof of access: SQL independently checks customer readability, assignee activity, and work-order location compatibility. The server also enforces the allowed transition graph; do not duplicate it as authorization logic in React.
### Reuse the native payment panel
When a custom Work Order detail needs collection, compose the existing panel rather than handling cards or processor secrets. It shows only controls allowed by the current payment policy and user capability:
Amounts in this payment API are currently named and represented in cents. Configure only currencies whose charge amounts use two decimal places; the server rejects JPY, KRW, CLP and other zero-decimal currencies with `tenant_payments_unsupported_currency`. A queued legacy amount in one of those currencies is failed before the worker contacts Stripe.
```
import type { WorkOrderDetail } from "@buildwithhq/module-sdk";
import { TenantPaymentsPanel } from "./TenantPaymentsPanel";
type WorkOrderPaymentsProps = {
workOrder: WorkOrderDetail;
reload: () => void;
};
export function WorkOrderPayments({ workOrder, reload }: WorkOrderPaymentsProps) {
return (
);
}
```
The panel submits only a protected payment-method identity, integer amount/currency, and a fresh idempotency key. Card numbers, security codes, raw provider tokens, API secrets, and webhook secrets do not enter the browser request. If the tenant policy requires successful payment before completion, an unpaid or still-open charge makes the `Completed` transition return `409`.
### Optional styling
```
.custom-work-orders { width: 100%; max-width: none; }
.custom-work-orders form { display: flex; gap: .75rem; align-items: end; flex-wrap: wrap; }
.custom-work-orders form label { min-width: 12rem; flex: 1; }
.custom-work-orders input,
.custom-work-orders select { box-sizing: border-box; width: 100%; }
.custom-work-orders ul { list-style: none; margin: 1rem 0; padding: 0; }
.custom-work-orders li + li { border-top: 1px solid var(--line, #d8dee8); }
.custom-work-orders li > button {
background: transparent;
border: 0;
display: grid;
gap: .25rem;
padding: .85rem 0;
text-align: left;
width: 100%;
}
.custom-work-orders nav { align-items: center; display: flex; gap: 1rem; }
.custom-work-orders article {
background: var(--surface, #fff);
border: 1px solid var(--line, #d8dee8);
border-radius: .75rem;
margin-top: 1rem;
padding: 1rem;
}
```
### Exact lifecycle and data path
| Purpose | Method and route | Contract behavior |
| --- | --- | --- |
| List/search/filter | `GET /api/modules/work-orders?search=&status=&locationId=&pageNumber=1&pageSize=100` | Stable updated-time ordering; 1–200 rows; `hasMore` plus an exact/lower-bound total |
| Detail | `GET /api/modules/work-orders/{recordId}` | Work fields, customer/location labels, active assignments, and relation count |
| Create | `POST /api/modules/work-orders` | Starts at Requested; server can generate the work-order number |
| Replace/update | `PUT /api/modules/work-orders/{recordId}` | Full editable shape plus exact `expectedUpdatedUtc` |
| Transition | `PUT /api/modules/work-orders/{recordId}/status` | Server-enforced transition graph, edit permission, concurrency, and payment gate |
| Assign/deactivate | `PUT /api/modules/work-orders/{recordId}/assignment` | Technician, Lead, Contractor, or Dispatcher; active/location-compatible tenant user |
| Payment settings | `GET /api/modules/payments/settings` | Returns only allowed management/collection capabilities and safe connector references |
| Payment policy | `PUT /api/modules/payments/settings/WorkOrders` | Account-owner policy for disabled/optional/required collection and timing |
| Save connector references | `PUT /api/modules/payments/connectors` | Protected secret-store references only; no processor secret value |
| Deactivate connector | `DELETE /api/modules/payments/connectors/{connectorId}` | Account-owner operation; existing payment evidence remains |
| Protected methods | `GET /api/modules/payments/methods?customerRecordId={customerRecordId}` | Active, consented, display-safe methods only |
| Payment history | `GET /api/modules/payments/records/{recordId}` | Secured totals, payments, and charge-intent states |
| Queue charge | `POST /api/modules/payments/records/{recordId}/charges` | 202 Accepted; protected method reference and idempotency key |
The Work Orders application service executes `sp_WorkOrders_ListSecured`, `sp_WorkOrders_GetSecured`, `sp_WorkOrders_CreateSecured`, `sp_WorkOrders_UpdateSecured`, `sp_WorkOrders_SetStatusSecured`, and `sp_WorkOrders_AssignSecured`. Identity and routing stay server-owned. SQL selects only the authorized page before the React component renders it.
Validation failures return `400`, permission failures return `403`, absent or inaccessible detail targets share the same `404 work_order_not_found` response, and stale versions, invalid transitions, or a required-payment completion fence return `409`.
See the [complete React component catalog](../builder-guide/react-component-catalog.html) for all generated page blocks and operations, or the [headless application guide](../builder-guide/headless-application-kit.html) when the React application lives outside the BuildWithHQ tenant shell.
## Build a professional Work Orders dashboard
These six registry-backed presentation blocks let a designer turn the secured Work Orders 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/work-orders`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/work-orders-sample.json`, its executable authenticated page at `pages/first-class/work-orders-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 Work Orders projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Open, due, overdue, completed, and priority counts |
| `entity-list` | `core.entity-list` | Recent authorized work orders and their next action |
| `progress-list` | `core.progress-list` | Distribution by status, priority, owner, or location |
| `series-chart` | `core.series-chart` | Created, started, completed, and closed work over time |
| `data-grid` | `core.presentation-grid` | One bounded page with ownership, state, and due dates |
| `timeline` | `core.timeline` | Create, assign, dispatch, update, evidence, signoff, and completion 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": "work-orders-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Work order command center",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "open",
"label": "Open work orders",
"value": 126,
"format": "number",
"tone": "primary"
},
{
"key": "due",
"label": "Due today",
"value": 18,
"format": "number",
"tone": "warning"
},
{
"key": "overdue",
"label": "Overdue",
"value": 7,
"format": "number",
"tone": "danger"
},
{
"key": "completed",
"label": "Completed this week",
"value": 84,
"format": "number",
"tone": "success"
}
]
},
"children": []
},
{
"_id": "work-orders-recent",
"_type": "core.entity-list",
"props": {
"title": "Recent work orders",
"hasMore": true,
"items": [
{
"id": "work-orders-sample-1",
"recordId": "work-orders-record-1",
"primary": "WO-1048 - North Wing closeout",
"secondary": "Acme Field Services - Reno",
"status": {
"key": "ready-for-signoff",
"label": "Ready for signoff",
"tone": "warning"
},
"trailing": "Due today"
},
{
"id": "work-orders-sample-2",
"recordId": "work-orders-record-2",
"primary": "WO-1048 - North Wing closeout - Follow-up",
"secondary": "Acme Field Services - Reno - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "work-orders-sample-3",
"recordId": "work-orders-record-3",
"primary": "WO-1048 - North Wing closeout - West region",
"secondary": "Acme Field Services - Reno - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "work-orders-sample-4",
"recordId": "work-orders-record-4",
"primary": "WO-1048 - North Wing closeout - Customer response",
"secondary": "Acme Field Services - Reno - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "work-orders-sample-5",
"recordId": "work-orders-record-5",
"primary": "WO-1048 - North Wing closeout - Regional operations review with a deliberately long title",
"secondary": "Acme Field Services - Reno - 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": "work-orders-sample-6",
"recordId": "work-orders-record-6",
"primary": "WO-1048 - North Wing closeout - Completed preview",
"secondary": "Acme Field Services - Reno - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "work-orders-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Work orders by status",
"items": [
{
"key": "new",
"label": "New",
"value": 21,
"maximum": 126,
"displayValue": "21",
"tone": "neutral",
"status": {
"key": "new",
"label": "New",
"tone": "neutral"
}
},
{
"key": "active",
"label": "In progress",
"value": 68,
"maximum": 126,
"displayValue": "68",
"tone": "primary",
"status": {
"key": "active",
"label": "In progress",
"tone": "primary"
}
},
{
"key": "ready",
"label": "Ready for signoff",
"value": 37,
"maximum": 126,
"displayValue": "37",
"tone": "success",
"status": {
"key": "ready",
"label": "Ready for signoff",
"tone": "success"
}
}
]
},
"children": []
},
{
"_id": "work-orders-trend",
"_type": "core.series-chart",
"props": {
"title": "Work order throughput",
"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": "Completed",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "work-orders-table",
"_type": "core.presentation-grid",
"props": {
"title": "Visible work orders",
"columns": [
{
"key": "number",
"label": "Work order",
"type": "text",
"align": "left"
},
{
"key": "summary",
"label": "Summary",
"type": "text",
"align": "left"
},
{
"key": "assignee",
"label": "Assignee",
"type": "text",
"align": "left"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "due",
"label": "Due",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "work-orders-row-1",
"recordId": "work-orders-record-1",
"cells": {
"number": "WO-1048",
"summary": "North Wing closeout",
"assignee": "Jordan Lee",
"status": {
"key": "ready",
"label": "Ready for signoff",
"tone": "warning"
},
"due": "2026-09-04T23:00:00Z"
}
},
{
"id": "work-orders-row-2",
"recordId": "work-orders-record-2",
"cells": {
"number": "WO-1048 - Follow-up",
"summary": "North Wing closeout",
"assignee": "Jordan Lee",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"due": "2026-09-04T15:42:00Z"
}
},
{
"id": "work-orders-row-3",
"recordId": "work-orders-record-3",
"cells": {
"number": "WO-1048 - West region",
"summary": "North Wing closeout",
"assignee": "Jordan Lee",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"due": "2026-09-04T12:18:00Z"
}
},
{
"id": "work-orders-row-4",
"recordId": "work-orders-record-4",
"cells": {
"number": "WO-1048 - Customer response",
"summary": "North Wing closeout",
"assignee": "Jordan Lee",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"due": "2026-09-03T21:07:00Z"
}
},
{
"id": "work-orders-row-5",
"recordId": "work-orders-record-5",
"cells": {
"number": "WO-1048 - Regional operations review with a deliberately long title",
"summary": "North Wing closeout - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"assignee": "Jordan Lee",
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"due": "2026-09-03T16:31:00Z"
}
},
{
"id": "work-orders-row-6",
"recordId": "work-orders-record-6",
"cells": {
"number": "WO-1048 - Completed preview",
"summary": "North Wing closeout",
"assignee": "Jordan Lee",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"due": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 126,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "work-orders-timeline",
"_type": "core.timeline",
"props": {
"title": "Work order activity",
"hasMore": true,
"items": [
{
"id": "work-orders-event-1",
"recordId": "work-orders-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Work order moved to signoff",
"description": "WO-1048 completed its field checklist and is ready for review.",
"actor": "Jordan Lee",
"tone": "success"
},
{
"id": "work-orders-event-2",
"recordId": "work-orders-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Work order moved to signoff - Follow-up",
"description": "WO-1048 completed its field checklist and is ready for review. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "work-orders-event-3",
"recordId": "work-orders-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Work order moved to signoff - West region",
"description": "WO-1048 completed its field checklist and is ready for review. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "work-orders-event-4",
"recordId": "work-orders-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Work order moved to signoff - Customer response",
"description": "WO-1048 completed its field checklist and is ready for review. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "work-orders-event-5",
"recordId": "work-orders-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Work order moved to signoff - Regional operations review with a deliberately long title",
"description": "WO-1048 completed its field checklist and is ready for review. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "work-orders-event-6",
"recordId": "work-orders-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Work order moved to signoff - Completed preview",
"description": "WO-1048 completed its field checklist and is ready for review. 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 WorkOrdersPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface WorkOrdersPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: WorkOrdersPresentationData | 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 WorkOrdersPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: WorkOrdersPresentationProps) {
if (error) return
The Work Orders presentation could not be loaded.
;
if (loading || !data) return
Loading Work Orders presentation...
;
return (
Work Orders
Work order command center
Current workload, priority, ownership, visible records, throughput, and recent field activity.
);
}
```
### 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/work-orders`, 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": "work-orders-live",
"_type": "module.work-orders",
"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 the smallest useful status model and explicit transition rules. Add industry-specific fields as secured dynamic fields instead of forking the native lifecycle.
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/work-orders.html).
---
title: "Reservations"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/reservations.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Reservations
Turns a bookable product purchase into one secured workflow: contact identity, capacity-protected reservation, protected charge, calendar event, tenant-user assignment, resource booking, and scheduled before/after-service follow-up.
## In brief
- **Runtime key:** module.reservations
- **Experience:** Sell, schedule & follow up
- **Security:** Server-enforced
Use Reservations for appointments, classes, tours, consultations, rentals, services, and other products that must be paid for, placed on a calendar, assigned to a tenant user, connected to a customer contact, and followed up consistently.
[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.reservations. 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
Turns a bookable product purchase into one secured workflow: contact identity, capacity-protected reservation, protected charge, calendar event, tenant-user assignment, resource booking, and scheduled before/after-service follow-up.
## Key capabilities
- Define bookable products with duration, price/currency, party limits, concurrent capacity, buffers, hold time, and default location, resource, assignee, and follow-up timing.
- Create or safely reuse an authorized Contact and preserve the purchaser's name, email, phone, and notes.
- Create a capacity-protected Reservation and linked Calendar event, with an optional Resource booking and an active tenant user assigned to deliver the service.
- Queue the existing idempotent charge-intent pipeline using a protected payment-method reference; the API never receives card numbers, security codes, processor credentials, or raw payment tokens.
- Keep paid bookings pending and private until the charge worker reports success, then promote the reservation, event, resource, and follow-ups transactionally.
- Track Pending Payment, Requires Action, Confirmed, Checked In, Completed, Cancelled, No Show, and Payment Failed with optimistic concurrency and an audit timeline.
- Schedule separate pre-service and post-service follow-up date/time, recipient email, subject, message, delivery state, attempts, sent time, and outbound-message reference.
- Use idempotency keys to make checkout retries safe and prevent duplicate contacts, charges, calendar events, and reservations.
- Let the tenant account owner choose disabled, optional, or required collection; creation, manual, or completion timing; full or deposit collection; partial-payment rules; currency; minimum charge; and paid-before-completion enforcement.
- Give users with the existing billing capability a secured balance, payment history, charge history, and protected charge action for the reservation record.
## Reservation purchase flow
| Step | Canonical result | Important behavior |
| --- | --- | --- |
| Choose product, time, and assignee | Product + UTC window + eligible tenant user | Call `GET /api/modules/reservations/assignees` with product, start time, and optional location; pass the selected `optionId` as `assignedUserId` to purchase. Duration, buffers, party size, location, and capacity are revalidated inside the transaction. |
| Identify customer | Contact | An authorized exact email match may be reused; otherwise one Contact is created and linked. |
| Reserve service | Reservation + Calendar event + optional Resource booking | A paid event stays a private payment hold until the charge succeeds. |
| Charge | Billing charge intent | The existing worker uses a protected method; retries use the same idempotency key. |
| Confirm and serve | Lifecycle + audit events | Calendar, resource, and follow-up state remains synchronized. |
| Follow up | PreService and PostService jobs | Each has its own time, email, message, attempts, result, and evidence. |
## Tenant API
- `GET/POST /api/modules/reservations/products` lists and manages the catalog.
- `GET /api/modules/reservations` lists a secured schedule window; `GET /api/modules/reservations/{recordId}` returns detail and history.
- `GET /api/modules/reservations/assignees` returns at most 100 eligible tenant users for the selected product, UTC start, and optional location. Use its selected ID as `assignedUserId` in purchase or reschedule; the server rechecks eligibility and conflicts on write.
- `POST /api/modules/reservations/purchase` performs idempotent contact, calendar, reservation, and charge orchestration.
- `PUT /api/modules/reservations/{recordId}/schedule` reschedules with the current revision and a freshly selected assignee. Purchase and reschedule can return `409 reservations_conflict` when capacity, assignee availability, or the revision changes; refresh choices rather than blindly retrying.
- `PUT /api/modules/reservations/{recordId}/status` applies a lifecycle transition.
- `PUT /api/modules/reservations/{recordId}/followups` creates or reschedules a before/after-service follow-up.
- `GET/PUT /api/modules/payments/settings` reads or changes the tenant-owned Reservations and Work Orders policies.
- `GET /api/modules/payments/records/{recordId}` returns the secured balance/history; `POST /api/modules/payments/records/{recordId}/charges` queues an idempotent protected charge.
- `GET /api/modules/payments/methods` lists display-only metadata for active consented methods; connector endpoints accept external secret references, never reusable credentials.
Reservation product prices and tenant payment policies currently use cents. Two-decimal charge currencies such as USD and EUR are supported. JPY, KRW, CLP and other zero-decimal currencies are rejected until the full API, storage and browser amount contract supports their minor units. Product writes return `reservations_unsupported_currency`; payment policy and direct-charge writes return `tenant_payments_unsupported_currency`.
## Common uses
- Paid consultations and professional-service appointments.
- Tours, classes, lessons, events, and capacity-limited sessions.
- Installations, inspections, salon/wellness visits, rentals, and service calls.
- Free appointments that still need contact, calendar, assignment, capacity, and follow-up.
## How it connects
Reservations reuse Contacts, Calendar, Resources, the billing charge-intent/payment ledger, Record Relations, and the communications worker boundary. The Reservation owns lifecycle; payment, contact, calendar, and message records remain canonical and linked instead of copied into a shadow subsystem.
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
List/detail access is inherited from secured universal Records. Product management and mutations require current DataRole capability. Locations and contact targets are revalidated in the user's scope; assignees must be active users assigned to the same app and selected location. Product capacity and opt-in consultation/appointment assignee exclusivity are rechecked transactionally on purchase, reschedule, confirmation, and delayed payment success. Existing shared-capacity products remain non-exclusive by default. Identity comes from verified server context. The tenant account owner controls payment policy and connector references; collection requires the existing `CanBillRecords` capability plus record scope. Raw card data and provider credentials are never accepted or persisted by Reservations.
- 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. As the tenant account owner, configure an external payment-connector secret reference and choose separate Reservations and Work Orders collection policies; never add card fields to a page.
2. Add `module.reservations` to an in-SaaS page and place that shared page in each intended user-type menu.
3. Create each product with duration, price, capacity/party rules, buffer, hold, location/resource/assignee, and follow-up timing.
4. Grant existing DataRole, `CanBillRecords`, and location access to users who may view, collect, sell, assign, check in, complete, cancel, or follow up.
5. Exercise free, disabled, optional, required, deposit, manual, and paid-before-completion paths plus idempotent replay, full capacity, payment success/failure/Requires Action, calendar/resource synchronization, and both follow-ups before publishing.
## Copy/paste the complete Reservations experience
The native block is the safest full starting point. It includes the product catalog, sell-and-reserve form, secured schedule, detail, lifecycle actions, follow-ups, linked payment panel, and bounded server pagination. Paste this page document into Raw JSON mode:
```
{
"blocks": [
{
"_id": "reservations-workspace",
"_type": "module.reservations",
"props": {},
"children": []
}
]
}
```
The browser supplies only filters and action targets. SaaS, account, user, DataRole, location scope, billing capability, and database routing come from the authenticated server context.
### Custom unstyled schedule and detail
This component asks SQL for one authorized time window and page. It does not download a tenant's complete schedule and reduce it in React. The window must be positive and no wider than 732 days; the page size is capped at 500.
```
import { FormEvent, useEffect, useMemo, useState } from "react";
import {
reservationStatuses,
type ReservationDetail,
type ReservationList,
type ReservationStatus,
} from "@buildwithhq/module-sdk";
import { getReservation, listReservations } from "./api";
const emptyResult: ReservationList = {
contractVersion: 1,
pageNumber: 1,
pageSize: 100,
totalRecords: 0,
totalPages: 0,
items: [],
};
type ReservationQueueProps = {
assignedUserId?: string;
locationId?: string;
};
export function ReservationQueue({ assignedUserId = "", locationId = "" }: ReservationQueueProps) {
const window = useMemo(() => ({
start: new Date(Date.now() - 30 * 86_400_000).toISOString(),
end: new Date(Date.now() + 365 * 86_400_000).toISOString(),
}), []);
const [draftSearch, setDraftSearch] = useState("");
const [search, setSearch] = useState("");
const [status, setStatus] = useState("");
const [page, setPage] = useState(1);
const [result, setResult] = useState(emptyResult);
const [selected, setSelected] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
listReservations(
window.start, window.end, search, status, controller.signal,
assignedUserId, locationId, page, 100,
)
.then(setResult)
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error ? caught.message : "Reservations could not be loaded.");
}
})
.finally(() => { if (!controller.signal.aborted) setLoading(false); });
return () => controller.abort();
}, [assignedUserId, locationId, page, search, status, window]);
function submitSearch(event: FormEvent) {
event.preventDefault();
setPage(1);
setSearch(draftSearch.trim());
}
async function open(recordId: string) {
setError(null);
try { setSelected(await getReservation(recordId)); }
catch (caught) {
setError(caught instanceof Error ? caught.message : "The reservation could not be loaded.");
}
}
return
}
;
}
```
### Create and update bookable products
Product update is a full optimistic replacement. Start from the current product, preserve every value, apply the deliberate edits, and send its latest `updatedUtc`. Duration is 5–43,200 minutes; buffers 0–10,080; capacity and party size 1–100,000; hold 1–1,440 minutes.
```
import type { ReservationProduct, ReservationProductWrite } from "@buildwithhq/module-sdk";
import { saveReservationProduct } from "./api";
export async function createConsultation(enforceAssigneeExclusivity: boolean) {
return saveReservationProduct({
productKey: "consultation",
productName: "Consultation",
description: "A one-hour consultation",
durationMinutes: 60,
bufferBeforeMinutes: 15,
bufferAfterMinutes: 15,
priceCents: 12500,
currencyCode: "USD",
concurrentCapacity: 1,
enforceAssigneeExclusivity,
maxPartySize: 1,
holdMinutes: 15,
preServiceFollowupMinutes: 1440,
postServiceFollowupMinutes: 1440,
preServiceSubject: "Your consultation is tomorrow",
postServiceSubject: "Thank you for meeting with us",
isActive: true,
});
}
export const productReplacement = (current: ReservationProduct): ReservationProductWrite => ({
reservationProductId: current.reservationProductId,
expectedUpdatedUtc: current.updatedUtc,
productKey: current.productKey,
productName: current.productName,
description: current.description || "",
durationMinutes: current.durationMinutes,
bufferBeforeMinutes: current.bufferBeforeMinutes,
bufferAfterMinutes: current.bufferAfterMinutes,
priceCents: current.priceCents,
currencyCode: current.currencyCode,
concurrentCapacity: current.concurrentCapacity,
enforceAssigneeExclusivity: current.enforceAssigneeExclusivity,
maxPartySize: current.maxPartySize,
defaultLocationId: current.defaultLocationId,
defaultResourceId: current.defaultResourceId,
defaultAssignedUserId: current.defaultAssignedUserId,
holdMinutes: current.holdMinutes,
preServiceFollowupMinutes: current.preServiceFollowupMinutes,
postServiceFollowupMinutes: current.postServiceFollowupMinutes,
preServiceSubject: current.preServiceSubject || "",
postServiceSubject: current.postServiceSubject || "",
isActive: current.isActive,
});
export async function changeProductPrice(current: ReservationProduct, priceCents: number) {
return saveReservationProduct({ ...productReplacement(current), priceCents });
}
```
### Purchase once, retry safely
Generate one idempotency key when checkout begins, persist it with that checkout attempt, and reuse it for ambiguous network retries. Do not create a new key until the user starts a genuinely new purchase. An existing Contact must be in scope; otherwise supply enough contact fields for the transaction to create one. There is no separate promise-of-availability endpoint: purchase is the authoritative transactional capacity check, and a `409` means the client should offer another time.
```
import type { ReservationPurchase } from "@buildwithhq/module-sdk";
import { purchaseReservation } from "./api";
export function beginCheckout(input: Omit) {
const idempotencyKey = crypto.randomUUID();
const request: ReservationPurchase = { ...input, idempotencyKey };
// Keep `request` in checkout state. Calling retry() again is safe after a timeout.
return {
idempotencyKey,
retry: () => purchaseReservation(request),
};
}
const checkout = beginCheckout({
reservationProductId: "PRODUCT-GUID-FROM-LIST",
startUtc: "2026-10-10T18:00:00.000Z",
timeZoneId: "America/Los_Angeles",
partySize: 1,
assignedUserId: "ACTIVE-TENANT-USER-GUID",
locationId: "AUTHORIZED-LOCATION-GUID",
firstName: "Ada",
lastName: "Lovelace",
email: "ada@example.test",
paymentMethodId: "PROTECTED-CONSENTED-METHOD-ID",
preServiceFollowupUtc: "2026-10-09T18:00:00.000Z",
preServiceFollowupEmail: "ada@example.test",
postServiceFollowupUtc: "2026-10-11T18:00:00.000Z",
postServiceFollowupEmail: "ada@example.test",
});
export const purchase = () => checkout.retry();
```
### Change state and schedule follow-up
Both actions use optimistic concurrency. Re-fetch detail after every successful mutation before enabling another action; never replay an old timestamp automatically. The server enforces the lifecycle and may refuse completion when the tenant payment policy requires a paid balance.
```
import type { ReservationDetail } from "@buildwithhq/module-sdk";
import { getReservation, setReservationStatus, updateReservationFollowup } from "./api";
export async function checkIn(current: ReservationDetail) {
await setReservationStatus(current.recordId, {
expectedUpdatedUtc: current.updatedUtc,
status: "CheckedIn",
});
return getReservation(current.recordId);
}
export async function scheduleThankYou(current: ReservationDetail) {
await updateReservationFollowup(current.recordId, {
expectedUpdatedUtc: current.updatedUtc,
followupType: "PostService",
scheduledUtc: new Date(Date.now() + 86_400_000).toISOString(),
recipientEmail: current.contactEmail || "",
subject: "Thank you",
bodyText: "Thank you for visiting us.",
});
return getReservation(current.recordId);
}
```
### Optional starter styling
```
.reservation-queue { width: 100%; max-width: none; }
.reservation-queue form { align-items: end; display: flex; gap: .75rem; }
.reservation-queue form label { flex: 1; }
.reservation-queue input, .reservation-queue select { box-sizing: border-box; width: 100%; }
.reservation-queue > ul { list-style: none; margin: 1rem 0; padding: 0; }
.reservation-queue > ul button { background: transparent; border: 0; display: grid; gap: .25rem; padding: .75rem 0; text-align: left; width: 100%; }
.reservation-queue nav { align-items: center; display: flex; gap: 1rem; }
.reservation-queue article { border: 1px solid var(--line, #d8dee8); margin-top: 1rem; padding: 1rem; }
@media (max-width: 700px) { .reservation-queue form { align-items: stretch; flex-direction: column; } }
```
### Exact data path
| Purpose | Method and route | Stored-procedure contract |
| --- | --- | --- |
| Product catalog | `GET /api/modules/reservations/products` | `sp_ReservationProducts_ListSecured` |
| Create/replace product | `POST /api/modules/reservations/products` | `sp_ReservationProducts_UpsertSecured` |
| Windowed schedule | `GET /api/modules/reservations` | `sp_Reservations_ListSecured`; exact count, 1–500 rows/page |
| Detail/history | `GET /api/modules/reservations/{recordId}` | `sp_Reservations_GetSecured` |
| Transactional purchase | `POST /api/modules/reservations/purchase` | `sp_Reservations_PurchaseSecured` |
| Lifecycle action | `PUT /api/modules/reservations/{recordId}/status` | `sp_Reservations_SetStatusSecured` |
| Follow-up | `PUT /api/modules/reservations/{recordId}/followups` | `sp_ReservationFollowups_UpdateSecured` |
Paid purchases stay in a held/private state until the billing worker reports success; Calendar, Resource, Reservation, and follow-up visibility are promoted together. Payment method identifiers are protected, consented references. Card numbers, security codes, processor credentials, connection strings, and tenant database names never belong in page JSON or React props.
See the [complete React component catalog](../builder-guide/react-component-catalog.html) for all generated blocks, or the [headless guide](../builder-guide/headless-application-kit.html) for an application outside the tenant shell.
## Build a professional Reservations dashboard
These six registry-backed presentation blocks let a designer turn the secured Reservations 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/reservations`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/reservations-sample.json`, its executable authenticated page at `pages/first-class/reservations-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 Reservations projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Upcoming, confirmed, payment-held, and exception counts |
| `entity-list` | `core.entity-list` | Next authorized appointments with contact and time |
| `progress-list` | `core.progress-list` | Distribution by lifecycle, product, or payment state |
| `series-chart` | `core.series-chart` | Purchases, check-ins, completions, and cancellations |
| `data-grid` | `core.presentation-grid` | One bounded schedule page with protected payment state |
| `timeline` | `core.timeline` | Purchase, payment, confirmation, check-in, completion, and follow-up |
### 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": "reservations-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Reservation operations",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "upcoming",
"label": "Upcoming",
"value": 64,
"format": "number",
"tone": "primary"
},
{
"key": "confirmed",
"label": "Confirmed",
"value": 48,
"format": "number",
"tone": "success"
},
{
"key": "payment",
"label": "Awaiting payment",
"value": 7,
"format": "number",
"tone": "warning"
},
{
"key": "attention",
"label": "Need attention",
"value": 3,
"format": "number",
"tone": "danger"
}
]
},
"children": []
},
{
"_id": "reservations-recent",
"_type": "core.entity-list",
"props": {
"title": "Upcoming reservations",
"hasMore": true,
"items": [
{
"id": "reservations-sample-1",
"recordId": "reservations-record-1",
"primary": "Strategy consultation",
"secondary": "Maya Chen - Sep 8, 10:00 AM",
"status": {
"key": "confirmed",
"label": "Confirmed",
"tone": "success"
},
"trailing": "1 guest"
},
{
"id": "reservations-sample-2",
"recordId": "reservations-record-2",
"primary": "Strategy consultation - Follow-up",
"secondary": "Maya Chen - Sep 8, 10:00 AM - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "reservations-sample-3",
"recordId": "reservations-record-3",
"primary": "Strategy consultation - West region",
"secondary": "Maya Chen - Sep 8, 10:00 AM - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "reservations-sample-4",
"recordId": "reservations-record-4",
"primary": "Strategy consultation - Customer response",
"secondary": "Maya Chen - Sep 8, 10:00 AM - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "reservations-sample-5",
"recordId": "reservations-record-5",
"primary": "Strategy consultation - Regional operations review with a deliberately long title",
"secondary": "Maya Chen - Sep 8, 10:00 AM - 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": "reservations-sample-6",
"recordId": "reservations-record-6",
"primary": "Strategy consultation - Completed preview",
"secondary": "Maya Chen - Sep 8, 10:00 AM - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "reservations-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Reservations by state",
"items": [
{
"key": "confirmed",
"label": "Confirmed",
"value": 48,
"maximum": 86,
"displayValue": "48",
"tone": "success",
"status": {
"key": "confirmed",
"label": "Confirmed",
"tone": "success"
}
},
{
"key": "pending",
"label": "Pending payment",
"value": 7,
"maximum": 86,
"displayValue": "7",
"tone": "warning",
"status": {
"key": "pending",
"label": "Pending payment",
"tone": "warning"
}
},
{
"key": "completed",
"label": "Completed",
"value": 31,
"maximum": 86,
"displayValue": "31",
"tone": "primary",
"status": {
"key": "completed",
"label": "Completed",
"tone": "primary"
}
}
]
},
"children": []
},
{
"_id": "reservations-trend",
"_type": "core.series-chart",
"props": {
"title": "Booking activity",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Purchased",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Completed",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "reservations-table",
"_type": "core.presentation-grid",
"props": {
"title": "Reservation schedule",
"columns": [
{
"key": "service",
"label": "Service",
"type": "text",
"align": "left"
},
{
"key": "contact",
"label": "Contact",
"type": "text",
"align": "left"
},
{
"key": "start",
"label": "Start",
"type": "date",
"align": "left"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "payment",
"label": "Payment",
"type": "status",
"align": "left"
}
],
"rows": [
{
"id": "reservations-row-1",
"recordId": "reservations-record-1",
"cells": {
"service": "Strategy consultation",
"contact": "Maya Chen",
"start": "2026-09-08T17:00:00Z",
"status": {
"key": "confirmed",
"label": "Confirmed",
"tone": "success"
},
"payment": {
"key": "paid",
"label": "Paid",
"tone": "success"
}
}
},
{
"id": "reservations-row-2",
"recordId": "reservations-record-2",
"cells": {
"service": "Strategy consultation - Follow-up",
"contact": "Maya Chen",
"start": "2026-09-04T15:42:00Z",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"payment": {
"key": "in-review",
"label": "In review",
"tone": "primary"
}
}
},
{
"id": "reservations-row-3",
"recordId": "reservations-record-3",
"cells": {
"service": "Strategy consultation - West region",
"contact": "Maya Chen",
"start": "2026-09-04T12:18:00Z",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"payment": {
"key": "on-track",
"label": "On track",
"tone": "success"
}
}
},
{
"id": "reservations-row-4",
"recordId": "reservations-record-4",
"cells": {
"service": "Strategy consultation - Customer response",
"contact": "Maya Chen",
"start": "2026-09-03T21:07:00Z",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"payment": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
}
}
},
{
"id": "reservations-row-5",
"recordId": "reservations-record-5",
"cells": {
"service": "Strategy consultation - Regional operations review with a deliberately long title",
"contact": "Maya Chen - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"start": "2026-09-03T16:31:00Z",
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"payment": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
}
}
},
{
"id": "reservations-row-6",
"recordId": "reservations-record-6",
"cells": {
"service": "Strategy consultation - Completed preview",
"contact": "Maya Chen",
"start": "2026-09-02T19:14:00Z",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"payment": {
"key": "complete",
"label": "Complete",
"tone": "success"
}
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 64,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "reservations-timeline",
"_type": "core.timeline",
"props": {
"title": "Reservation lifecycle",
"hasMore": true,
"items": [
{
"id": "reservations-event-1",
"recordId": "reservations-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Reservation confirmed",
"description": "Payment succeeded and the calendar hold was promoted.",
"actor": "Billing worker",
"tone": "success"
},
{
"id": "reservations-event-2",
"recordId": "reservations-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Reservation confirmed - Follow-up",
"description": "Payment succeeded and the calendar hold was promoted. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "reservations-event-3",
"recordId": "reservations-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Reservation confirmed - West region",
"description": "Payment succeeded and the calendar hold was promoted. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "reservations-event-4",
"recordId": "reservations-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Reservation confirmed - Customer response",
"description": "Payment succeeded and the calendar hold was promoted. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "reservations-event-5",
"recordId": "reservations-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Reservation confirmed - Regional operations review with a deliberately long title",
"description": "Payment succeeded and the calendar hold was promoted. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "reservations-event-6",
"recordId": "reservations-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Reservation confirmed - Completed preview",
"description": "Payment succeeded and the calendar hold was promoted. 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 ReservationsPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface ReservationsPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: ReservationsPresentationData | 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 ReservationsPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: ReservationsPresentationProps) {
if (error) return
The Reservations presentation could not be loaded.
;
if (loading || !data) return
Loading Reservations presentation...
;
return (
Reservations
Reservation operations
Upcoming services, named consultant availability, booking state, payment readiness, capacity, and follow-up work.
);
}
```
### 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/reservations`, 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": "reservations-live",
"_type": "module.reservations",
"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 one product and one assigned team. Decide whether capacity counts people or simultaneous services, set realistic buffers and hold time, configure payment separately, and test duplicate checkout, payment failure, Requires Action, cancellation, no-show, and both follow-ups.
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/reservations.html).
---
title: "Calendar"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/calendar.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Calendar
Provides secured month/agenda scheduling for events, attendees, resources, reminders, locations, private events, custom fields, relationships, activity, and favorites.
## In brief
- **Runtime key:** module.calendar
- **Experience:** Time & scheduling
- **Security:** Server-enforced
Use Calendar for meetings, appointments, deadlines, visits, resource bookings, and other time-based work that must remain connected to the app's records and permissions.
[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.calendar. 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
Provides secured month/agenda scheduling for events, attendees, resources, reminders, locations, private events, custom fields, relationships, activity, and favorites.
## Key capabilities
- Browse monthly event ranges and search visible events.
- Create timed or all-day events with location, reminder, color, and privacy settings.
- Show attendee responses, resource bookings, custom fields, related records, and activity.
- Favorite important events and connect them to customers, projects, work orders, or cases.
## Common uses
- Customer meetings and project milestones.
- Technician visits, inspections, shifts, or resource schedules.
- Renewal dates, campaign launches, approval deadlines, and case hearings.
## How it connects
Events can relate to Contacts, Work Orders, Conversations, Files, and any authorized dynamic record. Resource bookings remain separate records so shared equipment, rooms, or people can be scheduled without embedding availability in page content.
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
Calendar queries enforce tenant, DataRole, location, and private-event rules. Attendees and related records are independently secured, and event creation/editing requires the corresponding server permission.
- 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.calendar` 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.
### Explicit scope for create helpers
The native create helpers below require the user's current choice of Personal or one eligible DataRole. In a tenant-runtime component, render `RecordCreateScopeSelect allowAll` from `./RecordCreateScopeSelect` and read its submitted form with `readRecordCreateScope`. Pass that choice to the helper; do not infer it from the page, menu, role count, or a default location. When active locations exist, explicitly choose an authorized location or All when permitted; Personal has no location. For a shared-only helper, validate the returned choice is shared and has a DataRole before calling it. The typed choice is a request, and SQL rechecks current authority. Child actions keep their reviewed parent scope. Ordinary edits preserve the loaded scope and revision; moving scope is a separate explicit action.
## Copy/paste the Calendar page
The native block supplies a secured month agenda, search, event creation, detail, attendees, resource/relation counts, custom fields, private-event handling, and favorites:
```
{
"blocks": [
{ "_id": "team-calendar", "_type": "module.calendar", "props": {}, "children": [] }
]
}
```
### Custom unstyled React calendar
The list route requires an explicit UTC window no wider than 366 days and returns one authorized page of at most 500 events. Location and owner identifiers are filters, not authorization proof.
```
import { FormEvent, useEffect, useMemo, useState } from "react";
import type { CalendarEventDetail, CalendarEventList } from "@buildwithhq/module-sdk";
import { getCalendarEvent, listCalendarEvents } from "./api";
const empty: CalendarEventList = { contractVersion: 1, windowStart: new Date(0).toISOString(), windowEnd: new Date(1).toISOString(), pageNumber: 1, pageSize: 100, totalRecords: 0, totalPages: 0, items: [] };
export function UnstyledCalendar({ locationId = "", ownerUserId = "" }) {
const window = useMemo(() => {
const now = new Date();
return {
start: new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString(),
end: new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)).toISOString(),
};
}, []);
const [draft, setDraft] = useState("");
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [result, setResult] = useState(empty);
const [selected, setSelected] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
listCalendarEvents(window.start, window.end, search, controller.signal, locationId, ownerUserId, page, 100)
.then(setResult)
.catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "Calendar could not be loaded."); });
return () => controller.abort();
}, [locationId, ownerUserId, page, search, window]);
function submit(event: FormEvent) { event.preventDefault(); setPage(1); setSearch(draft.trim()); }
async function open(recordId: string) { setSelected(await getCalendarEvent(recordId)); }
return
Calendar
{error &&
{error}
}
{result.items.map(item => )}
{selected &&
{selected.title}
{selected.description}
{selected.attendeeCount} attendees - {selected.resourceBookingCount} resources - {selected.relationCount} related records
}
;
}
```
### Create, replace, and favorite events
Update is a full optimistic replacement. Reconstruct it from current detail and keep its latest `updatedUtc`. The server revalidates location, owner, private-event, dynamic-field, and edit scope.
```
import type { CalendarDynamicField, CalendarEventDetail, CalendarEventWrite } from "@buildwithhq/module-sdk";
import { createCalendarEvent, getCalendarEvent, setCalendarFavorite, updateCalendarEvent } from "./api";
import type { RecordCreateScope } from "./RecordCreateScopeSelect";
const fieldInput = (field: CalendarDynamicField) => ({
fieldKey: field.fieldKey, valueText: field.valueText ?? undefined, valueInt: field.valueInt ?? undefined,
valueDecimal: field.valueDecimal ?? undefined, valueDateTime: field.valueDateTime ?? undefined,
valueBool: field.valueBool ?? undefined, valueGuid: field.valueGuid ?? undefined, valueJson: field.valueJson,
});
export const eventReplacement = (current: CalendarEventDetail): CalendarEventWrite => ({
expectedUpdatedUtc: current.updatedUtc, title: current.title, description: current.description || "",
startUtc: current.startUtc, endUtc: current.endUtc, isAllDay: current.isAllDay,
locationId: current.locationId || undefined, locationText: current.locationText || "",
ownerUserId: current.ownerUserId || undefined, colorKey: current.colorKey || "",
recurrenceRule: current.recurrenceRule || "", recurrenceEnd: current.recurrenceEnd || undefined,
reminderMinutes: current.reminderMinutes || 0, isPrivate: current.isPrivate,
videoConferenceUrl: current.videoConferenceUrl || "", dynamicFields: current.dynamicFields.map(fieldInput),
});
export function createPlanningSession(chosenScope: RecordCreateScope, startUtc: string, endUtc: string) {
return createCalendarEvent({ ...chosenScope, title: "Planning session", startUtc, endUtc, reminderMinutes: 15, isPrivate: false });
}
export async function renameEvent(current: CalendarEventDetail, title: string) {
await updateCalendarEvent(current.recordId, { ...eventReplacement(current), title });
return getCalendarEvent(current.recordId);
}
export async function toggleEventFavorite(current: CalendarEventDetail) {
await setCalendarFavorite(current.recordId, !current.isFavorite);
return getCalendarEvent(current.recordId);
}
```
### Optional starter styling
```
.custom-calendar { width: 100%; max-width: none; }
.custom-calendar form, .custom-calendar nav { align-items: end; display: flex; gap: .75rem; }
.custom-calendar ol { list-style: none; margin: 1rem 0; padding: 0; }
.custom-calendar li button { background: transparent; border: 0; display: grid; gap: .2rem; padding: .75rem 0; text-align: left; width: 100%; }
.custom-calendar article { border: 1px solid var(--line, #d8dee8); margin-top: 1rem; padding: 1rem; }
```
### Exact data path
| Purpose | Route | Procedure |
| --- | --- | --- |
| Window/list | `GET /api/modules/calendar` | `sp_Calendar_ListSecured` |
| Detail | `GET /api/modules/calendar/{recordId}` | `sp_Calendar_GetSecured` |
| Create | `POST /api/modules/calendar` | `sp_Calendar_CreateSecured` |
| Replace | `PUT /api/modules/calendar/{recordId}` | `sp_Calendar_UpdateSecured` |
| Favorite | `PUT /api/modules/calendar/{recordId}/favorite` | `sp_Calendar_SetFavoriteSecured` |
Attendees and resource bookings shown in detail remain their canonical secured records. This route set does not expose a separate attendee/resource write operation, so a custom page must not invent one. Use Reservations or the owning workflow where those links are created.
## Build a professional Calendar dashboard
These six registry-backed presentation blocks let a designer turn the secured Calendar 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/calendar`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/calendar-sample.json`, its executable authenticated page at `pages/first-class/calendar-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 Calendar projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Today, upcoming, response, reminder, and conflict counts |
| `entity-list` | `core.entity-list` | Next visible events with time and location |
| `progress-list` | `core.progress-list` | Attendance, event state, or resource utilization |
| `series-chart` | `core.series-chart` | Created, scheduled, completed, and cancelled events |
| `data-grid` | `core.presentation-grid` | One explicit date-window page of events |
| `timeline` | `core.timeline` | Creation, reschedule, attendance, reminder, resource, and cancellation |
### 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": "calendar-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Calendar overview",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "today",
"label": "Events today",
"value": 12,
"format": "number",
"tone": "primary"
},
{
"key": "week",
"label": "Next 7 days",
"value": 47,
"format": "number",
"tone": "neutral"
},
{
"key": "responses",
"label": "Awaiting response",
"value": 6,
"format": "number",
"tone": "warning"
},
{
"key": "conflicts",
"label": "Resource conflicts",
"value": 1,
"format": "number",
"tone": "danger"
}
]
},
"children": []
},
{
"_id": "calendar-recent",
"_type": "core.entity-list",
"props": {
"title": "Next events",
"hasMore": true,
"items": [
{
"id": "calendar-sample-1",
"recordId": "calendar-record-1",
"primary": "Field team dispatch",
"secondary": "Sep 5, 8:30 AM - Las Vegas",
"status": {
"key": "confirmed",
"label": "Confirmed",
"tone": "success"
},
"trailing": "8 attendees"
},
{
"id": "calendar-sample-2",
"recordId": "calendar-record-2",
"primary": "Field team dispatch - Follow-up",
"secondary": "Sep 5, 8:30 AM - Las Vegas - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "calendar-sample-3",
"recordId": "calendar-record-3",
"primary": "Field team dispatch - West region",
"secondary": "Sep 5, 8:30 AM - Las Vegas - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "calendar-sample-4",
"recordId": "calendar-record-4",
"primary": "Field team dispatch - Customer response",
"secondary": "Sep 5, 8:30 AM - Las Vegas - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "calendar-sample-5",
"recordId": "calendar-record-5",
"primary": "Field team dispatch - Regional operations review with a deliberately long title",
"secondary": "Sep 5, 8:30 AM - Las Vegas - 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": "calendar-sample-6",
"recordId": "calendar-record-6",
"primary": "Field team dispatch - Completed preview",
"secondary": "Sep 5, 8:30 AM - Las Vegas - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "calendar-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Events by response",
"items": [
{
"key": "accepted",
"label": "Accepted",
"value": 34,
"maximum": 47,
"displayValue": "34",
"tone": "success",
"status": {
"key": "accepted",
"label": "Accepted",
"tone": "success"
}
},
{
"key": "tentative",
"label": "Tentative",
"value": 7,
"maximum": 47,
"displayValue": "7",
"tone": "warning",
"status": {
"key": "tentative",
"label": "Tentative",
"tone": "warning"
}
},
{
"key": "pending",
"label": "No response",
"value": 6,
"maximum": 47,
"displayValue": "6",
"tone": "neutral",
"status": {
"key": "pending",
"label": "No response",
"tone": "neutral"
}
}
]
},
"children": []
},
{
"_id": "calendar-trend",
"_type": "core.series-chart",
"props": {
"title": "Schedule volume",
"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": "Completed",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "calendar-table",
"_type": "core.presentation-grid",
"props": {
"title": "Upcoming schedule",
"columns": [
{
"key": "event",
"label": "Event",
"type": "text",
"align": "left"
},
{
"key": "start",
"label": "Start",
"type": "date",
"align": "left"
},
{
"key": "location",
"label": "Location",
"type": "text",
"align": "left"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "attendees",
"label": "Attendees",
"type": "number",
"align": "right"
}
],
"rows": [
{
"id": "calendar-row-1",
"recordId": "calendar-record-1",
"cells": {
"event": "Field team dispatch",
"start": "2026-09-05T15:30:00Z",
"location": "Las Vegas",
"status": {
"key": "confirmed",
"label": "Confirmed",
"tone": "success"
},
"attendees": 8
}
},
{
"id": "calendar-row-2",
"recordId": "calendar-record-2",
"cells": {
"event": "Field team dispatch - Follow-up",
"start": "2026-09-04T15:42:00Z",
"location": "Las Vegas",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"attendees": 9
}
},
{
"id": "calendar-row-3",
"recordId": "calendar-record-3",
"cells": {
"event": "Field team dispatch - West region",
"start": "2026-09-04T12:18:00Z",
"location": "Las Vegas",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"attendees": 10
}
},
{
"id": "calendar-row-4",
"recordId": "calendar-record-4",
"cells": {
"event": "Field team dispatch - Customer response",
"start": "2026-09-03T21:07:00Z",
"location": "Las Vegas",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"attendees": 11
}
},
{
"id": "calendar-row-5",
"recordId": "calendar-record-5",
"cells": {
"event": "Field team dispatch - Regional operations review with a deliberately long title",
"start": "2026-09-03T16:31:00Z",
"location": "Las Vegas - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"attendees": 12
}
},
{
"id": "calendar-row-6",
"recordId": "calendar-record-6",
"cells": {
"event": "Field team dispatch - Completed preview",
"start": "2026-09-02T19:14:00Z",
"location": "Las Vegas",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"attendees": 13
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 12,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "calendar-timeline",
"_type": "core.timeline",
"props": {
"title": "Calendar activity",
"hasMore": true,
"items": [
{
"id": "calendar-event-1",
"recordId": "calendar-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Attendee accepted",
"description": "Maya Chen accepted Field team dispatch.",
"actor": "Maya Chen",
"tone": "success"
},
{
"id": "calendar-event-2",
"recordId": "calendar-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Attendee accepted - Follow-up",
"description": "Maya Chen accepted Field team dispatch. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "calendar-event-3",
"recordId": "calendar-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Attendee accepted - West region",
"description": "Maya Chen accepted Field team dispatch. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "calendar-event-4",
"recordId": "calendar-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Attendee accepted - Customer response",
"description": "Maya Chen accepted Field team dispatch. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "calendar-event-5",
"recordId": "calendar-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Attendee accepted - Regional operations review with a deliberately long title",
"description": "Maya Chen accepted Field team dispatch. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "calendar-event-6",
"recordId": "calendar-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Attendee accepted - Completed preview",
"description": "Maya Chen accepted Field team dispatch. 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 CalendarPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface CalendarPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: CalendarPresentationData | 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 CalendarPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: CalendarPresentationProps) {
if (error) return
The Calendar presentation could not be loaded.
;
if (loading || !data) return
Loading Calendar presentation...
;
return (
Calendar
Calendar overview
Upcoming events, attendance, resource demand, reminders, and schedule changes.
);
}
```
### 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/calendar`, 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": "calendar-live",
"_type": "module.calendar",
"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
Decide which dates are true events and which are merely fields. Use Calendar when users need scheduling, attendance, reminders, or resource coordination.
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/calendar.html).
---
title: "Files"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/files.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Files
Provides private file upload, download, metadata, tags, record attachments, comments, locks, custom fields, relationships, activity, and favorites without exposing storage coordinates.
## In brief
- **Runtime key:** module.files
- **Experience:** Documents & evidence
- **Security:** Server-enforced
Use Files as the governed document and attachment layer for contracts, photos, manuals, evidence, exports, project artifacts, and other content that belongs beside business records.
[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.files. 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
Provides private file upload, download, metadata, tags, record attachments, comments, locks, custom fields, relationships, activity, and favorites without exposing storage coordinates.
## Key capabilities
- Search and filter secured file metadata by name and content type.
- Upload, describe, tag, edit, and privately download authorized files.
- Keep personal files only you can see, up to the per-person limit the organization owner sets within the app's ceiling.
- Attach one file to multiple permitted records without duplicating the binary.
- Show comments, locks, custom fields, relations, activity, and favorite state.
## Common uses
- Customer and project documents.
- Inspection photos, work-order evidence, manuals, and invoices.
- Case evidence, knowledge source files, and signed approvals.
## How it connects
Files attach to canonical Records across Contacts, Work Orders, Conversations, cases, and dynamic modules. AI ingestion may reference approved files through the secured source pipeline while storage keys and provider credentials stay server-only.
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
Download and upload flow through authenticated backend endpoints. The browser never receives provider credentials, tenant database identity, storage paths, or unrestricted object-store locations; attachment targets are reauthorized independently.
- 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.files` 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 the Files page
The native block includes secured list/search, upload, metadata replacement, detail, tags, locks, attachment links, download, dynamic fields, and favorites:
```
{
"blocks": [
{ "_id": "secured-files", "_type": "module.files", "props": {}, "children": [] }
]
}
```
### Custom unstyled file library
Search, MIME-prefix filtering, location filtering, and paging execute on the server. Detail intentionally contains no storage provider, path, or object key.
```
import { FormEvent, useEffect, useState } from "react";
import type { FileDetail, FileList } from "@buildwithhq/module-sdk";
import { downloadFileContent, getFile, listFiles } from "./api";
const empty: FileList = { contractVersion: 1, pageNumber: 1, pageSize: 100, totalRecords: 0, totalPages: 0, items: [] };
export function UnstyledFileLibrary({ locationId = "" }) {
const [draft, setDraft] = useState("");
const [search, setSearch] = useState("");
const [contentType, setContentType] = useState("");
const [page, setPage] = useState(1);
const [result, setResult] = useState(empty);
const [selected, setSelected] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
listFiles(search, contentType, controller.signal, locationId, page, 100)
.then(setResult)
.catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "Files could not be loaded."); });
return () => controller.abort();
}, [contentType, locationId, page, search]);
function submit(event: FormEvent) { event.preventDefault(); setPage(1); setSearch(draft.trim()); }
async function open(recordId: string) { setSelected(await getFile(recordId)); }
async function download(file: FileDetail) {
const blob = await downloadFileContent(file.recordId);
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url; link.download = file.fileName; link.click();
URL.revokeObjectURL(url);
}
return
);
}
```
### 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/files`, 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": "files-live",
"_type": "module.files",
"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
Model the business record first, then attach the file to it. Do not use folder names as a substitute for tenant, record, or location authorization.
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/files.html).
---
title: "Conversations"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/conversations.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Conversations
Provides native channel threads with participants, messages, status, priority, internal notes, outbound replies, related context, activity, dynamic fields, and favorites.
## In brief
- **Runtime key:** module.conversations
- **Experience:** Customer & team communication
- **Security:** Server-enforced
Use Conversations for durable communication history across internal, email, SMS, form-driven, and other supported channels while Universal Inbox points people to the threads that need attention.
[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.conversations. 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
Provides native channel threads with participants, messages, status, priority, internal notes, outbound replies, related context, activity, dynamic fields, and favorites.
## Key capabilities
- Search and filter conversation threads by status.
- Create internal conversations and manage subject, priority, and lifecycle.
- Read escaped message history and post internal notes or permitted outbound replies.
- Expose unread/favorite state, dynamic fields, related records, and activity.
## Common uses
- Customer support and success communication.
- Project, sales, case, and operations correspondence.
- The native thread behind Universal Inbox triage and governed AI suggestions.
## How it connects
Inbound providers normalize into native Conversations and messages, then create or update Inbox attention. Threads can relate to Contacts, orders, projects, Work Orders, Files, and any authorized record without making Inbox the source of truth.
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
Conversation, message, channel, DataRole, and location access is enforced server-side. Outbound capability and edit rights are explicit; provider credentials and direct channel endpoints never reach the browser.
- 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.conversations` 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.
### Explicit scope for create helpers
The native create helpers below require the user's current choice of Personal or one eligible DataRole. In a tenant-runtime component, render `RecordCreateScopeSelect allowAll` from `./RecordCreateScopeSelect` and read its submitted form with `readRecordCreateScope`. Pass that choice to the helper; do not infer it from the page, menu, role count, or a default location. When active locations exist, explicitly choose an authorized location or All when permitted; Personal has no location. For a shared-only helper, validate the returned choice is shared and has a DataRole before calling it. The typed choice is a request, and SQL rechecks current authority. Child actions keep their reviewed parent scope. Ordinary edits preserve the loaded scope and revision; moving scope is a separate explicit action.
## Copy/paste the Conversations page
The native block provides a secured thread queue, search/status filters, internal-thread creation, detail, edit/snooze state, messages, internal notes, outbound replies when permitted, attachments metadata, custom fields, and favorites:
```
{
"blocks": [
{ "_id": "customer-conversations", "_type": "module.conversations", "props": {}, "children": [] }
]
}
```
### Custom unstyled queue and thread
The server applies status, channel, location, assignee, awaiting-reply, and search filters before returning a page. Message history is separately bounded to 1–500 messages; pass the oldest loaded timestamp as `beforeUtc` to request an older slice.
```
import { FormEvent, useEffect, useState } from "react";
import type { ConversationDetail, ConversationList, ConversationStatus } from "@buildwithhq/module-sdk";
import { getConversation, listConversations } from "./api";
const empty: ConversationList = { contractVersion: 1, pageNumber: 1, pageSize: 100, totalRecords: 0, totalPages: 0, totalIsExact: true, hasMore: false, items: [] };
export function UnstyledConversationQueue({ locationId = "", assignedToUserId = "" }) {
const [draft, setDraft] = useState("");
const [search, setSearch] = useState("");
const [status, setStatus] = useState("");
const [awaitingReplyOnly, setAwaitingReplyOnly] = useState(false);
const [page, setPage] = useState(1);
const [result, setResult] = useState(empty);
const [selected, setSelected] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
listConversations(search, status, controller.signal, undefined, locationId, assignedToUserId, awaitingReplyOnly, page, 100)
.then(setResult)
.catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "Conversations could not be loaded."); });
return () => controller.abort();
}, [assignedToUserId, awaitingReplyOnly, locationId, page, search, status]);
function submit(event: FormEvent) { event.preventDefault(); setPage(1); setSearch(draft.trim()); }
async function open(recordId: string) { setSelected(await getConversation(recordId)); }
async function loadOlder(current: ConversationDetail) {
const oldest = current.messages[0]?.createdUtc;
if (oldest) setSelected(await getConversation(current.recordId, undefined, 200, oldest));
}
return
{message.deliveryStatus})}{selected.messages.length === 200 && }}
;
}
```
### Create, replace, reply, and send directly
Conversation replacement requires a fresh `updatedUtc`. Only offer Outbound when detail returns `isOutboundCapable=true`; otherwise post a private Note. Provider delivery is asynchronous and its returned delivery state is authoritative. Direct tenant-user messages use a client message ID for idempotency.
```
import type { ConversationDetail, ConversationDynamicField } from "@buildwithhq/module-sdk";
import { createConversation, getConversation, postConversationMessage, sendDirectConversation, setConversationFavorite, updateConversation } from "./api";
import type { RecordCreateScope } from "./RecordCreateScopeSelect";
const fieldInput = (field: ConversationDynamicField) => ({ fieldKey: field.fieldKey, valueText: field.valueText ?? undefined, valueInt: field.valueInt ?? undefined, valueDecimal: field.valueDecimal ?? undefined, valueDateTime: field.valueDateTime ?? undefined, valueBool: field.valueBool ?? undefined, valueGuid: field.valueGuid ?? undefined, valueJson: field.valueJson });
export function createInternalThread(subject: string, chosenScope: RecordCreateScope, contactRecordId?: string) {
return createConversation({ ...chosenScope, subject, contactRecordId, priority: 2 });
}
export async function closeThread(current: ConversationDetail) {
await updateConversation(current.recordId, {
expectedUpdatedUtc: current.updatedUtc, subject: current.subject, status: "Closed", priority: current.priority,
assignedToUserId: current.assignedToUserId || undefined, locationId: current.locationId || undefined,
contactRecordId: current.contactRecordId || undefined, dynamicFields: current.dynamicFields.map(fieldInput),
});
return getConversation(current.recordId);
}
export async function addInternalNote(current: ConversationDetail, text: string) {
await postConversationMessage(current.recordId, { direction: "Note", bodyText: text });
return getConversation(current.recordId);
}
export async function replyToCustomer(current: ConversationDetail, text: string) {
if (!current.isOutboundCapable) throw new Error("This channel does not permit outbound replies.");
await postConversationMessage(current.recordId, { direction: "Outbound", bodyText: text });
return getConversation(current.recordId);
}
export function messageTenantUser(recipientUserId: string, bodyText: string) {
return sendDirectConversation({ recipientUserId, clientMessageId: crypto.randomUUID(), bodyText });
}
export async function toggleConversationFavorite(current: ConversationDetail) {
await setConversationFavorite(current.recordId, !current.isFavorite);
return getConversation(current.recordId);
}
```
### Optional starter styling
```
.custom-conversations { width: 100%; max-width: none; }
.custom-conversations form, .custom-conversations nav { align-items: end; display: flex; gap: .75rem; }
.custom-conversations > ul { list-style: none; margin: 1rem 0; padding: 0; }
.custom-conversations > ul button { background: transparent; border: 0; display: grid; gap: .2rem; padding: .75rem 0; text-align: left; width: 100%; }
.custom-conversations article { border: 1px solid var(--line, #d8dee8); margin-top: 1rem; padding: 1rem; }
.custom-conversations article section { border-top: 1px solid var(--line, #d8dee8); padding: .75rem 0; }
```
### Exact data path
| Purpose | Route | Procedure |
| --- | --- | --- |
| Queue/detail | `GET /api/modules/conversations`, `GET /api/modules/conversations/{recordId}` | `sp_Conversations_ListSecured`, `sp_Conversations_GetSecured` |
| Create/replace | `POST /api/modules/conversations`, `PUT /api/modules/conversations/{recordId}` | `sp_Conversations_CreateSecured`, `sp_Conversations_UpdateSecured` |
| Note/outbound reply | `POST /api/modules/conversations/{recordId}/messages` | `sp_Conversations_ReplySecured` |
| Direct tenant message | `POST /api/modules/conversations/direct` | `sp_Conversations_SendDirectSecured` |
| Favorite | `PUT /api/modules/conversations/{recordId}/favorite` | `sp_Conversations_SetFavoriteSecured` |
Universal Inbox is the attention layer; Conversation remains the message source of truth. Never duplicate messages into page state or treat an Inbox item as the thread. Outbound capability, channel connection, contact, assignee, location, and every attachment are reauthorized server-side.
## Build a professional Conversations dashboard
These six registry-backed presentation blocks let a designer turn the secured Conversations 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/conversations`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/conversations-sample.json`, its executable authenticated page at `pages/first-class/conversations-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 Conversations projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Open, unread, waiting, and message-volume counts |
| `entity-list` | `core.entity-list` | Recent permitted threads with channel and participant |
| `progress-list` | `core.progress-list` | Distribution by channel, state, queue, or owner |
| `series-chart` | `core.series-chart` | Inbound and outbound messages over time |
| `data-grid` | `core.presentation-grid` | One bounded thread page without hidden message content |
| `timeline` | `core.timeline` | Thread creation, messages, assignment, reply, reopen, and resolution |
### 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": "conversations-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Conversation center",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "open",
"label": "Open threads",
"value": 73,
"format": "number",
"tone": "primary"
},
{
"key": "unread",
"label": "Unread",
"value": 19,
"format": "number",
"tone": "warning"
},
{
"key": "waiting",
"label": "Waiting for us",
"value": 11,
"format": "number",
"tone": "danger"
},
{
"key": "today",
"label": "Messages today",
"value": 146,
"format": "number",
"tone": "success"
}
]
},
"children": []
},
{
"_id": "conversations-recent",
"_type": "core.entity-list",
"props": {
"title": "Recent conversations",
"hasMore": true,
"items": [
{
"id": "conversations-sample-1",
"recordId": "conversations-record-1",
"primary": "Installation timing",
"secondary": "Maya Chen - Email",
"status": {
"key": "waiting-for-us",
"label": "Waiting for us",
"tone": "warning"
},
"trailing": "4 messages"
},
{
"id": "conversations-sample-2",
"recordId": "conversations-record-2",
"primary": "Installation timing - Follow-up",
"secondary": "Maya Chen - Email - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "conversations-sample-3",
"recordId": "conversations-record-3",
"primary": "Installation timing - West region",
"secondary": "Maya Chen - Email - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "conversations-sample-4",
"recordId": "conversations-record-4",
"primary": "Installation timing - Customer response",
"secondary": "Maya Chen - Email - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "conversations-sample-5",
"recordId": "conversations-record-5",
"primary": "Installation timing - Regional operations review with a deliberately long title",
"secondary": "Maya Chen - Email - 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": "conversations-sample-6",
"recordId": "conversations-record-6",
"primary": "Installation timing - Completed preview",
"secondary": "Maya Chen - Email - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "conversations-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Threads by channel",
"items": [
{
"key": "email",
"label": "Email",
"value": 45,
"maximum": 73,
"displayValue": "45",
"tone": "primary",
"status": {
"key": "email",
"label": "Email",
"tone": "primary"
}
},
{
"key": "sms",
"label": "SMS",
"value": 18,
"maximum": 73,
"displayValue": "18",
"tone": "success",
"status": {
"key": "sms",
"label": "SMS",
"tone": "success"
}
},
{
"key": "internal",
"label": "Internal",
"value": 10,
"maximum": 73,
"displayValue": "10",
"tone": "neutral",
"status": {
"key": "internal",
"label": "Internal",
"tone": "neutral"
}
}
]
},
"children": []
},
{
"_id": "conversations-trend",
"_type": "core.series-chart",
"props": {
"title": "Message volume",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Inbound",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Outbound",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "conversations-table",
"_type": "core.presentation-grid",
"props": {
"title": "Visible conversations",
"columns": [
{
"key": "subject",
"label": "Subject",
"type": "text",
"align": "left"
},
{
"key": "channel",
"label": "Channel",
"type": "text",
"align": "left"
},
{
"key": "participant",
"label": "Participant",
"type": "text",
"align": "left"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "updated",
"label": "Updated",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "conversations-row-1",
"recordId": "conversations-record-1",
"cells": {
"subject": "Installation timing",
"channel": "Email",
"participant": "Maya Chen",
"status": {
"key": "waiting",
"label": "Waiting for us",
"tone": "warning"
},
"updated": "2026-09-04T17:28:00Z"
}
},
{
"id": "conversations-row-2",
"recordId": "conversations-record-2",
"cells": {
"subject": "Installation timing - Follow-up",
"channel": "Email",
"participant": "Maya Chen",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"updated": "2026-09-04T15:42:00Z"
}
},
{
"id": "conversations-row-3",
"recordId": "conversations-record-3",
"cells": {
"subject": "Installation timing - West region",
"channel": "Email",
"participant": "Maya Chen",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"updated": "2026-09-04T12:18:00Z"
}
},
{
"id": "conversations-row-4",
"recordId": "conversations-record-4",
"cells": {
"subject": "Installation timing - Customer response",
"channel": "Email",
"participant": "Maya Chen",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"updated": "2026-09-03T21:07:00Z"
}
},
{
"id": "conversations-row-5",
"recordId": "conversations-record-5",
"cells": {
"subject": "Installation timing - Regional operations review with a deliberately long title",
"channel": "Email - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"participant": "Maya Chen",
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"updated": "2026-09-03T16:31:00Z"
}
},
{
"id": "conversations-row-6",
"recordId": "conversations-record-6",
"cells": {
"subject": "Installation timing - Completed preview",
"channel": "Email",
"participant": "Maya Chen",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"updated": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 73,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "conversations-timeline",
"_type": "core.timeline",
"props": {
"title": "Conversation activity",
"hasMore": true,
"items": [
{
"id": "conversations-event-1",
"recordId": "conversations-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Inbound reply received",
"description": "The customer replied to Installation timing.",
"actor": "Maya Chen",
"tone": "primary"
},
{
"id": "conversations-event-2",
"recordId": "conversations-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Inbound reply received - Follow-up",
"description": "The customer replied to Installation timing. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "conversations-event-3",
"recordId": "conversations-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Inbound reply received - West region",
"description": "The customer replied to Installation timing. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "conversations-event-4",
"recordId": "conversations-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Inbound reply received - Customer response",
"description": "The customer replied to Installation timing. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "conversations-event-5",
"recordId": "conversations-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Inbound reply received - Regional operations review with a deliberately long title",
"description": "The customer replied to Installation timing. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "conversations-event-6",
"recordId": "conversations-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Inbound reply received - Completed preview",
"description": "The customer replied to Installation timing. 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 ConversationsPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface ConversationsPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: ConversationsPresentationData | 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 ConversationsPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: ConversationsPresentationProps) {
if (error) return
The Conversations presentation could not be loaded.
;
if (loading || !data) return
Loading Conversations presentation...
;
return (
Conversations
Conversation center
Open threads, unread activity, channels, response state, and recent messages.
);
}
```
### 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/conversations`, 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": "conversations-live",
"_type": "module.conversations",
"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
Use one canonical thread per real conversation and relate it to business context. Avoid copying message bodies into custom records or building a second inbox table family.
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/conversations.html).
---
title: "Company news"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/company-news.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Company news
Publishes searchable internal announcements with draft, scheduled, published, and expired visibility, author controls, custom fields, relationships, activity, and favorites.
## In brief
- **Runtime key:** module.company-news
- **Experience:** Announcements
- **Security:** Server-enforced
Use Company News for controlled organization-wide or location-scoped announcements such as policy changes, releases, leadership updates, closures, and internal notices.
[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.company-news. 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
Publishes searchable internal announcements with draft, scheduled, published, and expired visibility, author controls, custom fields, relationships, activity, and favorites.
## Key capabilities
- Search visible announcements and open their secured detail.
- Author and edit announcements only when the response grants edit permission.
- Control draft, scheduled, published, and expired visibility.
- Connect announcements to records, activity, custom fields, and personal favorites.
## Common uses
- Internal news and release announcements.
- Location-specific closures, safety notices, or operating updates.
- Policy and program communications linked to supporting records or files.
## How it connects
Announcements can link to Knowledge Articles, Files, Web Links, and other records for durable supporting detail. Notifications can call attention to a published announcement without replacing the announcement itself.
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
Publication state, SaaS, tenant, DataRole, and location are evaluated by secured reads. Announcement bodies render as escaped text, and authoring controls depend on server-reported permission.
- 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.company-news` 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.
### Explicit scope for create helpers
The native create helpers below require the user's current choice of one eligible DataRole with `isPersonal: false`. In a tenant-runtime component, render `RecordCreateScopeSelect allowAll allowPersonal={false}` from `./RecordCreateScopeSelect` and read its submitted form with `readRecordCreateScope`. Pass that choice to the helper; do not infer it from the page, menu, role count, or a default location. When active locations exist, explicitly choose an authorized location or All when permitted; Personal has no location. For a shared-only helper, validate the returned choice is shared and has a DataRole before calling it. The typed choice is a request, and SQL rechecks current authority. Child actions keep their reviewed parent scope. Ordinary edits preserve the loaded scope and revision; moving scope is a separate explicit action.
## Copy/paste the Company News page
The native block supplies secured published/draft visibility, search, create/edit controls driven by `canEdit`, detail, custom fields, relation/activity counts, and favorites:
```
{
"blocks": [
{ "_id": "company-announcements", "_type": "module.company-news", "props": {}, "children": [] }
]
}
```
### Custom unstyled news page
Set `includeDrafts` only for an authoring view. The server still decides whether the current user may see or edit drafts. Search, location, publication rules, and paging are applied before rows reach React.
```
import { FormEvent, useEffect, useState } from "react";
import type { CompanyNewsDetail, CompanyNewsList } from "@buildwithhq/module-sdk";
import { getCompanyNews, listCompanyNews } from "./api";
const empty: CompanyNewsList = { contractVersion: 1, pageNumber: 1, pageSize: 50, totalRecords: 0, totalPages: 0, totalIsExact: false, hasMore: false, canCreate: false, canEdit: false, items: [] };
export function UnstyledCompanyNews({ authoring = false, locationId = "" }) {
const [draft, setDraft] = useState("");
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [result, setResult] = useState(empty);
const [selected, setSelected] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
listCompanyNews(search, authoring, controller.signal, locationId, page, 50)
.then(setResult)
.catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "News could not be loaded."); });
return () => controller.abort();
}, [authoring, locationId, page, search]);
function submit(event: FormEvent) { event.preventDefault(); setPage(1); setSearch(draft.trim()); }
async function open(recordId: string) { setSelected(await getCompanyNews(recordId)); }
return
{selected.status} - {selected.createdByDisplayName || "Company news"}}
;
}
```
### Create, schedule, replace, and favorite
An update is an optimistic full replacement. Preserve publication times, location, dynamic fields, and the latest `updatedUtc`. A future `publishUtc` schedules visibility; `expiresUtc` removes it from ordinary published reads after expiry.
```
import type { CompanyNewsDetail, CompanyNewsDynamicField } from "@buildwithhq/module-sdk";
import { createCompanyNews, getCompanyNews, setCompanyNewsFavorite, updateCompanyNews } from "./api";
import type { RecordCreateScope } from "./RecordCreateScopeSelect";
type SharedScope = RecordCreateScope & { isPersonal: false; dataRoleId: number };
const fieldInput = (field: CompanyNewsDynamicField) => ({ fieldKey: field.fieldKey, valueText: field.valueText ?? undefined, valueInt: field.valueInt ?? undefined, valueDecimal: field.valueDecimal ?? undefined, valueDateTime: field.valueDateTime ?? undefined, valueBool: field.valueBool ?? undefined, valueGuid: field.valueGuid ?? undefined, valueJson: field.valueJson });
export function createDraftAnnouncement(title: string, body: string, chosenScope: SharedScope) {
return createCompanyNews({ ...chosenScope, title, body, isPublished: false, dynamicFields: [] });
}
export async function publishAnnouncement(current: CompanyNewsDetail, publishUtc = new Date().toISOString()) {
if (!current.canEdit) throw new Error("The signed-in user cannot edit this announcement.");
await updateCompanyNews(current.recordId, {
expectedUpdatedUtc: current.updatedUtc, title: current.title, body: current.body || "",
locationId: current.locationId || undefined, publishUtc, expiresUtc: current.expiresUtc || undefined,
isPublished: true, dynamicFields: current.dynamicFields.map(fieldInput),
});
return getCompanyNews(current.recordId);
}
export async function toggleNewsFavorite(current: CompanyNewsDetail) {
await setCompanyNewsFavorite(current.recordId, !current.isFavorite);
return getCompanyNews(current.recordId);
}
```
### Optional starter styling
```
.custom-news { width: 100%; max-width: none; }
.custom-news > header, .custom-news form, .custom-news nav { align-items: center; display: flex; gap: .75rem; justify-content: space-between; }
.custom-news ol { list-style: none; margin: 1rem 0; padding: 0; }
.custom-news li button { background: transparent; border: 0; display: grid; gap: .2rem; padding: .75rem 0; text-align: left; width: 100%; }
.custom-news article { border: 1px solid var(--line, #d8dee8); margin-top: 1rem; padding: 1rem; white-space: pre-wrap; }
```
### Exact data path
| Purpose | Route | Procedure |
| --- | --- | --- |
| List/detail | `GET /api/modules/company-news`, `GET /api/modules/company-news/{recordId}` | `sp_CompanyNews_ListSecured`, `sp_CompanyNews_GetSecured` |
| Create/replace | `POST /api/modules/company-news`, `PUT /api/modules/company-news/{recordId}` | `sp_CompanyNews_CreateSecured`, `sp_CompanyNews_UpdateSecured` |
| Favorite | `PUT /api/modules/company-news/{recordId}/favorite` | `sp_CompanyNews_SetFavoriteSecured` |
Title is limited to 300 characters, list page size to 200, and dynamic inputs to 200 unique field keys. The API returns escaped text; a custom renderer must not inject an announcement body with `dangerouslySetInnerHTML`.
## Build a professional Company news dashboard
These six registry-backed presentation blocks let a designer turn the secured Company news 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/company-news`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/company-news-sample.json`, its executable authenticated page at `pages/first-class/company-news-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 Company news projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Published, draft, scheduled, and expiring counts |
| `entity-list` | `core.entity-list` | Recent announcements within the user's audience |
| `progress-list` | `core.progress-list` | Distribution by lifecycle, audience, or category |
| `series-chart` | `core.series-chart` | Publishing and archival activity |
| `data-grid` | `core.presentation-grid` | One bounded editorial page with safe text |
| `timeline` | `core.timeline` | Draft, schedule, publish, edit, expire, 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": "company-news-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Company news overview",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "published",
"label": "Published",
"value": 42,
"format": "number",
"tone": "success"
},
{
"key": "drafts",
"label": "Drafts",
"value": 6,
"format": "number",
"tone": "neutral"
},
{
"key": "scheduled",
"label": "Scheduled",
"value": 3,
"format": "number",
"tone": "primary"
},
{
"key": "expiring",
"label": "Expiring soon",
"value": 2,
"format": "number",
"tone": "warning"
}
]
},
"children": []
},
{
"_id": "company-news-recent",
"_type": "core.entity-list",
"props": {
"title": "Recent announcements",
"hasMore": true,
"items": [
{
"id": "company-news-sample-1",
"recordId": "company-news-record-1",
"primary": "New field safety standard",
"secondary": "Operations - all locations",
"status": {
"key": "published",
"label": "Published",
"tone": "success"
},
"trailing": "Sep 4"
},
{
"id": "company-news-sample-2",
"recordId": "company-news-record-2",
"primary": "New field safety standard - Follow-up",
"secondary": "Operations - all locations - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "company-news-sample-3",
"recordId": "company-news-record-3",
"primary": "New field safety standard - West region",
"secondary": "Operations - all locations - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "company-news-sample-4",
"recordId": "company-news-record-4",
"primary": "New field safety standard - Customer response",
"secondary": "Operations - all locations - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "company-news-sample-5",
"recordId": "company-news-record-5",
"primary": "New field safety standard - Regional operations review with a deliberately long title",
"secondary": "Operations - all locations - 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": "company-news-sample-6",
"recordId": "company-news-record-6",
"primary": "New field safety standard - Completed preview",
"secondary": "Operations - all locations - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "company-news-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Announcements by state",
"items": [
{
"key": "published",
"label": "Published",
"value": 42,
"maximum": 51,
"displayValue": "42",
"tone": "success",
"status": {
"key": "published",
"label": "Published",
"tone": "success"
}
},
{
"key": "draft",
"label": "Draft",
"value": 6,
"maximum": 51,
"displayValue": "6",
"tone": "neutral",
"status": {
"key": "draft",
"label": "Draft",
"tone": "neutral"
}
},
{
"key": "scheduled",
"label": "Scheduled",
"value": 3,
"maximum": 51,
"displayValue": "3",
"tone": "primary",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "primary"
}
}
]
},
"children": []
},
{
"_id": "company-news-trend",
"_type": "core.series-chart",
"props": {
"title": "Publishing activity",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Published",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Archived",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "company-news-table",
"_type": "core.presentation-grid",
"props": {
"title": "Announcements",
"columns": [
{
"key": "title",
"label": "Announcement",
"type": "text",
"align": "left"
},
{
"key": "audience",
"label": "Audience",
"type": "text",
"align": "left"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "publish",
"label": "Publish time",
"type": "date",
"align": "left"
},
{
"key": "author",
"label": "Author",
"type": "text",
"align": "left"
}
],
"rows": [
{
"id": "company-news-row-1",
"recordId": "company-news-record-1",
"cells": {
"title": "New field safety standard",
"audience": "All locations",
"status": {
"key": "published",
"label": "Published",
"tone": "success"
},
"publish": "2026-09-04T16:00:00Z",
"author": "Operations"
}
},
{
"id": "company-news-row-2",
"recordId": "company-news-record-2",
"cells": {
"title": "New field safety standard - Follow-up",
"audience": "All locations",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"publish": "2026-09-04T15:42:00Z",
"author": "Operations"
}
},
{
"id": "company-news-row-3",
"recordId": "company-news-record-3",
"cells": {
"title": "New field safety standard - West region",
"audience": "All locations",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"publish": "2026-09-04T12:18:00Z",
"author": "Operations"
}
},
{
"id": "company-news-row-4",
"recordId": "company-news-record-4",
"cells": {
"title": "New field safety standard - Customer response",
"audience": "All locations",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"publish": "2026-09-03T21:07:00Z",
"author": "Operations"
}
},
{
"id": "company-news-row-5",
"recordId": "company-news-record-5",
"cells": {
"title": "New field safety standard - Regional operations review with a deliberately long title",
"audience": "All locations - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"publish": "2026-09-03T16:31:00Z",
"author": "Operations"
}
},
{
"id": "company-news-row-6",
"recordId": "company-news-record-6",
"cells": {
"title": "New field safety standard - Completed preview",
"audience": "All locations",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"publish": "2026-09-02T19:14:00Z",
"author": "Operations"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 42,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "company-news-timeline",
"_type": "core.timeline",
"props": {
"title": "Editorial timeline",
"hasMore": true,
"items": [
{
"id": "company-news-event-1",
"recordId": "company-news-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Announcement published",
"description": "New field safety standard became visible to its audience.",
"actor": "Operations",
"tone": "success"
},
{
"id": "company-news-event-2",
"recordId": "company-news-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Announcement published - Follow-up",
"description": "New field safety standard became visible to its audience. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "company-news-event-3",
"recordId": "company-news-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Announcement published - West region",
"description": "New field safety standard became visible to its audience. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "company-news-event-4",
"recordId": "company-news-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Announcement published - Customer response",
"description": "New field safety standard became visible to its audience. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "company-news-event-5",
"recordId": "company-news-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Announcement published - Regional operations review with a deliberately long title",
"description": "New field safety standard became visible to its audience. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "company-news-event-6",
"recordId": "company-news-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Announcement published - Completed preview",
"description": "New field safety standard became visible to its audience. 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 CompanyNewsPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface CompanyNewsPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: CompanyNewsPresentationData | 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 CompanyNewsPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: CompanyNewsPresentationProps) {
if (error) return
The Company news presentation could not be loaded.
;
if (loading || !data) return
Loading Company news presentation...
;
return (
Company news
Company news overview
Published announcements, drafts, schedules, audience reach, and editorial activity.
);
}
```
### 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/company-news`, 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": "company-news-live",
"_type": "module.company-news",
"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
Use Company News for timely announcements and Knowledge Articles for maintained reference material. Link the two when an announcement introduces a lasting policy.
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/company-news.html).
---
title: "Knowledge Core"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/knowledge-core.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Knowledge Core
Turns tenant knowledge into owned, revisioned, reviewable records that connect to business context, answer questions, support permission-aware AI/RAG, and learn safely from resolved support work.
## In brief
- **Runtime key:** module.knowledge-articles
- **Experience:** Maintained, cited knowledge
- **Security:** Server-enforced
Use Knowledge Core for policies, procedures, product help, FAQs, playbooks, support resolutions, and other maintained reference material whose ownership, revision, freshness, audience, and AI use must remain explicit.
[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.knowledge-articles. 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
Turns tenant knowledge into owned, revisioned, reviewable records that connect to business context, answer questions, support permission-aware AI/RAG, and learn safely from resolved support work.
## Key capabilities
- Create Draft knowledge and publish or archive it through permission-checked lifecycle actions.
- Preserve immutable revisions, publication history, authority, AI eligibility, and optimistic-concurrency evidence.
- Assign one primary accountable owner to a user or DataRole and schedule recurring review work.
- Relate knowledge to any authorized `RecordId` and retrieve bounded contextual knowledge without copying the business record.
- Capture unanswered questions, repeated occurrences, usage, feedback, and secured knowledge-gap signals.
- Register only Published, AI-eligible revisions for secured semantic/multimodal retrieval with exact citations.
- Bring current support knowledge into fenced GoClaw Inbox context and turn resolved cases into sanitized Draft candidates.
## Ownership, revisions, and lifecycle
Each item remains one canonical Record and Knowledge Core item. Its lifecycle is **Draft**, **Published**, or **Archived**; publication and archive are explicit permission-checked actions. Editing creates an immutable revision instead of erasing the evidence behind an earlier answer or citation. Stale browser updates are rejected through the item's expected update timestamp.
An active primary owner is either a named user or a DataRole, never an ambiguous mixture. Ownership makes review and escalation accountable without granting the owner access beyond normal tenant, DataRole, record, field, and location rules.
## Reviews and freshness
A review policy sets the interval, due-soon window, stale threshold, and whether a review Task is created. Due work appears through the existing Tasks and Universal Inbox attention surfaces. An authorized reviewer can confirm the current revision, revise and republish it, reassign the review, or archive the item. Confirming current schedules the next review without pretending the content changed.
Freshness is evidence, not decoration. Current, due-soon, due, and stale states help users and AI distinguish maintained guidance from material that needs review. Stale content does not silently become authoritative merely because it still matches a search.
## Relations and contextual knowledge
Knowledge can be linked to a customer, product, case, work order, conversation, file, or any other authorized `RecordId`. Related and contextual reads traverse the existing secured record graph within bounded depth and result limits. A relation improves context; it never grants access to either endpoint.
## Questions, feedback, and knowledge gaps
When a user or GoClaw cannot find authoritative evidence, Knowledge Core records a question instead of fabricating an answer. Repeated occurrences are grouped inside the same tenant and may be assigned, answered, closed, or converted into a normal **GeneratedCandidate** Draft for review.
Usage and feedback such as helpful, outdated, or incorrect remain linked to the visible item and context. The health view combines repeated questions, weak/no-evidence retrieval, negative feedback, stale high-use sources, overdue reviews, and missing owners into secured gap signals. Aggregate counts never reveal an inaccessible question or source.
## Secured AI, RAG, and citations
Only an explicitly Published and AI-eligible revision enters the canonical AI source, document-element, chunk, visual-index, and embedding pipeline. Tenant, `CanAiReadRecords`, DataRole, location, record, and field security are applied before model context is assembled and again when stored record identities are returned.
Answers retain the Knowledge item, immutable revision, authority, publication/review freshness, source, page, and visual evidence needed for a citation. Contextual record links can improve ranking, but relevance never grants access. Draft, archived, deleted, AI-excluded, or unauthorized material is absent from normal retrieval.
## Universal Inbox, GoClaw, and support learning
Universal Inbox remains the support attention layer. GoClaw must hold the current claim/lease and exact `ActivityVersion` before assembling secured support context. Customer-visible citations include only support-eligible sources the represented user may disclose; internal-only knowledge is omitted. Read-only diagnostics can describe current account state, while RAG explains how the system should work. Neither authorizes a mutation.
After a case is Resolved or Closed, the controlled learning path accepts structured symptom, product area, likely cause, diagnostic steps, verified resolution, and affected-version fields—not the raw transcript. The `support-resolution-v1` sanitizer removes credentials, secrets, PII, and tenant-specific identifiers. The result is an idempotent **SupportResolution** Draft with **GeneratedCandidate** authority, a source Inbox/version link, one owner, and a review Task. It remains unavailable to normal RAG until an authorized human reviews, marks it eligible, and publishes it.
## OpsAtlas — AI Operational Knowledge Core
Your company’s operating knowledge, mapped and available on demand.
OpsAtlas is the thirteenth Build This product pattern and a complete product experience built on Knowledge Core. It turns SOPs, policies, manuals, training, system documentation, and hard-won tribal knowledge into maintained operating memory for employees and permission-bound AI assistants.
[How to build OpsAtlas](../how-to-do-this/opsatlas.html) [Explore the OpsAtlas product concept](https://buildwithhq.com/buildthis/opsatlas.html)
### What OpsAtlas can do
- Ingest approved SOPs, policies, manuals, training material, system documentation, and tribal knowledge.
- Chunk, embed, version, and relate that knowledge while retaining source and revision evidence.
- Respect department, DataRole, location, record, field, and `CanAiReadRecords` permissions before retrieval or model context.
- Give employees evidence-backed answers to *How do we do this?* with exact sources, revisions, and freshness.
- Launch the appropriate BuildWithHQ workflow from an answer while preserving action permission, approval, and current-context checks.
- Give GoClaw agents and approved MCP tools the permitted operational context they need before they plan or act.
- Reduce repetitive internal questions and accelerate employee onboarding.
- Standardize procedures across teams and locations without flattening their access boundaries.
- Preserve institutional knowledge when employees, contractors, or specialists leave.
- Identify unanswered questions, weak answers, stale sources, negative feedback, and documentation gaps.
### The continuous knowledge flywheel
| Stage | What happens |
| --- | --- |
| **Capture** | Bring approved operating sources and reusable support knowledge into accountable collections. |
| **Understand** | Extract text and visual evidence, create revisions, relate business context, and build secured semantic indexes. |
| **Retrieve** | Return the strongest authorized current guidance with citations, freshness, and explicit gaps. |
| **Act + Improve** | Start governed workflows, collect usage and feedback, answer questions, and route Draft improvements to their owners. |
**Capture → Understand → Retrieve → Act + Improve** is continuous: each real question or support outcome can reveal what should be reviewed, clarified, or added next without automatically promoting private or generated material.
### OpsAtlas and CompanyIQ answer different questions
| Product pattern | Primary question | Focus |
| --- | --- | --- |
| **OpsAtlas** | How do we operate? | Current procedures, policy, training, operating guidance, evidence, and the governed workflow that follows. |
| **CompanyIQ** | What is happening in the business, and where is the opportunity? | Signals, company context, risks, opportunities, evidence-backed insights, and reviewable next actions. |
The two products can share BuildWithHQ’s secured records, relationships, AI retrieval, and action foundations. OpsAtlas organizes the business’s operating memory; CompanyIQ uses permitted business evidence to understand change and opportunity.
## Common uses
- Owned product documentation, policies, procedures, FAQs, and employee help.
- Customer-safe help centers and support answers with exact current citations.
- Contextual playbooks linked to customers, products, cases, work orders, files, and conversations.
- Permission-aware AI/RAG sources whose authority and freshness must be inspectable.
- A controlled support learning loop that turns reusable resolutions into reviewable Drafts.
## How it connects
Knowledge Core composes with Files and Web Links for source evidence, Record Relations for business context, Tasks and Universal Inbox for review/support attention, GoClaw for fenced assistance, and the existing AI ingestion/retrieval chain for cited answers. The Knowledge item and immutable revision remain the source of truth throughout.
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
Every list, item, relation, review, question, feedback, support-context, and AI path derives the SaaS app, tenant, user, DataRole, location, record, field, and permission scope from verified server identity. `CanAiReadRecords` is required before AI context. Publication, ownership, review, support learning, and archive actions have separate permission checks; customer-visible context excludes internal-only sources.
- 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 add the validated `module.knowledge-articles` block to the pages and existing user-type menus that need Knowledge Core.
2. Create collections around reader/support journeys and assign their audience, default owner, review policy, and existing DataRole/location visibility.
3. Create or migrate source items as Drafts, confirm their authority and AI/support eligibility, then publish through the reviewed lifecycle.
4. Link knowledge to relevant Records, configure the review worker/Tasks and Inbox attention, and enable GoClaw support context only for permitted users and queues.
5. Test Draft/Published/Archived visibility, revision restore, stale updates, owner review, questions/feedback, cross-tenant/location denial, AI-read denial, internal-source exclusion, exact citations, stale Inbox activity, and Draft support learning before publishing an exact app version.
### Explicit scope for create helpers
The native create helpers below require the user's current choice of one eligible DataRole with `isPersonal: false`. In a tenant-runtime component, render `RecordCreateScopeSelect allowAll allowPersonal={false}` from `./RecordCreateScopeSelect` and read its submitted form with `readRecordCreateScope`. Pass that choice to the helper; do not infer it from the page, menu, role count, or a default location. When active locations exist, explicitly choose an authorized location or All when permitted; Personal has no location. For a shared-only helper, validate the returned choice is shared and has a DataRole before calling it. The typed choice is a request, and SQL rechecks current authority. Child actions keep their reviewed parent scope. Ordinary edits preserve the loaded scope and revision; moving scope is a separate explicit action.
## Copy/paste the native knowledge page
The native block is the compatibility reader/editor for searchable articles, drafts, detail, dynamic fields, relationships, activity, and favorites. Paste it for the fastest page. Use the custom Knowledge Core workspace below when you need explicit lifecycle, ownership, reviews, questions, and health:
```
{
"blocks": [
{ "_id": "knowledge-library", "_type": "module.knowledge-articles", "props": {}, "children": [] }
]
}
```
### Custom unstyled Knowledge Core workspace
This uses the version 2 Knowledge Core contract. Search, lifecycle, type, location, and pagination are applied by `sp_kc_ListKnowledgeItemsSecured`; the component receives only its authorized page.
```
import { FormEvent, useEffect, useState } from "react";
import type { KnowledgeCoreDetail, KnowledgeCoreList, KnowledgeHealth, KnowledgeLifecycleStatus } from "@buildwithhq/module-sdk";
import { getKnowledgeCore, getKnowledgeHealth, listKnowledgeCore } from "./api";
const empty: KnowledgeCoreList = { contractVersion: 2, pageNumber: 1, pageSize: 50, totalRecords: 0, totalPages: 0, canCreate: false, canManage: false, items: [] };
export function KnowledgeCoreWorkspace({ locationId = "" }) {
const [draftSearch, setDraftSearch] = useState("");
const [search, setSearch] = useState("");
const [lifecycle, setLifecycle] = useState("");
const [knowledgeType, setKnowledgeType] = useState("");
const [page, setPage] = useState(1);
const [result, setResult] = useState(empty);
const [health, setHealth] = useState(null);
const [selected, setSelected] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
Promise.all([
listKnowledgeCore(search, lifecycle, knowledgeType, controller.signal, locationId, page, 50),
getKnowledgeHealth(controller.signal),
]).then(([items, status]) => { setResult(items); setHealth(status); })
.catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "Knowledge could not be loaded."); });
return () => controller.abort();
}, [knowledgeType, lifecycle, locationId, page, search]);
function submit(event: FormEvent) { event.preventDefault(); setPage(1); setSearch(draftSearch.trim()); }
async function open(recordId: string) { setSelected(await getKnowledgeCore(recordId)); }
return
Knowledge Core
{health &&
{health.publishedCount} published - {health.reviewDueCount} reviews due - {health.openQuestionCount} open questions - {health.staleCount} stale
}
{error &&
{error}
}
{result.items.map(item => )}
{selected &&
{selected.title}
{selected.summary}
{selected.articleBody}
{selected.lifecycleStatus} - revision {selected.currentRevisionNumber} - {selected.freshnessStatus}}
;
}
```
### Draft, revise, publish, archive, and restore
Every edit creates immutable revision evidence. Update, publish, archive, and restore use the latest `updatedUtc`; after every mutation re-fetch detail before enabling another action. Restoring an old revision creates a new Draft—it does not erase later history.
```
import type { KnowledgeCoreDetail, KnowledgeCoreWrite } from "@buildwithhq/module-sdk";
import { archiveKnowledgeCore, createKnowledgeCore, getKnowledgeCore, publishKnowledgeCore, restoreKnowledgeCoreRevision, updateKnowledgeCore } from "./api";
import type { RecordCreateScope } from "./RecordCreateScopeSelect";
type SharedScope = RecordCreateScope & { isPersonal: false; dataRoleId: number };
export const knowledgeReplacement = (current: KnowledgeCoreDetail): KnowledgeCoreWrite => ({
expectedUpdatedUtc: current.updatedUtc, slug: current.slug, title: current.title,
summary: current.summary || "", articleBody: current.articleBody || "", category: current.category || "",
knowledgeType: current.knowledgeType, authorityLevel: current.authorityLevel,
locationId: current.locationId || undefined, reviewPolicyId: current.reviewPolicyId || undefined,
isAiEligible: current.isAiEligible, isSupportEligible: current.isSupportEligible,
});
export function createRunbookDraft(chosenScope: SharedScope) {
return createKnowledgeCore({ ...chosenScope, slug: "incident-runbook", title: "Incident runbook", articleBody: "Draft steps", knowledgeType: "Procedure", authorityLevel: "Reference", isAiEligible: false, isSupportEligible: false, changeNote: "Initial draft" });
}
export async function revise(current: KnowledgeCoreDetail, articleBody: string) {
await updateKnowledgeCore(current.recordId, { ...knowledgeReplacement(current), articleBody, changeNote: "Reviewed procedure" });
return getKnowledgeCore(current.recordId);
}
export async function publish(current: KnowledgeCoreDetail) {
if (!current.canPublish) throw new Error("The signed-in user cannot publish this item.");
await publishKnowledgeCore(current.recordId, current.updatedUtc, "Approved for publication");
return getKnowledgeCore(current.recordId);
}
export async function archive(current: KnowledgeCoreDetail) {
await archiveKnowledgeCore(current.recordId, current.updatedUtc, "Retired guidance");
return getKnowledgeCore(current.recordId);
}
export async function restoreAsDraft(current: KnowledgeCoreDetail, knowledgeRevisionId: string) {
await restoreKnowledgeCoreRevision(current.recordId, knowledgeRevisionId, current.updatedUtc, "Restore for review");
return getKnowledgeCore(current.recordId);
}
```
### Collections, accountable ownership, and review signoff
One primary owner may be an active tenant user or a DataRole—not both. Assigning ownership never grants record access. Review actions preserve their own optimistic-concurrency timestamp and create/advance the configured Task/Inbox attention.
```
import type { KnowledgeCollection, KnowledgeReviewCycle } from "@buildwithhq/module-sdk";
import { addKnowledgeCoreCollectionItem, beginKnowledgeReview, completeKnowledgeReview, createKnowledgeCoreCollection, getKnowledgeCoreOwners, listKnowledgeCoreCollections, listKnowledgeReviews, reassignKnowledgeReview, removeKnowledgeCoreCollectionItem, removeKnowledgeCoreOwner, setKnowledgeCoreOwner, updateKnowledgeCoreCollection } from "./api";
export const createOperationsCollection = () => createKnowledgeCoreCollection({ name: "Operations", description: "Approved operating procedures", isSupportCollection: true, sortOrder: 10 });
export const renameCollection = (current: KnowledgeCollection, name: string) => updateKnowledgeCoreCollection(current.collectionId, {
expectedUpdatedUtc: current.updatedUtc, name, description: current.description || "",
parentCollectionId: current.parentCollectionId || undefined, defaultReviewPolicyId: current.defaultReviewPolicyId || undefined,
isSupportCollection: current.isSupportCollection, sortOrder: current.sortOrder, isActive: true,
});
export const addToCollection = (collectionId: string, recordId: string) => addKnowledgeCoreCollectionItem(collectionId, recordId, 0);
export const removeFromCollection = (collectionId: string, recordId: string) => removeKnowledgeCoreCollectionItem(collectionId, recordId);
export const makeUserPrimaryOwner = (recordId: string, ownerUserId: string) => setKnowledgeCoreOwner(recordId, { ownerUserId, responsibilityType: "Owner", isPrimary: true });
export const makeRolePrimaryOwner = (recordId: string, dataRoleId: number) => setKnowledgeCoreOwner(recordId, { dataRoleId, responsibilityType: "Owner", isPrimary: true });
export const removeOwner = (knowledgeOwnerId: string) => removeKnowledgeCoreOwner(knowledgeOwnerId);
export async function confirmCurrent(review: KnowledgeReviewCycle) {
const begun = await beginKnowledgeReview(review.reviewCycleId, review.updatedUtc);
if (!begun.updatedUtc) throw new Error("Review did not return its concurrency token.");
return completeKnowledgeReview(review.reviewCycleId, begun.updatedUtc, "ConfirmedCurrent", "Content remains current");
}
export const reassignReviewToRole = (review: KnowledgeReviewCycle, assignedDataRoleId: number) => reassignKnowledgeReview(review.reviewCycleId, review.updatedUtc, { assignedDataRoleId });
export const loadGovernance = () => Promise.all([listKnowledgeCoreCollections(), listKnowledgeReviews()]);
export const loadOwners = (recordId: string) => getKnowledgeCoreOwners(recordId);
```
### Questions, feedback, and measurable gaps
When evidence is missing, record a question instead of generating a confident answer. Repeated questions group inside the same tenant. Answer against an authorized Knowledge record or convert the question into a GeneratedCandidate Draft; it still requires human review and publication before normal RAG can use it.
```
import type { KnowledgeQuestion } from "@buildwithhq/module-sdk";
import { answerKnowledgeQuestion, assignKnowledgeQuestion, convertKnowledgeQuestion, createKnowledgeQuestion, getKnowledgeFeedback, getKnowledgeGaps, listKnowledgeQuestions, recordKnowledgeFeedback } from "./api";
import type { RecordCreateScope } from "./RecordCreateScopeSelect";
type SharedScope = RecordCreateScope & { isPersonal: false; dataRoleId: number };
export const ask = (questionText: string, contextRecordId?: string) => createKnowledgeQuestion(questionText, contextRecordId);
export const assignQuestionToRole = (question: KnowledgeQuestion, assignedDataRoleId: number) => assignKnowledgeQuestion(question.knowledgeQuestionId, question.updatedUtc, { assignedDataRoleId });
export const answerFromKnowledge = (question: KnowledgeQuestion, answerText: string, answerKnowledgeRecordId: string) => answerKnowledgeQuestion(question.knowledgeQuestionId, question.updatedUtc, answerText, answerKnowledgeRecordId);
export const convertToReviewedDraft = (question: KnowledgeQuestion, chosenScope: SharedScope) => convertKnowledgeQuestion(question.knowledgeQuestionId, { ...chosenScope, expectedUpdatedUtc: question.updatedUtc, slug: `question-${question.knowledgeQuestionId}`, title: question.questionText, knowledgeType: "FAQ", authorityLevel: "GeneratedCandidate" });
export const markHelpful = (recordId: string, contextRecordId?: string) => recordKnowledgeFeedback(recordId, "Helpful", undefined, contextRecordId);
export const loadImprovementQueue = (page = 1) => Promise.all([listKnowledgeQuestions("Open", undefined, page, 50), getKnowledgeGaps(undefined, 90, 50)]);
export const loadFeedback = (recordId: string) => getKnowledgeFeedback(recordId);
```
### Optional starter styling
```
.knowledge-core { width: 100%; max-width: none; }
.knowledge-core form, .knowledge-core nav { align-items: end; display: flex; gap: .75rem; }
.knowledge-core ol { list-style: none; margin: 1rem 0; padding: 0; }
.knowledge-core li button { background: transparent; border: 0; display: grid; gap: .2rem; padding: .75rem 0; text-align: left; width: 100%; }
.knowledge-core article { border: 1px solid var(--line, #d8dee8); margin-top: 1rem; padding: 1rem; white-space: pre-wrap; }
```
### Exact lifecycle and supporting routes
| Area | Routes | Canonical procedures |
| --- | --- | --- |
| Items | `GET/POST /api/modules/knowledge-core`; `GET/PUT /{recordId}` | `sp_kc_ListKnowledgeItemsSecured`, `sp_kc_GetKnowledgeItemSecured`, create/update |
| Lifecycle/revisions | `POST /{recordId}/publish`, `/archive`, `/restore`; `GET /revisions` | Publish, archive, immutable history, restore-as-Draft |
| Collections/owners | `/collections`, `/{recordId}/owners`, `/unowned` | `sp_kc_ListCollectionsSecured`, collection membership, secured owner operations |
| Reviews/health | `/reviews`, `/health` | Due-review lifecycle and secured health aggregation |
| Questions/feedback/gaps | `/questions`, `/{recordId}/feedback`, `/gaps` | Evidence-preserving improvement loop |
Lists are bounded to 200 rows per page. AI/RAG eligibility does not grant access: only Published, explicitly eligible content that survives tenant, DataRole, location, record, field, and `CanAiReadRecords` checks enters model context. Generated or support-derived candidates remain Draft until a permitted human publishes them.
## Build a professional Knowledge Core dashboard
These six registry-backed presentation blocks let a designer turn the secured Knowledge Core 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/knowledge-articles`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/knowledge-core-sample.json`, its executable authenticated page at `pages/first-class/knowledge-core-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 Knowledge Core projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Published, draft, due-review, stale, and gap counts |
| `entity-list` | `core.entity-list` | Important articles, owners, freshness, and use |
| `progress-list` | `core.progress-list` | Distribution by freshness, lifecycle, category, or owner |
| `series-chart` | `core.series-chart` | Draft, publish, review, use, and gap activity |
| `data-grid` | `core.presentation-grid` | One bounded article page with lifecycle evidence |
| `timeline` | `core.timeline` | Revision, publication, review, retrieval, feedback, gap, and archive |
### 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": "knowledge-core-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Knowledge operations",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "published",
"label": "Published",
"value": 684,
"format": "number",
"tone": "success"
},
{
"key": "review",
"label": "Review due",
"value": 31,
"format": "number",
"tone": "warning"
},
{
"key": "draft",
"label": "Drafts",
"value": 24,
"format": "number",
"tone": "neutral"
},
{
"key": "gaps",
"label": "Open gaps",
"value": 12,
"format": "number",
"tone": "danger"
}
]
},
"children": []
},
{
"_id": "knowledge-core-recent",
"_type": "core.entity-list",
"props": {
"title": "Knowledge needing attention",
"hasMore": true,
"items": [
{
"id": "knowledge-core-sample-1",
"recordId": "knowledge-core-record-1",
"primary": "Escalation playbook",
"secondary": "Support operations - owner: Maya Chen",
"status": {
"key": "review-due",
"label": "Review due",
"tone": "warning"
},
"trailing": "14 uses"
},
{
"id": "knowledge-core-sample-2",
"recordId": "knowledge-core-record-2",
"primary": "Escalation playbook - Follow-up",
"secondary": "Support operations - owner: Maya Chen - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "knowledge-core-sample-3",
"recordId": "knowledge-core-record-3",
"primary": "Escalation playbook - West region",
"secondary": "Support operations - owner: Maya Chen - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "knowledge-core-sample-4",
"recordId": "knowledge-core-record-4",
"primary": "Escalation playbook - Customer response",
"secondary": "Support operations - owner: Maya Chen - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "knowledge-core-sample-5",
"recordId": "knowledge-core-record-5",
"primary": "Escalation playbook - Regional operations review with a deliberately long title",
"secondary": "Support operations - owner: Maya Chen - 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": "knowledge-core-sample-6",
"recordId": "knowledge-core-record-6",
"primary": "Escalation playbook - Completed preview",
"secondary": "Support operations - owner: Maya Chen - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "knowledge-core-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Knowledge freshness",
"items": [
{
"key": "current",
"label": "Current",
"value": 611,
"maximum": 684,
"displayValue": "611",
"tone": "success",
"status": {
"key": "current",
"label": "Current",
"tone": "success"
}
},
{
"key": "due",
"label": "Due soon",
"value": 42,
"maximum": 684,
"displayValue": "42",
"tone": "warning",
"status": {
"key": "due",
"label": "Due soon",
"tone": "warning"
}
},
{
"key": "stale",
"label": "Stale",
"value": 31,
"maximum": 684,
"displayValue": "31",
"tone": "danger",
"status": {
"key": "stale",
"label": "Stale",
"tone": "danger"
}
}
]
},
"children": []
},
{
"_id": "knowledge-core-trend",
"_type": "core.series-chart",
"props": {
"title": "Knowledge lifecycle",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Published",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Reviewed",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "knowledge-core-table",
"_type": "core.presentation-grid",
"props": {
"title": "Authorized knowledge",
"columns": [
{
"key": "article",
"label": "Article",
"type": "text",
"align": "left"
},
{
"key": "owner",
"label": "Owner",
"type": "text",
"align": "left"
},
{
"key": "freshness",
"label": "Freshness",
"type": "status",
"align": "left"
},
{
"key": "uses",
"label": "Uses",
"type": "number",
"align": "right"
},
{
"key": "review",
"label": "Next review",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "knowledge-core-row-1",
"recordId": "knowledge-core-record-1",
"cells": {
"article": "Escalation playbook",
"owner": "Maya Chen",
"freshness": {
"key": "due",
"label": "Review due",
"tone": "warning"
},
"uses": 14,
"review": "2026-09-10T00:00:00Z"
}
},
{
"id": "knowledge-core-row-2",
"recordId": "knowledge-core-record-2",
"cells": {
"article": "Escalation playbook - Follow-up",
"owner": "Maya Chen",
"freshness": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"uses": 15,
"review": "2026-09-04T15:42:00Z"
}
},
{
"id": "knowledge-core-row-3",
"recordId": "knowledge-core-record-3",
"cells": {
"article": "Escalation playbook - West region",
"owner": "Maya Chen",
"freshness": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"uses": 16,
"review": "2026-09-04T12:18:00Z"
}
},
{
"id": "knowledge-core-row-4",
"recordId": "knowledge-core-record-4",
"cells": {
"article": "Escalation playbook - Customer response",
"owner": "Maya Chen",
"freshness": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"uses": 17,
"review": "2026-09-03T21:07:00Z"
}
},
{
"id": "knowledge-core-row-5",
"recordId": "knowledge-core-record-5",
"cells": {
"article": "Escalation playbook - Regional operations review with a deliberately long title",
"owner": "Maya Chen - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"freshness": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"uses": 18,
"review": "2026-09-03T16:31:00Z"
}
},
{
"id": "knowledge-core-row-6",
"recordId": "knowledge-core-record-6",
"cells": {
"article": "Escalation playbook - Completed preview",
"owner": "Maya Chen",
"freshness": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"uses": 19,
"review": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 684,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "knowledge-core-timeline",
"_type": "core.timeline",
"props": {
"title": "Knowledge activity",
"hasMore": true,
"items": [
{
"id": "knowledge-core-event-1",
"recordId": "knowledge-core-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Review requested",
"description": "Escalation playbook entered its scheduled review window.",
"actor": "Knowledge Core",
"tone": "warning"
},
{
"id": "knowledge-core-event-2",
"recordId": "knowledge-core-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Review requested - Follow-up",
"description": "Escalation playbook entered its scheduled review window. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "knowledge-core-event-3",
"recordId": "knowledge-core-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Review requested - West region",
"description": "Escalation playbook entered its scheduled review window. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "knowledge-core-event-4",
"recordId": "knowledge-core-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Review requested - Customer response",
"description": "Escalation playbook entered its scheduled review window. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "knowledge-core-event-5",
"recordId": "knowledge-core-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Review requested - Regional operations review with a deliberately long title",
"description": "Escalation playbook entered its scheduled review window. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "knowledge-core-event-6",
"recordId": "knowledge-core-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Review requested - Completed preview",
"description": "Escalation playbook entered its scheduled review window. 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 KnowledgeCorePresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface KnowledgeCorePresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: KnowledgeCorePresentationData | 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 KnowledgeCorePresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: KnowledgeCorePresentationProps) {
if (error) return
The Knowledge Core presentation could not be loaded.
;
if (loading || !data) return
Loading Knowledge Core presentation...
;
return (
Knowledge Core
Knowledge operations
Published knowledge, review freshness, ownership, gaps, retrieval use, and lifecycle evidence.
);
}
```
### 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/knowledge-articles`, 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": "knowledge-core-live",
"_type": "module.knowledge-articles",
"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 a few high-value collections, one accountable owner per item, and a practical review interval. Mark content AI-eligible only when its audience, authority, source, and freshness are suitable for model context; missing evidence should create a question, not a confident guess.
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/knowledge-core.html).
---
title: "Web links"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/web-links.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Web links
Maintains searchable, categorized, location-aware external links with descriptions, custom fields, relationships, activity, and favorites.
## In brief
- **Runtime key:** module.web-links
- **Experience:** Curated external resources
- **Security:** Server-enforced
Use Web Links to curate external tools, reference sites, customer resources, vendor portals, or research alongside the records and teams that use them.
[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.web-links. 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
Maintains searchable, categorized, location-aware external links with descriptions, custom fields, relationships, activity, and favorites.
## Key capabilities
- Search links and filter them by category.
- Create and edit title, URL, description, category, and custom fields.
- Open only contract-validated HTTP or HTTPS destinations in an isolated browsing context.
- Relate and favorite links while preserving location-aware access.
## Common uses
- Team resource directories.
- Customer/vendor portals and external documentation.
- Research links connected to cases, projects, accounts, or knowledge.
## How it connects
Links can relate to Knowledge Articles, Contacts, projects, and dynamic records. They store the reference and business context, not credentials or an embedded authenticated browser session.
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 strict host-based HTTP(S) URLs without embedded credentials. The runtime opens destinations with an isolated external context and applies normal tenant, DataRole, and location checks to the link record.
- 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.web-links` 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.
### Explicit scope for create helpers
The native create helpers below require the user's current choice of Personal or one eligible DataRole. In a tenant-runtime component, render `RecordCreateScopeSelect allowAll` from `./RecordCreateScopeSelect` and read its submitted form with `readRecordCreateScope`. Pass that choice to the helper; do not infer it from the page, menu, role count, or a default location. When active locations exist, explicitly choose an authorized location or All when permitted; Personal has no location. For a shared-only helper, validate the returned choice is shared and has a DataRole before calling it. The typed choice is a request, and SQL rechecks current authority. Child actions keep their reviewed parent scope. Ordinary edits preserve the loaded scope and revision; moving scope is a separate explicit action.
## Copy/paste the Web Links page
The native block includes secured search/category browsing, create/edit, detail, custom fields, relation/activity counts, isolated external opening, and favorites:
```
{
"blocks": [
{ "_id": "resource-links", "_type": "module.web-links", "props": {}, "children": [] }
]
}
```
### Custom unstyled links directory
```
import { FormEvent, useEffect, useState } from "react";
import type { WebLinkDetail, WebLinksList } from "@buildwithhq/module-sdk";
import { getWebLink, listWebLinks } from "./api";
const empty: WebLinksList = { contractVersion: 1, pageNumber: 1, pageSize: 50, totalRecords: 0, totalPages: 0, canCreate: false, canEdit: false, items: [] };
export function UnstyledWebLinks({ category = "", locationId = "" }) {
const [draft, setDraft] = useState(""); const [search, setSearch] = useState(""); const [page, setPage] = useState(1);
const [result, setResult] = useState(empty); const [selected, setSelected] = useState(null);
useEffect(() => { const controller = new AbortController(); listWebLinks(search, category, controller.signal, locationId, page, 50).then(setResult); return () => controller.abort(); }, [category, locationId, page, search]);
function submit(event: FormEvent) { event.preventDefault(); setPage(1); setSearch(draft.trim()); }
async function open(recordId: string) { setSelected(await getWebLink(recordId)); }
return
Resources
{result.items.map(link => )}
{selected &&
{selected.title}
{selected.description}
Open external resource}
;
}
```
### Create, replace, and favorite links
Only absolute HTTP/HTTPS URLs with a real host and no embedded username/password are accepted. Use a managed connector for credentials. Updates are full optimistic replacements.
```
import type { WebLinkDetail, WebLinkDynamicField } from "@buildwithhq/module-sdk";
import { createWebLink, getWebLink, setWebLinkFavorite, updateWebLink } from "./api";
import type { RecordCreateScope } from "./RecordCreateScopeSelect";
const fields = (current: WebLinkDetail) => current.dynamicFields.map((field: WebLinkDynamicField) => ({ fieldKey: field.fieldKey, valueText: field.valueText ?? undefined, valueInt: field.valueInt ?? undefined, valueDecimal: field.valueDecimal ?? undefined, valueDateTime: field.valueDateTime ?? undefined, valueBool: field.valueBool ?? undefined, valueGuid: field.valueGuid ?? undefined, valueJson: field.valueJson }));
export const addDocumentation = (chosenScope: RecordCreateScope) => createWebLink({ ...chosenScope, title: "Product documentation", url: "https://docs.example.com/", category: "Reference", description: "Public product documentation" });
export async function renameLink(current: WebLinkDetail, title: string) { await updateWebLink(current.recordId, { expectedUpdatedUtc: current.updatedUtc, title, url: current.url, description: current.description || "", category: current.category || "", locationId: current.locationId || undefined, dynamicFields: fields(current) }); return getWebLink(current.recordId); }
export async function toggleLinkFavorite(current: WebLinkDetail) { await setWebLinkFavorite(current.recordId, !current.isFavorite); return getWebLink(current.recordId); }
```
### Optional styling
```
.custom-links { width: 100%; max-width: none; }
.custom-links form, .custom-links nav { display: flex; gap: .75rem; }
.custom-links ul { list-style: none; margin: 1rem 0; padding: 0; }
.custom-links li button { background: transparent; border: 0; display: grid; padding: .75rem 0; text-align: left; width: 100%; }
.custom-links article { border: 1px solid var(--line, #d8dee8); margin-top: 1rem; padding: 1rem; }
```
### Exact data path
`GET /api/modules/web-links` and `GET /{recordId}` use `sp_WebLinks_ListSecured` and `sp_WebLinks_GetSecured`. `POST`, `PUT /{recordId}`, and `PUT /{recordId}/favorite` use the reviewed create, update, and favorite procedures. Pages are capped at 200 rows; URL is capped at 1,000 characters, title at 300, and dynamic fields at 200.
## Build a professional Web links dashboard
These six registry-backed presentation blocks let a designer turn the secured Web links 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/web-links`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/web-links-sample.json`, its executable authenticated page at `pages/first-class/web-links-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 Web links projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Active, new, favorite, and review-due counts |
| `entity-list` | `core.entity-list` | Recently added or maintained authorized links |
| `progress-list` | `core.progress-list` | Distribution by category, owner, or state |
| `series-chart` | `core.series-chart` | Link creation and maintenance over time |
| `data-grid` | `core.presentation-grid` | One bounded link page with safe display URLs |
| `timeline` | `core.timeline` | Create, edit, favorite, relate, review, 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": "web-links-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Web link library",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "active",
"label": "Active links",
"value": 238,
"format": "number",
"tone": "success"
},
{
"key": "new",
"label": "Added this month",
"value": 17,
"format": "number",
"tone": "primary"
},
{
"key": "favorites",
"label": "Your favorites",
"value": 14,
"format": "number",
"tone": "warning"
},
{
"key": "review",
"label": "Review due",
"value": 8,
"format": "number",
"tone": "danger"
}
]
},
"children": []
},
{
"_id": "web-links-recent",
"_type": "core.entity-list",
"props": {
"title": "Recently updated links",
"hasMore": true,
"items": [
{
"id": "web-links-sample-1",
"recordId": "web-links-record-1",
"primary": "Field safety portal",
"secondary": "Operations - docs.example.test",
"status": {
"key": "active",
"label": "Active",
"tone": "success"
},
"trailing": "Favorite"
},
{
"id": "web-links-sample-2",
"recordId": "web-links-record-2",
"primary": "Field safety portal - Follow-up",
"secondary": "Operations - docs.example.test - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "web-links-sample-3",
"recordId": "web-links-record-3",
"primary": "Field safety portal - West region",
"secondary": "Operations - docs.example.test - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "web-links-sample-4",
"recordId": "web-links-record-4",
"primary": "Field safety portal - Customer response",
"secondary": "Operations - docs.example.test - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "web-links-sample-5",
"recordId": "web-links-record-5",
"primary": "Field safety portal - Regional operations review with a deliberately long title",
"secondary": "Operations - docs.example.test - 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": "web-links-sample-6",
"recordId": "web-links-record-6",
"primary": "Field safety portal - Completed preview",
"secondary": "Operations - docs.example.test - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "web-links-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Links by category",
"items": [
{
"key": "operations",
"label": "Operations",
"value": 96,
"maximum": 238,
"displayValue": "96",
"tone": "primary",
"status": {
"key": "operations",
"label": "Operations",
"tone": "primary"
}
},
{
"key": "sales",
"label": "Sales",
"value": 74,
"maximum": 238,
"displayValue": "74",
"tone": "success",
"status": {
"key": "sales",
"label": "Sales",
"tone": "success"
}
},
{
"key": "support",
"label": "Support",
"value": 68,
"maximum": 238,
"displayValue": "68",
"tone": "warning",
"status": {
"key": "support",
"label": "Support",
"tone": "warning"
}
}
]
},
"children": []
},
{
"_id": "web-links-trend",
"_type": "core.series-chart",
"props": {
"title": "Link maintenance",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Added",
"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": "web-links-table",
"_type": "core.presentation-grid",
"props": {
"title": "Authorized web links",
"columns": [
{
"key": "name",
"label": "Link",
"type": "text",
"align": "left"
},
{
"key": "host",
"label": "Host",
"type": "text",
"align": "left"
},
{
"key": "category",
"label": "Category",
"type": "text",
"align": "left"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "updated",
"label": "Updated",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "web-links-row-1",
"recordId": "web-links-record-1",
"cells": {
"name": "Field safety portal",
"host": "docs.example.test",
"category": "Operations",
"status": {
"key": "active",
"label": "Active",
"tone": "success"
},
"updated": "2026-09-04T13:20:00Z"
}
},
{
"id": "web-links-row-2",
"recordId": "web-links-record-2",
"cells": {
"name": "Field safety portal - Follow-up",
"host": "docs.example.test",
"category": "Operations",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"updated": "2026-09-04T15:42:00Z"
}
},
{
"id": "web-links-row-3",
"recordId": "web-links-record-3",
"cells": {
"name": "Field safety portal - West region",
"host": "docs.example.test",
"category": "Operations",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"updated": "2026-09-04T12:18:00Z"
}
},
{
"id": "web-links-row-4",
"recordId": "web-links-record-4",
"cells": {
"name": "Field safety portal - Customer response",
"host": "docs.example.test",
"category": "Operations",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"updated": "2026-09-03T21:07:00Z"
}
},
{
"id": "web-links-row-5",
"recordId": "web-links-record-5",
"cells": {
"name": "Field safety portal - Regional operations review with a deliberately long title",
"host": "docs.example.test - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"category": "Operations",
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"updated": "2026-09-03T16:31:00Z"
}
},
{
"id": "web-links-row-6",
"recordId": "web-links-record-6",
"cells": {
"name": "Field safety portal - Completed preview",
"host": "docs.example.test",
"category": "Operations",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"updated": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 238,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "web-links-timeline",
"_type": "core.timeline",
"props": {
"title": "Link activity",
"hasMore": true,
"items": [
{
"id": "web-links-event-1",
"recordId": "web-links-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Web link updated",
"description": "The Field safety portal description was refreshed.",
"actor": "Jordan Lee",
"tone": "neutral"
},
{
"id": "web-links-event-2",
"recordId": "web-links-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Web link updated - Follow-up",
"description": "The Field safety portal description was refreshed. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "web-links-event-3",
"recordId": "web-links-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Web link updated - West region",
"description": "The Field safety portal description was refreshed. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "web-links-event-4",
"recordId": "web-links-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Web link updated - Customer response",
"description": "The Field safety portal description was refreshed. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "web-links-event-5",
"recordId": "web-links-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Web link updated - Regional operations review with a deliberately long title",
"description": "The Field safety portal description was refreshed. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "web-links-event-6",
"recordId": "web-links-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Web link updated - Completed preview",
"description": "The Field safety portal description was refreshed. 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 WebLinksPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface WebLinksPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: WebLinksPresentationData | 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 WebLinksPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: WebLinksPresentationProps) {
if (error) return
The Web links presentation could not be loaded.
;
if (loading || !data) return
Loading Web links presentation...
;
return (
Web links
Web link library
Authorized links, categories, ownership, favorites, and recent maintenance.
);
}
```
### 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/web-links`, 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": "web-links-live",
"_type": "module.web-links",
"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
Store the public destination only. If an integration needs OAuth or an API secret, use a managed connector instead of putting credentials in a URL.
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/web-links.html).
---
title: "Chat rooms"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/chat-rooms.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Chat rooms
Provides secured team rooms with messages, unread state, membership, room administration, mute settings, reactions, archives, custom fields, relations, activity, and favorites.
## In brief
- **Runtime key:** module.chat-rooms
- **Experience:** Members-only team discussion
- **Security:** Server-enforced
Use Chat Rooms for ongoing team conversation where membership, unread state, lightweight reactions, and room administration matter more than a formal topic hierarchy.
[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.chat-rooms. 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
Provides secured team rooms with messages, unread state, membership, room administration, mute settings, reactions, archives, custom fields, relations, activity, and favorites.
## Key capabilities
- Search active or archived rooms and see unread counts.
- Create/edit rooms, post messages, and react with bounded reaction types.
- Manage members, room administrators, mute state, and archive state when authorized.
- Show location, dynamic fields, related records, activity, and favorites.
## Common uses
- Project or operations team chat.
- Location, department, incident, or customer team rooms.
- Informal collaboration adjacent to records that retain the formal work state.
## How it connects
Rooms can relate to projects, customers, incidents, Files, and other Records. Use Conversations for durable customer/channel threads and Discussion Boards for structured topics; Chat Rooms remain the fast team discussion surface.
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
Rooms are shared-only: explicitly select one eligible DataRole and an authorized location or All when available. Room and message reads require current membership plus DataRole/location access, including Graph, Activity, Favorites and direct record links. Native Chat Rooms search remains available; general and AI search exclude chat content. Saves recheck current authority. Member administration, room edits and archiving require the appropriate room capability; members can post and edit their own messages without gaining room-administration rights.
- 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.chat-rooms` 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 the Chat Rooms page
The native block includes rooms, unread state, create/edit/archive, membership and mute administration, messages, replies, file references, reactions, custom fields, relations, activity, and favorites. Each action remains subject to the current actor's record and room permissions.
**Availability limits:** the current native composer sends no mentions; accepting mention IDs in a message contract does not establish mention selection or notification delivery. Attachment upload controls exist, but attachment metadata/content authorization and delivery need their own acceptance. Generic search, AI, graph and export room-membership coverage also remains under security review; this page is not certification of those paths.
```
{
"blocks": [
{ "_id": "team-chat", "_type": "module.chat-rooms", "props": {}, "children": [] }
]
}
```
### Custom unstyled room and message view
Opening a room deliberately marks its latest visible message read for the current user. The server still applies record and membership scope before returning content.
```
import { FormEvent, useEffect, useState } from "react";
import type { ChatRoomDetail, ChatRoomsList } from "@buildwithhq/module-sdk";
import { getChatRoom, listChatRooms, markChatRoomRead, postChatMessage } from "./api";
const empty: ChatRoomsList = { contractVersion: 1, pageNumber: 1, pageSize: 50, totalRecords: 0, totalPages: 0, canCreate: false, items: [] };
export function UnstyledChatRooms({ locationId = "" }) {
const [draft, setDraft] = useState(""); const [search, setSearch] = useState(""); const [page, setPage] = useState(1);
const [rooms, setRooms] = useState(empty); const [selected, setSelected] = useState(null);
useEffect(() => { const controller = new AbortController(); listChatRooms(search, false, controller.signal, locationId, page, 50).then(setRooms); return () => controller.abort(); }, [locationId, page, search]);
async function open(recordId: string) { const detail = await getChatRoom(recordId); setSelected(detail); await markChatRoomRead(recordId); }
async function send(event: FormEvent) { event.preventDefault(); if (!selected?.canPost) return; const form = event.currentTarget; const text = String(new FormData(form).get("message") || ""); await postChatMessage(selected.recordId, { messageText: text, mentionedUserIds: [] }); form.reset(); setSelected(await getChatRoom(selected.recordId)); }
return
)}{selected.canPost && }}
;
}
```
### Create, administer, react, and attach proof
Room replacement uses `expectedUpdatedUtc`. Personal scope is not available for Chat Rooms. Creation requires an explicit authorized DataRole choice with `isPersonal: false`. When active locations exist, select one authorized location or explicitly choose All if you have all-location access. Omit the location choice only when no active locations exist. Never derive these choices from a menu, sole role or default. The server rechecks authority when saving. Member changes require room administration; a tenant-user ID is a target and is revalidated. The message contract accepts up to 50 unique mention IDs and one File ID; this does not establish mention notification or attachment delivery acceptance. Reactions are exactly `like`, `love`, `celebrate`, `insightful`, or `ack`.
```
import type { ChatRoomDetail } from "@buildwithhq/module-sdk";
import { createChatRoom, getChatRoom, postChatMessage, setChatReaction, setChatRoomFavorite, setChatRoomMember, updateChatRoom } from "./api";
const roomFields = (room: ChatRoomDetail) => room.dynamicFields.map(field => ({ fieldKey: field.fieldKey, valueText: field.valueText, valueInt: field.valueInt, valueDecimal: field.valueDecimal, valueDateTime: field.valueDateTime, valueBool: field.valueBool, valueGuid: field.valueGuid, valueJson: field.valueJson }));
// Collect this from the secured scope selector; do not supply inferred defaults.
type ChosenRoomScope = { isPersonal: false; dataRoleId: number } & (
| { locationScope: "Specific"; locationId: string }
| { locationScope: "All"; locationId?: never }
| { locationScope?: never; locationId?: never }
);
export const createProjectRoom = (chosenScope: ChosenRoomScope) => createChatRoom({ roomName: "Project team", description: "Daily coordination", roomType: 0, ...chosenScope, dynamicFields: [] });
export async function archiveRoom(room: ChatRoomDetail) { await updateChatRoom(room.recordId, { expectedUpdatedUtc: room.updatedUtc, roomName: room.roomName, description: room.description || "", roomType: room.roomType, isArchived: true, locationId: room.locationId || undefined, dynamicFields: roomFields(room) }); return getChatRoom(room.recordId); }
export async function addMember(room: ChatRoomDetail, memberUserId: string, isAdmin = false) { await setChatRoomMember(room.recordId, memberUserId, { isMember: true, isAdmin, isMuted: false }); return getChatRoom(room.recordId); }
export async function attachMessage(room: ChatRoomDetail, messageText: string, attachedFileId: string) { await postChatMessage(room.recordId, { messageText, attachedFileId, mentionedUserIds: [] }); return getChatRoom(room.recordId); }
export async function acknowledge(room: ChatRoomDetail, chatMessageId: string, isActive = true) { await setChatReaction(room.recordId, chatMessageId, "ack", isActive); return getChatRoom(room.recordId); }
export async function toggleRoomFavorite(room: ChatRoomDetail) { await setChatRoomFavorite(room.recordId, !room.isFavorite); return getChatRoom(room.recordId); }
```
### Optional styling
```
.custom-chat { width: 100%; max-width: none; }
.custom-chat form, .custom-chat nav { display: flex; gap: .75rem; }
.custom-chat > ul { list-style: none; margin: 1rem 0; padding: 0; }
.custom-chat > ul button { background: transparent; border: 0; display: grid; padding: .75rem 0; text-align: left; width: 100%; }
.custom-chat article { border: 1px solid var(--line, #d8dee8); margin-top: 1rem; padding: 1rem; }
```
### Exact data path
Room administrators can add users who already have the required active DataRole and location access. An eligible user does not gain access until membership is added; removing membership takes effect on subsequent reads and saves, including an existing signed-in session. A stale message revision is rejected; reload the current message before deciding whether to save again.
Removing a room soft-deletes the room and its active messages while retaining their history. Where retention and restore are enabled, an authorized restore brings back messages hidden by that room removal. Messages removed individually remain removed. Use Deleted items for the supported restore action.
`GET/POST /api/modules/chat-rooms`, `GET/PUT /{recordId}`, `POST /{recordId}/messages`, and the `/reaction`, `/members`, `/read`, and `/favorite` actions map to the corresponding `sp_ChatRooms_*Secured` procedures. Lists are at most 200 rooms per page. Room detail returns a bounded message/member view; use a formal record or Conversation when durable workflow/channel semantics are required.
## Build a professional Chat rooms dashboard
These six registry-backed presentation blocks let a designer turn the secured Chat rooms 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/chat-rooms`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/chat-rooms-sample.json`, its executable authenticated page at `pages/first-class/chat-rooms-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 Chat rooms projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Accessible rooms, unread rooms, messages, and mentions |
| `entity-list` | `core.entity-list` | Rooms the current member can open |
| `progress-list` | `core.progress-list` | Distribution by activity, membership, or state |
| `series-chart` | `core.series-chart` | Message and active-member volume |
| `data-grid` | `core.presentation-grid` | One bounded member-authorized room page |
| `timeline` | `core.timeline` | Room creation, membership, message, mention, pin, and archive |
### 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": "chat-rooms-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Chat room overview",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "rooms",
"label": "Accessible rooms",
"value": 28,
"format": "number",
"tone": "neutral"
},
{
"key": "unread",
"label": "Unread rooms",
"value": 6,
"format": "number",
"tone": "warning"
},
{
"key": "messages",
"label": "Messages today",
"value": 214,
"format": "number",
"tone": "primary"
},
{
"key": "mentions",
"label": "Mentions",
"value": 9,
"format": "number",
"tone": "danger"
}
]
},
"children": []
},
{
"_id": "chat-rooms-recent",
"_type": "core.entity-list",
"props": {
"title": "Active rooms",
"hasMore": true,
"items": [
{
"id": "chat-rooms-sample-1",
"recordId": "chat-rooms-record-1",
"primary": "Field Operations",
"secondary": "18 members - 42 messages today",
"status": {
"key": "active",
"label": "Active",
"tone": "success"
},
"trailing": "3 unread"
},
{
"id": "chat-rooms-sample-2",
"recordId": "chat-rooms-record-2",
"primary": "Field Operations - Follow-up",
"secondary": "18 members - 42 messages today - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "chat-rooms-sample-3",
"recordId": "chat-rooms-record-3",
"primary": "Field Operations - West region",
"secondary": "18 members - 42 messages today - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "chat-rooms-sample-4",
"recordId": "chat-rooms-record-4",
"primary": "Field Operations - Customer response",
"secondary": "18 members - 42 messages today - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "chat-rooms-sample-5",
"recordId": "chat-rooms-record-5",
"primary": "Field Operations - Regional operations review with a deliberately long title",
"secondary": "18 members - 42 messages today - 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": "chat-rooms-sample-6",
"recordId": "chat-rooms-record-6",
"primary": "Field Operations - Completed preview",
"secondary": "18 members - 42 messages today - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "chat-rooms-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Rooms by activity",
"items": [
{
"key": "high",
"label": "High activity",
"value": 7,
"maximum": 28,
"displayValue": "7",
"tone": "primary",
"status": {
"key": "high",
"label": "High activity",
"tone": "primary"
}
},
{
"key": "normal",
"label": "Normal",
"value": 15,
"maximum": 28,
"displayValue": "15",
"tone": "success",
"status": {
"key": "normal",
"label": "Normal",
"tone": "success"
}
},
{
"key": "quiet",
"label": "Quiet",
"value": 6,
"maximum": 28,
"displayValue": "6",
"tone": "neutral",
"status": {
"key": "quiet",
"label": "Quiet",
"tone": "neutral"
}
}
]
},
"children": []
},
{
"_id": "chat-rooms-trend",
"_type": "core.series-chart",
"props": {
"title": "Chat activity",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Messages",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Active members",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "chat-rooms-table",
"_type": "core.presentation-grid",
"props": {
"title": "Accessible chat rooms",
"columns": [
{
"key": "room",
"label": "Room",
"type": "text",
"align": "left"
},
{
"key": "members",
"label": "Members",
"type": "number",
"align": "right"
},
{
"key": "unread",
"label": "Unread",
"type": "number",
"align": "right"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "updated",
"label": "Last activity",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "chat-rooms-row-1",
"recordId": "chat-rooms-record-1",
"cells": {
"room": "Field Operations",
"members": 18,
"unread": 3,
"status": {
"key": "active",
"label": "Active",
"tone": "success"
},
"updated": "2026-09-04T17:51:00Z"
}
},
{
"id": "chat-rooms-row-2",
"recordId": "chat-rooms-record-2",
"cells": {
"room": "Field Operations - Follow-up",
"members": 19,
"unread": 4,
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"updated": "2026-09-04T15:42:00Z"
}
},
{
"id": "chat-rooms-row-3",
"recordId": "chat-rooms-record-3",
"cells": {
"room": "Field Operations - West region",
"members": 20,
"unread": 5,
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"updated": "2026-09-04T12:18:00Z"
}
},
{
"id": "chat-rooms-row-4",
"recordId": "chat-rooms-record-4",
"cells": {
"room": "Field Operations - Customer response",
"members": 21,
"unread": 6,
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"updated": "2026-09-03T21:07:00Z"
}
},
{
"id": "chat-rooms-row-5",
"recordId": "chat-rooms-record-5",
"cells": {
"room": "Field Operations - Regional operations review with a deliberately long title",
"members": 22,
"unread": 7,
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"updated": "2026-09-03T16:31:00Z"
}
},
{
"id": "chat-rooms-row-6",
"recordId": "chat-rooms-record-6",
"cells": {
"room": "Field Operations - Completed preview",
"members": 23,
"unread": 8,
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"updated": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 28,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "chat-rooms-timeline",
"_type": "core.timeline",
"props": {
"title": "Room activity",
"hasMore": true,
"items": [
{
"id": "chat-rooms-event-1",
"recordId": "chat-rooms-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Message posted",
"description": "A schedule update was posted in Field Operations.",
"actor": "Maya Chen",
"tone": "primary"
},
{
"id": "chat-rooms-event-2",
"recordId": "chat-rooms-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Message posted - Follow-up",
"description": "A schedule update was posted in Field Operations. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "chat-rooms-event-3",
"recordId": "chat-rooms-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Message posted - West region",
"description": "A schedule update was posted in Field Operations. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "chat-rooms-event-4",
"recordId": "chat-rooms-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Message posted - Customer response",
"description": "A schedule update was posted in Field Operations. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "chat-rooms-event-5",
"recordId": "chat-rooms-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Message posted - Regional operations review with a deliberately long title",
"description": "A schedule update was posted in Field Operations. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "chat-rooms-event-6",
"recordId": "chat-rooms-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Message posted - Completed preview",
"description": "A schedule update was posted in Field Operations. 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 ChatRoomsPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface ChatRoomsPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: ChatRoomsPresentationData | 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 ChatRoomsPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: ChatRoomsPresentationProps) {
if (error) return
The Chat rooms presentation could not be loaded.
;
if (loading || !data) return
Loading Chat rooms presentation...
;
return (
Chat rooms
Chat room overview
Accessible rooms, unread work, membership, message volume, and recent collaboration.
);
}
```
### 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/chat-rooms`, 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": "chat-rooms-live",
"_type": "module.chat-rooms",
"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
Use rooms for conversation, not as the only place a decision or task exists. Promote important outcomes into the appropriate record, workflow, or activity trail.
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/chat-rooms.html).
---
title: "Discussion Boards"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/discussion-boards.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Discussion Boards
Provides boards, topics, threaded posts, replies, pin/lock moderation, archives, owned-post editing, custom fields, relations, activity, and favorites.
## In brief
- **Runtime key:** module.discussion-boards
- **Experience:** Structured community discussion
- **Security:** Server-enforced
Use Discussion Boards when collaboration needs named boards, durable topics, threaded replies, and moderation rather than a continuous chat stream.
[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.discussion-boards. 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
Provides boards, topics, threaded posts, replies, pin/lock moderation, archives, owned-post editing, custom fields, relations, activity, and favorites.
## Key capabilities
- Search active or archived boards and browse topic/post counts.
- Create boards and topics, reply to topics, and edit owned or otherwise authorized content.
- Pin or lock topics and archive boards with the required moderation capability.
- Favorite boards/topics and show secured relations, activity, location, and dynamic fields.
## Common uses
- Customer or partner communities.
- Internal Q&A, proposals, and long-running technical discussions.
- Program, project, or policy forums that benefit from topic-level history.
## How it connects
Boards and topics can relate to Knowledge Articles, Files, projects, products, or cases. A resolved discussion can become maintained knowledge while the original thread remains available as evidence.
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
Board, topic, and post operations enforce tenant, DataRole, location, ownership, and moderation rules. Locked topics refuse ordinary replies, and edit controls reflect secured response capabilities.
- 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.discussion-boards` 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.
### Explicit scope for create helpers
The native create helpers below require the user's current choice of one eligible DataRole with `isPersonal: false`. In a tenant-runtime component, render `RecordCreateScopeSelect allowAll allowPersonal={false}` from `./RecordCreateScopeSelect` and read its submitted form with `readRecordCreateScope`. Pass that choice to the helper; do not infer it from the page, menu, role count, or a default location. When active locations exist, explicitly choose an authorized location or All when permitted; Personal has no location. For a shared-only helper, validate the returned choice is shared and has a DataRole before calling it. The typed choice is a request, and SQL rechecks current authority. Child actions keep their reviewed parent scope. Ordinary edits preserve the loaded scope and revision; moving scope is a separate explicit action.
## Copy/paste the Discussion Boards page
The native block supplies secured board search, board/topic/post detail, create/edit, topic pin/lock moderation, replies, archive state, custom fields, relations, activity, and favorites:
```
{
"blocks": [
{ "_id": "community-discussions", "_type": "module.discussion-boards", "props": {}, "children": [] }
]
}
```
### Custom unstyled board and topic browser
```
import { FormEvent, useEffect, useState } from "react";
import type { DiscussionBoardDetail, DiscussionBoardsList, DiscussionTopicDetail } from "@buildwithhq/module-sdk";
import { getDiscussionBoard, getDiscussionTopic, listDiscussionBoards } from "./api";
const empty: DiscussionBoardsList = { contractVersion: 1, pageNumber: 1, pageSize: 50, totalRecords: 0, totalPages: 0, canCreate: false, canEdit: false, items: [] };
export function UnstyledDiscussionBoards({ locationId = "" }) {
const [draft, setDraft] = useState(""); const [search, setSearch] = useState(""); const [page, setPage] = useState(1);
const [boards, setBoards] = useState(empty); const [board, setBoard] = useState(null); const [topic, setTopic] = useState(null);
useEffect(() => { const controller = new AbortController(); listDiscussionBoards(search, false, controller.signal, locationId, page, 50).then(setBoards); return () => controller.abort(); }, [locationId, page, search]);
async function openBoard(recordId: string) { setBoard(await getDiscussionBoard(recordId)); setTopic(null); }
async function openTopic(recordId: string) { setTopic(await getDiscussionTopic(recordId)); }
return
)}}
;
}
```
### Create boards and topics, moderate, reply, and edit
Board, topic, and post edits are separate optimistic commands. Locked topics refuse ordinary replies; archived boards refuse new work. `canEdit`, `canCreateTopic`, `canReply`, and each post's `canEdit` are server decisions.
```
import type { DiscussionBoardDetail, DiscussionPost, DiscussionTopicDetail } from "@buildwithhq/module-sdk";
import { createDiscussionBoard, createDiscussionTopic, getDiscussionBoard, getDiscussionTopic, postDiscussionReply, setDiscussionFavorite, updateDiscussionBoard, updateDiscussionPost, updateDiscussionTopic } from "./api";
import type { RecordCreateScope } from "./RecordCreateScopeSelect";
type SharedScope = RecordCreateScope & { isPersonal: false; dataRoleId: number };
export const createBoard = (chosenScope: SharedScope) => createDiscussionBoard({ ...chosenScope, boardName: "Product questions", description: "Durable product discussions", dynamicFields: [] });
export async function archiveBoard(board: DiscussionBoardDetail) { await updateDiscussionBoard(board.recordId, { expectedUpdatedUtc: board.updatedUtc, boardName: board.boardName, description: board.description || "", isArchived: true, locationId: board.locationId || undefined, dynamicFields: board.dynamicFields }); return getDiscussionBoard(board.recordId); }
export const startTopic = (board: DiscussionBoardDetail, topicTitle: string, initialPostBody: string) => createDiscussionTopic(board.recordId, { topicTitle, initialPostBody });
export async function lockTopic(topic: DiscussionTopicDetail) { await updateDiscussionTopic(topic.recordId, { expectedUpdatedUtc: topic.updatedUtc, topicTitle: topic.topicTitle, isPinned: topic.isPinned, isLocked: true }); return getDiscussionTopic(topic.recordId); }
export async function reply(topic: DiscussionTopicDetail, postBody: string, parentDiscussionPostId?: string) { if (!topic.canReply) throw new Error("This topic is closed to replies."); await postDiscussionReply(topic.recordId, { postBody, parentDiscussionPostId }); return getDiscussionTopic(topic.recordId); }
export async function editPost(topic: DiscussionTopicDetail, post: DiscussionPost, postBody: string) { if (!post.canEdit) throw new Error("This post cannot be edited."); await updateDiscussionPost(post.recordId, { expectedUpdatedUtc: post.updatedUtc, postBody }); return getDiscussionTopic(topic.recordId); }
export async function toggleTopicFavorite(topic: DiscussionTopicDetail) { await setDiscussionFavorite(topic.recordId, !topic.isFavorite); return getDiscussionTopic(topic.recordId); }
```
### Optional styling
```
.custom-boards { width: 100%; max-width: none; }
.custom-boards > ul, .custom-boards ol { list-style: none; margin: 1rem 0; padding: 0; }
.custom-boards button { text-align: left; }
.custom-boards > article { border: 1px solid var(--line, #d8dee8); margin-top: 1rem; padding: 1rem; }
.custom-boards article section { border-top: 1px solid var(--line, #d8dee8); padding: .75rem 0; }
```
### Exact data path and current scale boundary
The route family is `/api/modules/discussion-boards`: list/create/update board; `/{boardRecordId}/topics`; `/topics/{recordId}`; `/topics/{topicRecordId}/posts`; `/posts/{recordId}`; and `/records/{recordId}/favorite`. Each maps to an explicit `sp_DiscussionBoards_*Secured` procedure and rechecks tenant, record, location, ownership, lock, archive, and moderation scope.
**Important:** board list pages are capped at 200, but the current board-detail contract returns all visible topics and the topic-detail contract returns all visible posts. Use this module for bounded communities today. Before using it for a forum with very large topics, add measured server pagination to those two detail contracts rather than fetching everything and trimming it in React.
## Build a professional Discussion Boards dashboard
These six registry-backed presentation blocks let a designer turn the secured Discussion Boards 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/discussion-boards`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/discussion-boards-sample.json`, its executable authenticated page at `pages/first-class/discussion-boards-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 Discussion Boards projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Visible boards, open topics, unanswered work, and posts |
| `entity-list` | `core.entity-list` | Active topics the user may open |
| `progress-list` | `core.progress-list` | Distribution by topic state, board, or response |
| `series-chart` | `core.series-chart` | New topics, replies, and resolved discussions |
| `data-grid` | `core.presentation-grid` | One bounded topic page with counts and state |
| `timeline` | `core.timeline` | Topic, reply, follow, moderation, answer, lock, and archive |
### 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": "discussion-boards-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Discussion overview",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "boards",
"label": "Visible boards",
"value": 12,
"format": "number",
"tone": "neutral"
},
{
"key": "topics",
"label": "Open topics",
"value": 86,
"format": "number",
"tone": "primary"
},
{
"key": "unanswered",
"label": "Unanswered",
"value": 7,
"format": "number",
"tone": "warning"
},
{
"key": "posts",
"label": "Posts this week",
"value": 143,
"format": "number",
"tone": "success"
}
]
},
"children": []
},
{
"_id": "discussion-boards-recent",
"_type": "core.entity-list",
"props": {
"title": "Active topics",
"hasMore": true,
"items": [
{
"id": "discussion-boards-sample-1",
"recordId": "discussion-boards-record-1",
"primary": "Reducing repeat truck rolls",
"secondary": "Field service - 12 replies",
"status": {
"key": "open",
"label": "Open",
"tone": "primary"
},
"trailing": "2 new"
},
{
"id": "discussion-boards-sample-2",
"recordId": "discussion-boards-record-2",
"primary": "Reducing repeat truck rolls - Follow-up",
"secondary": "Field service - 12 replies - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "discussion-boards-sample-3",
"recordId": "discussion-boards-record-3",
"primary": "Reducing repeat truck rolls - West region",
"secondary": "Field service - 12 replies - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "discussion-boards-sample-4",
"recordId": "discussion-boards-record-4",
"primary": "Reducing repeat truck rolls - Customer response",
"secondary": "Field service - 12 replies - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "discussion-boards-sample-5",
"recordId": "discussion-boards-record-5",
"primary": "Reducing repeat truck rolls - Regional operations review with a deliberately long title",
"secondary": "Field service - 12 replies - 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": "discussion-boards-sample-6",
"recordId": "discussion-boards-record-6",
"primary": "Reducing repeat truck rolls - Completed preview",
"secondary": "Field service - 12 replies - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "discussion-boards-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Topics by state",
"items": [
{
"key": "open",
"label": "Open",
"value": 86,
"maximum": 159,
"displayValue": "86",
"tone": "primary",
"status": {
"key": "open",
"label": "Open",
"tone": "primary"
}
},
{
"key": "answered",
"label": "Answered",
"value": 64,
"maximum": 159,
"displayValue": "64",
"tone": "success",
"status": {
"key": "answered",
"label": "Answered",
"tone": "success"
}
},
{
"key": "locked",
"label": "Locked",
"value": 9,
"maximum": 159,
"displayValue": "9",
"tone": "neutral",
"status": {
"key": "locked",
"label": "Locked",
"tone": "neutral"
}
}
]
},
"children": []
},
{
"_id": "discussion-boards-trend",
"_type": "core.series-chart",
"props": {
"title": "Discussion activity",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Topics",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Posts",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "discussion-boards-table",
"_type": "core.presentation-grid",
"props": {
"title": "Visible discussion topics",
"columns": [
{
"key": "topic",
"label": "Topic",
"type": "text",
"align": "left"
},
{
"key": "board",
"label": "Board",
"type": "text",
"align": "left"
},
{
"key": "replies",
"label": "Replies",
"type": "number",
"align": "right"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "updated",
"label": "Updated",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "discussion-boards-row-1",
"recordId": "discussion-boards-record-1",
"cells": {
"topic": "Reducing repeat truck rolls",
"board": "Field service",
"replies": 12,
"status": {
"key": "open",
"label": "Open",
"tone": "primary"
},
"updated": "2026-09-04T16:44:00Z"
}
},
{
"id": "discussion-boards-row-2",
"recordId": "discussion-boards-record-2",
"cells": {
"topic": "Reducing repeat truck rolls - Follow-up",
"board": "Field service",
"replies": 13,
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"updated": "2026-09-04T15:42:00Z"
}
},
{
"id": "discussion-boards-row-3",
"recordId": "discussion-boards-record-3",
"cells": {
"topic": "Reducing repeat truck rolls - West region",
"board": "Field service",
"replies": 14,
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"updated": "2026-09-04T12:18:00Z"
}
},
{
"id": "discussion-boards-row-4",
"recordId": "discussion-boards-record-4",
"cells": {
"topic": "Reducing repeat truck rolls - Customer response",
"board": "Field service",
"replies": 15,
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"updated": "2026-09-03T21:07:00Z"
}
},
{
"id": "discussion-boards-row-5",
"recordId": "discussion-boards-record-5",
"cells": {
"topic": "Reducing repeat truck rolls - Regional operations review with a deliberately long title",
"board": "Field service - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"replies": 16,
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"updated": "2026-09-03T16:31:00Z"
}
},
{
"id": "discussion-boards-row-6",
"recordId": "discussion-boards-record-6",
"cells": {
"topic": "Reducing repeat truck rolls - Completed preview",
"board": "Field service",
"replies": 17,
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"updated": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 12,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "discussion-boards-timeline",
"_type": "core.timeline",
"props": {
"title": "Discussion activity",
"hasMore": true,
"items": [
{
"id": "discussion-boards-event-1",
"recordId": "discussion-boards-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Reply posted",
"description": "A new field checklist suggestion was added.",
"actor": "Jordan Lee",
"tone": "primary"
},
{
"id": "discussion-boards-event-2",
"recordId": "discussion-boards-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Reply posted - Follow-up",
"description": "A new field checklist suggestion was added. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "discussion-boards-event-3",
"recordId": "discussion-boards-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Reply posted - West region",
"description": "A new field checklist suggestion was added. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "discussion-boards-event-4",
"recordId": "discussion-boards-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Reply posted - Customer response",
"description": "A new field checklist suggestion was added. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "discussion-boards-event-5",
"recordId": "discussion-boards-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Reply posted - Regional operations review with a deliberately long title",
"description": "A new field checklist suggestion was added. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "discussion-boards-event-6",
"recordId": "discussion-boards-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Reply posted - Completed preview",
"description": "A new field checklist suggestion was added. 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 DiscussionBoardsPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface DiscussionBoardsPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: DiscussionBoardsPresentationData | 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 DiscussionBoardsPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: DiscussionBoardsPresentationProps) {
if (error) return
The Discussion Boards presentation could not be loaded.
;
if (loading || !data) return
Loading Discussion Boards presentation...
;
return (
Discussion Boards
Discussion overview
Boards, topics, participation, unanswered work, and recent posts.
);
}
```
### 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/discussion-boards`, 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": "discussion-boards-live",
"_type": "module.discussion-boards",
"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
Choose Chat Rooms for fast conversation and Discussion Boards for durable topics. Avoid enabling both for the same job without a clear information lifecycle.
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/discussion-boards.html).
---
title: "Mail reader"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/mail-reader.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Mail reader
Provides a read-focused view across authorized personal/shared mail accounts with search, unread filtering, plain-text message detail, read state, relations, activity, dynamic fields, and favorites.
## In brief
- **Runtime key:** module.mail-reader
- **Experience:** Secured inbound mail
- **Security:** Server-enforced
Use Mail Reader when tenant users need a bounded view of synchronized email inside the app without exposing mail-server settings, credentials, or raw executable content.
[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.mail-reader. 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
Provides a read-focused view across authorized personal/shared mail accounts with search, unread filtering, plain-text message detail, read state, relations, activity, dynamic fields, and favorites.
## Key capabilities
- Select authorized personal or shared accounts and view unread counts.
- Search messages and filter unread mail.
- Read safe plain-text headers, previews, and bodies; raw HTML is not rendered.
- Mark permitted messages read/unread and connect them to favorites, relations, activity, and custom fields.
## Common uses
- Shared operations or support mailbox review.
- Account/project email research linked to business records.
- Read-focused compliance or case correspondence views.
## How it connects
Mail Reader represents synchronized mail records. Use Conversations when messages should become managed customer threads and Universal Inbox when a message requires assignment or action.
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
Account ownership/shared access, SaaS, tenant, DataRole, and location are enforced server-side. Incoming-server settings, credential references, raw HTML, and private provider details are excluded from browser responses.
- 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.mail-reader` 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 the complete Mail Reader experience
The native block is the quickest complete page: authorized accounts, unread counts, server-side search, message detail, safe plain text, read state, custom fields, relation/activity counts, and favorites.
```
{
"blocks": [
{ "_id": "inbound-mail", "_type": "module.mail-reader", "props": {}, "children": [] }
]
}
```
### Custom unstyled React account, list, and message view
This component requests one bounded server page at a time. The account and location values narrow an already-authorized result; they never establish tenant or mailbox authority.
```
import { FormEvent, useEffect, useState } from "react";
import type { MailReaderList, MailReaderMessageDetail } from "@buildwithhq/module-sdk";
import { getMailReaderMessage, listMailReaderMessages } from "./api";
const empty: MailReaderList = { contractVersion: 1, pageNumber: 1, pageSize: 50, totalRecords: 0, totalPages: 0, accounts: [], items: [] };
export function UnstyledMailReader({ locationId = "" }) {
const [draft, setDraft] = useState("");
const [search, setSearch] = useState("");
const [mailAccountId, setMailAccountId] = useState("");
const [unreadOnly, setUnreadOnly] = useState(false);
const [page, setPage] = useState(1);
const [result, setResult] = useState(empty);
const [selected, setSelected] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setError(null);
listMailReaderMessages(mailAccountId, search, unreadOnly, controller.signal, locationId, page, 50)
.then(setResult)
.catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "Mail could not be loaded."); });
return () => controller.abort();
}, [locationId, mailAccountId, page, search, unreadOnly]);
function submit(event: FormEvent) { event.preventDefault(); setPage(1); setSearch(draft.trim()); }
async function open(recordId: string) { setSelected(await getMailReaderMessage(recordId)); }
return
Mail reader
{error &&
{error}
}
{result.items.map(message => )}{selected ? <>
{selected.subject || "(No subject)"}
From
{selected.fromAddress || "-"}
To
{selected.toAddress || "-"}
Received
{selected.receivedUtc ? new Date(selected.receivedUtc).toLocaleString() : "-"}
{selected.bodyText || "No plain-text body is available."}
{selected.hasHtmlBody && HTML content is intentionally not rendered.}
{selected.relationCount} related - {selected.activityCount} activity
> :
Select a message.
}
;
}
```
### Read and favorite actions
Read state is allowed only when `canMarkRead` is true. Mail Reader does not invent send or reply endpoints: use Conversations for managed replies and Universal Inbox for assigned work.
```
import type { MailReaderMessageDetail } from "@buildwithhq/module-sdk";
import { getMailReaderMessage, setMailReaderFavorite, setMailReaderRead } from "./api";
export async function toggleRead(message: MailReaderMessageDetail) {
if (!message.canMarkRead) throw new Error("This message's read state cannot be changed.");
await setMailReaderRead(message.recordId, !message.isRead, message.updatedUtc);
return getMailReaderMessage(message.recordId);
}
export async function toggleMailFavorite(message: MailReaderMessageDetail) {
await setMailReaderFavorite(message.recordId, !message.isFavorite);
return getMailReaderMessage(message.recordId);
}
```
### Optional starter styling
```
.custom-mail-reader { width: 100%; max-width: none; }
.custom-mail-reader form, .custom-mail-reader nav { align-items: end; display: flex; flex-wrap: wrap; gap: .75rem; }
.mail-columns { display: grid; gap: 1rem; grid-template-columns: minmax(260px, 36%) minmax(0, 1fr); margin: 1rem 0; }
.mail-columns ol { list-style: none; margin: 0; padding: 0; }
.mail-columns li button { background: transparent; border: 0; display: grid; gap: .2rem; padding: .75rem; text-align: left; width: 100%; }
.mail-columns article { border: 1px solid var(--line, #d8dee8); padding: 1rem; }
.mail-body { white-space: pre-wrap; }
@media (max-width: 760px) { .mail-columns { grid-template-columns: 1fr; } }
```
### Exact data path
| Purpose | Route | Procedure |
| --- | --- | --- |
| Accounts and messages | `GET /api/modules/mail-reader` | `sp_MailReader_ListSecured` |
| Safe detail | `GET /api/modules/mail-reader/{recordId}` | `sp_MailReader_GetSecured` |
| Read state | `PUT /api/modules/mail-reader/{recordId}/read` | `sp_MailReader_SetReadSecured` |
| Favorite | `PUT /api/modules/mail-reader/{recordId}/favorite` | `sp_MailReader_SetFavoriteSecured` |
The list is capped at 200 rows per request and detail returns `bodyText`, not executable message HTML, provider credentials, mailbox connection settings, or storage coordinates.
## Build a professional Mail reader dashboard
These six registry-backed presentation blocks let a designer turn the secured Mail reader 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/mail-reader`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/mail-reader-sample.json`, its executable authenticated page at `pages/first-class/mail-reader-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 Mail reader projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Unread, received, account, attachment, and relation counts |
| `entity-list` | `core.entity-list` | Recent mail from accounts the user may read |
| `progress-list` | `core.progress-list` | Distribution by account, read state, or folder |
| `series-chart` | `core.series-chart` | Received, read, linked, and favorited mail |
| `data-grid` | `core.presentation-grid` | One bounded page without raw HTML or credentials |
| `timeline` | `core.timeline` | Receive, read, favorite, relate, attach, and retention 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": "mail-reader-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Mail overview",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "unread",
"label": "Unread",
"value": 37,
"format": "number",
"tone": "warning"
},
{
"key": "today",
"label": "Received today",
"value": 92,
"format": "number",
"tone": "primary"
},
{
"key": "accounts",
"label": "Accessible accounts",
"value": 4,
"format": "number",
"tone": "neutral"
},
{
"key": "linked",
"label": "Linked to records",
"value": 21,
"format": "number",
"tone": "success"
}
]
},
"children": []
},
{
"_id": "mail-reader-recent",
"_type": "core.entity-list",
"props": {
"title": "Recent mail",
"hasMore": true,
"items": [
{
"id": "mail-reader-sample-1",
"recordId": "mail-reader-record-1",
"primary": "Updated project schedule",
"secondary": "maya@example.test - Operations inbox",
"status": {
"key": "unread",
"label": "Unread",
"tone": "warning"
},
"trailing": "2 attachments"
},
{
"id": "mail-reader-sample-2",
"recordId": "mail-reader-record-2",
"primary": "Updated project schedule - Follow-up",
"secondary": "maya@example.test - Operations inbox - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "mail-reader-sample-3",
"recordId": "mail-reader-record-3",
"primary": "Updated project schedule - West region",
"secondary": "maya@example.test - Operations inbox - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "mail-reader-sample-4",
"recordId": "mail-reader-record-4",
"primary": "Updated project schedule - Customer response",
"secondary": "maya@example.test - Operations inbox - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "mail-reader-sample-5",
"recordId": "mail-reader-record-5",
"primary": "Updated project schedule - Regional operations review with a deliberately long title",
"secondary": "maya@example.test - Operations inbox - 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": "mail-reader-sample-6",
"recordId": "mail-reader-record-6",
"primary": "Updated project schedule - Completed preview",
"secondary": "maya@example.test - Operations inbox - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "mail-reader-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Mail by account",
"items": [
{
"key": "operations",
"label": "Operations",
"value": 48,
"maximum": 92,
"displayValue": "48",
"tone": "primary",
"status": {
"key": "operations",
"label": "Operations",
"tone": "primary"
}
},
{
"key": "support",
"label": "Support",
"value": 31,
"maximum": 92,
"displayValue": "31",
"tone": "warning",
"status": {
"key": "support",
"label": "Support",
"tone": "warning"
}
},
{
"key": "sales",
"label": "Sales",
"value": 13,
"maximum": 92,
"displayValue": "13",
"tone": "success",
"status": {
"key": "sales",
"label": "Sales",
"tone": "success"
}
}
]
},
"children": []
},
{
"_id": "mail-reader-trend",
"_type": "core.series-chart",
"props": {
"title": "Mail volume",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Received",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Read",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "mail-reader-table",
"_type": "core.presentation-grid",
"props": {
"title": "Authorized messages",
"columns": [
{
"key": "subject",
"label": "Subject",
"type": "text",
"align": "left"
},
{
"key": "from",
"label": "From",
"type": "text",
"align": "left"
},
{
"key": "account",
"label": "Account",
"type": "text",
"align": "left"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "received",
"label": "Received",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "mail-reader-row-1",
"recordId": "mail-reader-record-1",
"cells": {
"subject": "Updated project schedule",
"from": "maya@example.test",
"account": "Operations",
"status": {
"key": "unread",
"label": "Unread",
"tone": "warning"
},
"received": "2026-09-04T17:19:00Z"
}
},
{
"id": "mail-reader-row-2",
"recordId": "mail-reader-record-2",
"cells": {
"subject": "Updated project schedule - Follow-up",
"from": "maya@example.test",
"account": "Operations",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"received": "2026-09-04T15:42:00Z"
}
},
{
"id": "mail-reader-row-3",
"recordId": "mail-reader-record-3",
"cells": {
"subject": "Updated project schedule - West region",
"from": "maya@example.test",
"account": "Operations",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"received": "2026-09-04T12:18:00Z"
}
},
{
"id": "mail-reader-row-4",
"recordId": "mail-reader-record-4",
"cells": {
"subject": "Updated project schedule - Customer response",
"from": "maya@example.test",
"account": "Operations",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"received": "2026-09-03T21:07:00Z"
}
},
{
"id": "mail-reader-row-5",
"recordId": "mail-reader-record-5",
"cells": {
"subject": "Updated project schedule - Regional operations review with a deliberately long title",
"from": "maya@example.test - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"account": "Operations",
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"received": "2026-09-03T16:31:00Z"
}
},
{
"id": "mail-reader-row-6",
"recordId": "mail-reader-record-6",
"cells": {
"subject": "Updated project schedule - Completed preview",
"from": "maya@example.test",
"account": "Operations",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"received": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 37,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "mail-reader-timeline",
"_type": "core.timeline",
"props": {
"title": "Mail activity",
"hasMore": true,
"items": [
{
"id": "mail-reader-event-1",
"recordId": "mail-reader-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Message linked",
"description": "Updated project schedule was related to Project North.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "mail-reader-event-2",
"recordId": "mail-reader-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Message linked - Follow-up",
"description": "Updated project schedule was related to Project North. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "mail-reader-event-3",
"recordId": "mail-reader-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Message linked - West region",
"description": "Updated project schedule was related to Project North. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "mail-reader-event-4",
"recordId": "mail-reader-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Message linked - Customer response",
"description": "Updated project schedule was related to Project North. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "mail-reader-event-5",
"recordId": "mail-reader-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Message linked - Regional operations review with a deliberately long title",
"description": "Updated project schedule was related to Project North. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "mail-reader-event-6",
"recordId": "mail-reader-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Message linked - Completed preview",
"description": "Updated project schedule was related to Project North. 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 MailReaderPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface MailReaderPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: MailReaderPresentationData | 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 MailReaderPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: MailReaderPresentationProps) {
if (error) return
The Mail reader presentation could not be loaded.
;
if (loading || !data) return
Loading Mail reader presentation...
;
return (
Mail reader
Mail overview
Authorized accounts, unread messages, senders, links to work, and recent mail activity.
);
}
```
### 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/mail-reader`, 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": "mail-reader-live",
"_type": "module.mail-reader",
"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
Do not treat Mail Reader as an unrestricted webmail client. Define the synchronized accounts, read-state authority, and handoff into Conversations or Inbox explicitly.
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/mail-reader.html).
---
title: "Favorites"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/favorites.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Favorites
Gives each user a secured cross-module list of saved records with search, module filters, pinning, change tracking, related context, activity, and dynamic fields.
## In brief
- **Runtime key:** module.favorites
- **Experience:** Personal workspace
- **Security:** Server-enforced
Use Favorites as a personal shortcut layer so each user can return to important records across the app without changing the records themselves.
[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.favorites. 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
Gives each user a secured cross-module list of saved records with search, module filters, pinning, change tracking, related context, activity, and dynamic fields.
## Key capabilities
- Search personal favorites and filter by source module.
- Pin, reorder, remove, and optionally track changes on saved records.
- Show current secured status, location, relations, activity, and dynamic fields.
- Collapse duplicate favorite targets into a coherent personal workspace.
## Common uses
- Pinned customers, projects, cases, files, or articles.
- A personal watch list for records whose changes matter.
- Cross-module navigation for frequently used work.
## How it connects
Any supported canonical Record can participate. The favorite stores personal preference; the target remains owned by its native module and is reauthorized whenever it is listed or opened.
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
Only the current user's favorite rows are returned. Target records, related records, fields, activity, and location are independently secured, so a favorite never preserves access after permission is removed.
- 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.favorites` 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 the complete Favorites workspace
The native block provides personal search, module and tracked filters, detail, related context, pinning, ordering, change tracking, dynamic fields, and removal.
```
{
"blocks": [
{ "_id": "my-favorites", "_type": "module.favorites", "props": {}, "children": [] }
]
}
```
### Custom unstyled React list and detail
Each result page is assembled by the secured procedure. Do not cache a favorite as proof that its target is still visible; opening and listing reauthorize the target every time.
```
import { FormEvent, useEffect, useState } from "react";
import { favoriteDynamicValue, type FavoriteDetail, type FavoritesList } from "@buildwithhq/module-sdk";
import { getFavorite, listFavorites } from "./api";
const empty: FavoritesList = { contractVersion: 1, pageNumber: 1, pageSize: 50, totalRecords: 0, totalPages: 0, modules: [], items: [] };
export function UnstyledFavorites({ locationId = "" }) {
const [draft, setDraft] = useState("");
const [search, setSearch] = useState("");
const [moduleKey, setModuleKey] = useState("");
const [trackedOnly, setTrackedOnly] = useState(false);
const [page, setPage] = useState(1);
const [result, setResult] = useState(empty);
const [selected, setSelected] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
listFavorites(moduleKey, search, trackedOnly, controller.signal, locationId, page, 50)
.then(setResult)
.catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "Favorites could not be loaded."); });
return () => controller.abort();
}, [locationId, moduleKey, page, search, trackedOnly]);
function submit(event: FormEvent) { event.preventDefault(); setPage(1); setSearch(draft.trim()); }
async function open(recordId: string) { setSelected(await getFavorite(recordId)); }
return
Favorites
{error &&
{error}
}
{result.items.map(item => )}{selected ? <>
{selected.recordTitle || selected.favoriteName}
{selected.recordStatus || "No status"} - {selected.relationCount} related - {selected.activityCount} activity
);
}
```
### 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/notifications`, 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": "notifications-live",
"_type": "module.notifications",
"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
Notify only when the recipient can act or benefit. High-volume events should roll up into an Inbox queue, digest, or dashboard rather than producing alert fatigue.
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/notifications.html).
---
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
## Model your own business data
Read [Build custom data modules](../builder-guide/dynamic-module-system.html) for the custom-field model, RecordId, field setup, permissions and public API access.
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
Save this helper as `dynamicFieldInputs.ts` beside the custom component below. It converts only field definitions returned by the validated schema. Enter a date as `YYYY-MM-DD` (encoded at UTC midnight), or a timestamp with an explicit `Z` or numeric offset. The helper sends the equivalent UTC timestamp and preserves up to seven fractional digits, so server-local time cannot reinterpret the chosen instant. Decimal input uses the current native SDK's `decimal(18,6)` converter: values travel as JSON numbers when exact, or canonical decimal strings when a number would lose digits. Use the current tenant runtime SDK source for this helper; the immutable Developer Kit describes its own published edition. The current Dynamic Records contract preserves these strings on reads and writes. The editor sends only changed fields on update. Precision already lost in a legacy JSON number cannot be recovered. Other modules require their own reviewed decimal contract. The server independently enforces field visibility, write permissions, and validation.
```
import { decimalFieldInput, integerFieldInput, type DecimalFieldValue, type DynamicModuleDetail, type DynamicModuleFieldDefinition, type 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 "";
if (field.dataType.toLowerCase() === "json") return field.valueJson == null ? "" : JSON.stringify(field.valueJson);
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 dynamicDecimalInput(raw: string): DecimalFieldValue {
return decimalFieldInput(raw);
}
export function dynamicFieldInput(field: DynamicModuleFieldDefinition, raw: string): DynamicModuleFieldInput {
const dataType = field.dataType.toLowerCase();
if (!raw) {
if (field.required) throw new Error(`Enter ${field.label}.`);
return { fieldKey: field.fieldKey, clear: true };
}
if (["boolean", "bool"].includes(dataType)) {
if (raw !== "true" && raw !== "false") throw new Error(`Choose Yes or No for ${field.label}.`);
return { fieldKey: field.fieldKey, valueBool: raw === "true" };
}
if (["int", "integer", "bigint"].includes(dataType)) return { fieldKey: field.fieldKey, valueInt: integerFieldInput(raw) };
if (["decimal", "number", "currency"].includes(dataType)) {
return { fieldKey: field.fieldKey, valueDecimal: dynamicDecimalInput(raw) };
}
if (["datetime", "date"].includes(dataType)) {
const timestamp = dataType === "date" && /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(raw) ? `${raw}T00:00:00Z` : raw;
const zoned = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:[.][0-9]{1,7})?(?:Z|[+-][0-9]{2}:[0-9]{2})$/i.test(timestamp);
const day = timestamp.slice(0, 10);
const calendar = new Date(`${day}T00:00:00Z`);
if (!zoned || Number(day.slice(0, 4)) < 1 || !Number.isFinite(Date.parse(timestamp)) || !Number.isFinite(calendar.getTime()) || calendar.toISOString().slice(0, 10) !== day)
throw new Error(`Enter a valid date or a timestamp with a timezone for ${field.label}.`);
const instant = new Date(timestamp);
if (instant.getUTCFullYear() < 1 || instant.getUTCFullYear() > 9999)
throw new Error(`Enter a timestamp within the supported UTC range for ${field.label}.`);
const fraction = timestamp.match(/([.][0-9]{1,7})(?:Z|[+-][0-9]{2}:[0-9]{2})$/i)?.[1] || "";
// Numeric offsets contain no fractional seconds, so the entered fraction is unchanged by conversion.
const utc = instant.toISOString().replace(/[.][0-9]{3}Z$/, `${fraction}Z`);
return { fieldKey: field.fieldKey, valueDateTime: utc };
}
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 create/edit workspace
This example requests schema and data separately, pages on the server, and builds controls from the schema. It illustrates create, detail, edit, and favorite behavior; use the native module for its complete removal workflow. Mount it inside the verified tenant runtime host, which must remount it when the actor, session, or persisted page context changes. The configured module key is a query target, never tenant authority; built-in or foreign module keys fail closed.
```
import { FormEvent, useEffect, useRef, 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";
import { RecordCreateScopeSelect, RecordCurrentScopeDisplay, readRecordCreateScope } from "./RecordCreateScopeSelect";
const empty = (moduleKey: string): DynamicModuleList => ({ contractVersion: 1, moduleKey, records: [], page: 1, pageSize: 50, total: 0, totalIsExact: false, hasMore: false });
export function UnstyledDynamicRecords({ moduleKey }: { moduleKey: string }) {
return ;
}
function DynamicWorkspace({ moduleKey }: { moduleKey: string }) {
const [schema, setSchema] = useState(null);
const [formSchema, setFormSchema] = useState(null);
const [result, setResult] = useState(empty(moduleKey));
const [selected, setSelected] = useState(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>({});
const [originalValues, setOriginalValues] = useState>({});
const [error, setError] = useState(null);
const [saving, setSaving] = useState(false);
const [needsReview, setNeedsReview] = useState(false);
const pending = useRef(false);
const active = useRef(true);
const detailVersion = useRef(0);
useEffect(() => { active.current = true; return () => { active.current = false; detailVersion.current += 1; }; }, []);
async function load(signal?: AbortSignal) {
const [nextSchema, nextList] = await Promise.all([getDynamicModuleSchema(moduleKey, signal), listDynamicModuleRecords(moduleKey, search, signal, statusFilter, page, 50, sort)]);
if (active.current && !signal?.aborted) { setSchema(nextSchema); setResult(nextList); }
}
useEffect(() => { const controller = new AbortController(); setError(null); load(controller.signal).catch(() => { if (!controller.signal.aborted) setError("Records could not be loaded. Reload to try the read again."); }); return () => controller.abort(); }, [moduleKey, page, search, sort, statusFilter]);
async function open(recordId: string) {
if (pending.current || needsReview) return;
const version = ++detailVersion.current;
setSelected(null); setEditing(false); setError(null);
try {
const detail = await getDynamicModuleRecord(moduleKey, recordId);
if (active.current && version === detailVersion.current) setSelected(detail);
} catch { if (active.current && version === detailVersion.current) setError("The selected record could not be loaded."); }
}
function begin(detail: DynamicModuleDetail | null) {
if (!schema || pending.current || needsReview) return;
detailVersion.current += 1; setCreating(!detail); setEditing(true); setSelected(detail); setError(null);
setTitle(detail?.title || ""); setStatus(detail?.status || "Active"); setFormSchema(schema);
const initial = Object.fromEntries(schema.fields.map(field => [field.fieldKey, dynamicRawValue(detail, field.fieldKey)]));
setValues(initial); setOriginalValues(initial);
}
async function save(event: FormEvent) {
event.preventDefault(); if (!formSchema || pending.current || needsReview) return;
const form = event.currentTarget as HTMLFormElement;
let submitted = false;
let validationComplete = false;
pending.current = true; setSaving(true); setError(null);
try {
const fields = formSchema.fields.filter(field => creating || values[field.fieldKey] !== originalValues[field.fieldKey])
.map(field => dynamicFieldInput(field, values[field.fieldKey] || ""));
const chosenScope = creating ? readRecordCreateScope(new FormData(form)) : null;
if (!creating && (!selected || !selected.updatedUtc)) throw new Error("Reload this record to obtain its current revision before saving.");
validationComplete = true; submitted = true;
const mutation = creating
? await createDynamicModuleRecord(moduleKey, { ...chosenScope!, title, fields })
: await updateDynamicModuleRecord(moduleKey, selected!.recordId, { expectedUpdatedUtc: selected!.updatedUtc!, title, status, fields });
// A confirmed write closes the draft before any follow-up read can fail.
submitted = false;
if (!active.current) return;
setEditing(false); setCreating(false);
const detail = await getDynamicModuleRecord(moduleKey, mutation.recordId);
if (active.current) setSelected(detail);
await load();
} catch (caught: unknown) {
if (active.current) {
if (submitted) setNeedsReview(true);
setError(!validationComplete ? caught instanceof Error ? caught.message : "Check the form values."
: submitted ? "The save was not confirmed. Review the current records before trying another write."
: "The form or refresh could not be completed. Reload the current record if needed.");
}
} finally { pending.current = false; if (active.current) setSaving(false); }
}
async function favorite() {
if (!selected || pending.current || needsReview) return;
pending.current = true; setSaving(true); setError(null);
let submitted = false;
try {
submitted = true;
await setDynamicModuleFavorite(moduleKey, selected.recordId, !selected.isFavorite);
submitted = false;
const detail = await getDynamicModuleRecord(moduleKey, selected.recordId);
if (active.current) setSelected(detail);
} catch {
if (active.current) { if (submitted) setNeedsReview(true); setError("The favorite change or its refresh was not confirmed. Reload to review the current state."); }
} finally { pending.current = false; if (active.current) setSaving(false); }
}
return
{error &&
{error}
}
{!schema && !error &&
Loading records…
}
{schema && result.records.length === 0 &&
No records match this view.
}
{needsReview && }
;
}
```
### Location changes and the current concurrency boundary
Creation uses the secured visibility/location selector without a preselected scope. Editing shows the persisted scope. Every update passes the exact `updatedUtc` from the loaded record as `expectedUpdatedUtc`; if it is absent, reload or stop rather than inventing a revision. Location changes require an explicit authorized choice: omit both values to preserve location, pass the chosen `locationId` to replace it, or use `clearLocation: true` only for an explicitly chosen All scope when permitted. SQL rechecks scope and authority. An unconfirmed write locks this example until the user reloads and reviews current records; it never retries a mutation automatically.
```
import type { DynamicModuleDetail } from "@buildwithhq/module-sdk";
import { getDynamicModuleRecord, updateDynamicModuleRecord } from "./api";
export async function moveDynamicRecord(moduleKey: string, current: DynamicModuleDetail, chosenLocationId: string) {
if (!current.updatedUtc) throw new Error("Reload the record before moving it.");
await updateDynamicModuleRecord(moduleKey, current.recordId, { expectedUpdatedUtc: current.updatedUtc, locationId: chosenLocationId });
return getDynamicModuleRecord(moduleKey, current.recordId);
}
export async function chooseAllDynamicRecordLocations(moduleKey: string, current: DynamicModuleDetail) {
if (!current.updatedUtc) throw new Error("Reload the record before moving it.");
await updateDynamicModuleRecord(moduleKey, current.recordId, { expectedUpdatedUtc: current.updatedUtc, clearLocation: true });
return getDynamicModuleRecord(moduleKey, current.recordId);
}
```
### Optional starter styling
```
.custom-dynamic-records { width: 100%; max-width: none; }
.custom-dynamic-records > fieldset { border: 0; margin: 0; min-width: 0; padding: 0; }
.custom-dynamic-records > fieldset > header, .custom-dynamic-records > fieldset > form, .custom-dynamic-records > fieldset > 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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
The Dynamic Records presentation could not be loaded.
;
if (loading || !data) return
Loading Dynamic Records presentation...
;
return (
Dynamic Records
Custom record operations
Builder-defined records, lifecycle state, ownership, location, relationships, and recent changes.
);
}
```
### 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).
---
title: "ActiveWorkplace"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/active-workplace.html"
reviewed_at: "2026-09-14"
authority: Official
---
# ActiveWorkplace
Pins any permitted Record into a collaborative workplace over its secured recursive relationship graph, with people, GoClaws, workflows, Record Pulse, Inbox attention, activity, and read-only Graph Playback.
## In brief
- **Runtime key:** module.active-workplace
- **Experience:** Recursive work coordination
- **Security:** Server-enforced
Use ActiveWorkplace when work crosses several connected records and people need one durable coordination view without creating a duplicate project, case, customer, file, or other business record.
[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.active-workplace. 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
Pins any permitted Record into a collaborative workplace over its secured recursive relationship graph, with people, GoClaws, workflows, Record Pulse, Inbox attention, activity, and read-only Graph Playback.
## Key capabilities
- Pin any currently permitted Record once as the workplace root.
- Capture a cycle-safe, bounded Active Graph over existing typed Record Relations.
- Coordinate named users, GoClaw assistants, and published workflows with explicit responsibilities.
- Track Active, Waiting, At Risk, Stale, Dormant, and Resolved Record Pulse states.
- Create or touch one Universal Inbox attention item when a workplace becomes At Risk or Stale.
- Record meaningful comments, participant changes, status transitions, related-record changes, and graph refreshes in an immutable activity stream.
- Reconstruct retained workplace, participant, graph, and activity state through unmistakably read-only Graph Playback while applying current permissions.
- Accept bounded builder defaults for root modules, graph depth/nodes, stale timing, participant types, GoClaw watchers, comments, and playback.
## The Active Graph
The pinned Record stays owned by its native module. ActiveWorkplace follows the existing `RecordRelations` graph to the configured depth and node limit, records identity/topology snapshots, and resolves titles, status, module, location, and edit state through the current secured Record envelope. Cycles are safe, and hidden nodes and their edges are omitted.
## Record Pulse
| Pulse | Meaning |
| --- | --- |
| **Active** | Meaningful work is moving. |
| **Waiting** | Work is intentionally waiting for a date, response, or dependency. |
| **At Risk** | The configured inactivity threshold is approaching. |
| **Stale** | The inactivity threshold has passed and attention is required. |
| **Dormant** | The workplace is intentionally inactive without being resolved. |
| **Resolved** | The coordinated work is complete. |
A bounded background evaluator observes meaningful activity and configured timing. At Risk and Stale transitions reuse Universal Inbox create-or-touch behavior, so a retry updates the existing attention item instead of creating an alert pile.
## Participants and governed assistance
A participant assignment states responsibility; it does not grant access. A user still needs current Record permission. A GoClaw must independently have AI-read access through its backing user, and material actions still require the existing action permission and approval policy. A workflow remains limited to its published definition and normal execution authority.
## Graph Playback
Playback combines temporal workplace/participant state, immutable events, retained graph snapshots, and historical Record values. It then intersects every retained Record identity with the viewer’s *current* DataRole and location scope. Historical mode has a prominent read-only banner and removes status, participant, comment, workflow, GoClaw, refresh, and destructive controls.
## Common uses
- Complex customer, case, opportunity, project, asset, incident, contract, or collections coordination.
- A Wave-like shared work surface around connected records without copying those records.
- Cross-team handoffs where responsibilities, inactivity, dependencies, and history must stay visible.
- GoClaw monitoring and workflow participation under the same permissions and approvals as the rest of the app.
## How it connects
ActiveWorkplace composes the universal Records/RecordRelations graph, tenant DataRole/location envelope, activity, workflows, GoClaw identities and actions, and Universal Inbox attention. Its state belongs to the tenant runtime database. AI Orchestration is used only when an AI job actually plans or executes work; it does not hold a shadow workplace or graph.
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 derives app, account, user, DataRole, and locations. Root creation requires current read/edit access; each displayed graph node is reauthorized; GoClaw assignment requires independent AI-read access; and playback applies current authorization before returning any retained identity or event. Direct table access is denied to runtime and Pulse-worker roles.
- 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. Add the validated `module.active-workplace` block to an appropriate page and place that page in existing user-type menus.
2. Choose optional root module filters, graph depth (1–5), node limit (2–200), stale threshold, warning window, participant types, comments, and playback defaults.
3. If desired, select eligible default GoClaw participant IDs and retain human approval for material actions; unavailable or unauthorized agents are not assigned.
4. Confirm the app has useful typed Record Relations, then test pinning a cyclic graph, an inaccessible branch, another location, Waiting expiry, At Risk/Stale Inbox attention, and Resolved behavior.
5. Test Graph Playback after changing a user’s DataRole or location and confirm historical mode exposes no mutation controls.
## Copy/paste the complete ActiveWorkplace experience
The native block already combines root-record search and pinning, bounded graph capture, Pulse state, participants, comments, Inbox attention, graph refresh, and read-only playback. Builder props set safe defaults; the server remains authoritative.
```
{
"blocks": [
{
"_id": "active-work", "_type": "module.active-workplace",
"props": {
"allowedRootModuleKeys": [], "maxDepth": 2, "maxNodes": 100,
"staleAfterMinutes": 4320, "warningBeforeStaleMinutes": 720,
"participantTypes": ["User", "GoClaw", "Workflow"],
"defaultGoClawParticipantIds": [], "commentsEnabled": true, "playbackEnabled": true
},
"children": []
}
]
}
```
### Custom unstyled React list, graph, people, and activity
The list is server-paged. Detail returns a cycle-safe graph snapshot capped by the workplace's reviewed depth/node policy, up to depth 5 and 200 nodes; hidden nodes and their edges are absent before React receives the response.
```
import { FormEvent, useEffect, useMemo, useState } from "react";
import { activeWorkplaceStatusLabel, type ActiveWorkplaceDetail, type ActiveWorkplaceList } from "@buildwithhq/module-sdk";
import { getActiveWorkplace, listActiveWorkplaces } from "./api";
const empty: ActiveWorkplaceList = { contractVersion: 1, pageNumber: 1, pageSize: 50, totalRecords: 0, totalPages: 0, items: [] };
export function UnstyledActiveWorkplace({ ownerUserId = "", participantId = "" }) {
const [draft, setDraft] = useState("");
const [search, setSearch] = useState("");
const [status, setStatus] = useState("");
const [page, setPage] = useState(1);
const [result, setResult] = useState(empty);
const [detail, setDetail] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
listActiveWorkplaces(search, status, page, controller.signal, 50, ownerUserId, participantId)
.then(setResult)
.catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "Workplaces could not be loaded."); });
return () => controller.abort();
}, [ownerUserId, page, participantId, search, status]);
const nodesByDepth = useMemo(() => {
const grouped = new Map();
for (const node of detail?.graph.nodes || []) { const depth = node.depth || 0; grouped.set(depth, [...(grouped.get(depth) || []), node]); }
return [...grouped.entries()].sort(([left], [right]) => left - right);
}, [detail]);
function submit(event: FormEvent) { event.preventDefault(); setPage(1); setSearch(draft.trim()); }
async function open(activeWorkplaceId: string) { setDetail(await getActiveWorkplace(activeWorkplaceId)); }
return
;
}
```
### Pin a record, replace settings, and move the Pulse
Create uses a visible canonical root `RecordId`. Update and Pulse changes are optimistic: always send the latest `updatedUtc` and reload after success or a 409 conflict.
```
import type { ActiveWorkplaceDetail, ActiveWorkplaceStatus } from "@buildwithhq/module-sdk";
import { createActiveWorkplace, getActiveWorkplace, setActiveWorkplaceStatus, updateActiveWorkplace } from "./api";
export async function pinRecord(rootRecordId: string) {
const created = await createActiveWorkplace({ rootRecordId, priority: 2, graphScope: { maxDepth: 2, maxNodes: 100, playbackEnabled: true }, staleAfterMinutes: 4320, warningBeforeStaleMinutes: 720 });
return getActiveWorkplace(created.activeWorkplaceId);
}
export async function renameWorkplace(current: ActiveWorkplaceDetail, displayName: string) {
await updateActiveWorkplace(current.activeWorkplaceId, { displayName, ownerUserId: current.ownerUserId, priority: current.priority, graphScope: current.graphScope, staleAfterMinutes: current.staleAfterMinutes, warningBeforeStaleMinutes: current.warningBeforeStaleMinutes, expectedUpdatedUtc: current.updatedUtc });
return getActiveWorkplace(current.activeWorkplaceId);
}
export async function changePulse(current: ActiveWorkplaceDetail, status: ActiveWorkplaceStatus, waiting?: { untilUtc?: string; reason?: string }) {
await setActiveWorkplaceStatus(current.activeWorkplaceId, { status, waitingUntilUtc: status === "Waiting" ? waiting?.untilUtc : undefined, waitingReason: status === "Waiting" ? waiting?.reason : undefined, expectedUpdatedUtc: current.updatedUtc });
return getActiveWorkplace(current.activeWorkplaceId);
}
```
### Participants, governed GoClaw, comments, refresh, and playback
Select participants only from the server catalog. Assignment expresses responsibility; it does not grant data access. A GoClaw remains bound to its effective user, AI-read scope, action permission, and approval policy.
```
import type { ActiveWorkplaceDetail, ActiveWorkplaceParticipantWrite } from "@buildwithhq/module-sdk";
import { addActiveWorkplaceParticipant, getActiveWorkplace, getActiveWorkplaceParticipantCatalog, getActiveWorkplacePlayback, recordActiveWorkplaceActivity, refreshActiveWorkplaceGraph, removeActiveWorkplaceParticipant } from "./api";
export const participantCatalog = (workplaceId: string) => getActiveWorkplaceParticipantCatalog(workplaceId);
export async function addParticipant(current: ActiveWorkplaceDetail, participant: ActiveWorkplaceParticipantWrite) {
if (!current.canEdit) throw new Error("This workplace cannot be edited.");
await addActiveWorkplaceParticipant(current.activeWorkplaceId, participant);
return getActiveWorkplace(current.activeWorkplaceId);
}
export async function addGovernedGoClaw(current: ActiveWorkplaceDetail, participantId: string) {
return addParticipant(current, { participantType: "GoClaw", participantId, responsibilityText: "Monitor changes and prepare suggestions", isWatcher: true, canAct: false, approvalPolicy: { materialActions: "humanApproval" } });
}
export async function removeParticipant(current: ActiveWorkplaceDetail, activeWorkplaceParticipantId: string) {
await removeActiveWorkplaceParticipant(current.activeWorkplaceId, activeWorkplaceParticipantId);
return getActiveWorkplace(current.activeWorkplaceId);
}
export async function postUpdate(current: ActiveWorkplaceDetail, summary: string) {
await recordActiveWorkplaceActivity(current.activeWorkplaceId, { eventType: "Comment", summary, isMeaningfulActivity: true });
return getActiveWorkplace(current.activeWorkplaceId);
}
export async function refreshGraph(current: ActiveWorkplaceDetail) { await refreshActiveWorkplaceGraph(current.activeWorkplaceId); return getActiveWorkplace(current.activeWorkplaceId); }
export const playBackReadOnly = (current: ActiveWorkplaceDetail, asOfUtc: string) => getActiveWorkplacePlayback(current.activeWorkplaceId, asOfUtc);
```
### Optional starter styling
```
.custom-active-workplace { width: 100%; max-width: none; }
.custom-active-workplace > form, .custom-active-workplace > nav { align-items: end; display: flex; flex-wrap: wrap; gap: .75rem; }
.workplace-columns { display: grid; gap: 1rem; grid-template-columns: 280px minmax(0, 1fr); margin: 1rem 0; }
.workplace-columns ol { list-style: none; margin: 0; padding: 0; }
.workplace-columns aside button { background: transparent; border: 0; display: grid; gap: .2rem; padding: .75rem; text-align: left; width: 100%; }
.workplace-columns main { border: 1px solid var(--line, #d8dee8); padding: 1rem; }
.workplace-columns main section article { border-left: 3px solid var(--accent, #4967d5); display: grid; margin: .5rem 0; padding: .5rem .75rem; }
@media (max-width: 760px) { .workplace-columns { grid-template-columns: 1fr; } }
```
### Exact lifecycle and data path
| Purpose | Route | Procedure |
| --- | --- | --- |
| List/detail | `GET /api/modules/active-workplace` and `/{activeWorkplaceId}` | `sp_ActiveWorkplace_ListSecured`, `sp_ActiveWorkplace_GetSecured` |
| Pin/replace/Pulse | `POST /api/modules/active-workplace`, `PATCH /{id}`, `POST /{id}/status` | `sp_ActiveWorkplace_CreateSecured`, `sp_ActiveWorkplace_UpdateSecured`, `sp_ActiveWorkplace_SetStatusSecured` |
| Participants | `GET /{id}/participants/catalog`, `POST /{id}/participants`, `DELETE /{id}/participants/{participantId}` | `sp_ActiveWorkplace_GetParticipantCatalogSecured`, `sp_ActiveWorkplace_AddParticipantSecured`, `sp_ActiveWorkplace_RemoveParticipantSecured` |
| Activity/graph | `POST /{id}/activity`, `POST /{id}/graph/refresh` | `sp_ActiveWorkplace_RecordActivitySecured`, `sp_ActiveWorkplace_RefreshGraphSecured` |
| Read-only history | `GET /{id}/playback?at=...` | `sp_ActiveWorkplace_GetPlaybackSecured` |
At Risk and Stale transitions create-or-touch Universal Inbox attention instead of duplicating alerts. Playback is explicitly `readOnly: true` and intersects retained history with the viewer's current permissions.
## Build a professional ActiveWorkplace dashboard
These six registry-backed presentation blocks let a designer turn the secured ActiveWorkplace 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/active-workplace`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/active-workplace-sample.json`, its executable authenticated page at `pages/first-class/active-workplace-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 ActiveWorkplace projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Active, healthy, at-risk, blocked, and inactive counts |
| `entity-list` | `core.entity-list` | Workspaces requiring the current participant's attention |
| `progress-list` | `core.progress-list` | Distribution by health, owner, team, or state |
| `series-chart` | `core.series-chart` | Updates, handoffs, completed work, and blockers |
| `data-grid` | `core.presentation-grid` | One bounded participant-authorized workspace page |
| `timeline` | `core.timeline` | Create, participant, relation, update, handoff, blocker, and completion |
### 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": "active-workplace-metrics",
"_type": "core.metric-strip",
"props": {
"title": "ActiveWorkplace operations",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "active",
"label": "Active workspaces",
"value": 38,
"format": "number",
"tone": "primary"
},
{
"key": "attention",
"label": "Need attention",
"value": 7,
"format": "number",
"tone": "warning"
},
{
"key": "blocked",
"label": "Blocked",
"value": 3,
"format": "number",
"tone": "danger"
},
{
"key": "healthy",
"label": "On track",
"value": 28,
"format": "number",
"tone": "success"
}
]
},
"children": []
},
{
"_id": "active-workplace-recent",
"_type": "core.entity-list",
"props": {
"title": "Workspaces needing attention",
"hasMore": true,
"items": [
{
"id": "active-workplace-sample-1",
"recordId": "active-workplace-record-1",
"primary": "West region rollout",
"secondary": "8 participants - 3 related records",
"status": {
"key": "at-risk",
"label": "At risk",
"tone": "warning"
},
"trailing": "2 blockers"
},
{
"id": "active-workplace-sample-2",
"recordId": "active-workplace-record-2",
"primary": "West region rollout - Follow-up",
"secondary": "8 participants - 3 related records - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "active-workplace-sample-3",
"recordId": "active-workplace-record-3",
"primary": "West region rollout - West region",
"secondary": "8 participants - 3 related records - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "active-workplace-sample-4",
"recordId": "active-workplace-record-4",
"primary": "West region rollout - Customer response",
"secondary": "8 participants - 3 related records - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "active-workplace-sample-5",
"recordId": "active-workplace-record-5",
"primary": "West region rollout - Regional operations review with a deliberately long title",
"secondary": "8 participants - 3 related records - 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": "active-workplace-sample-6",
"recordId": "active-workplace-record-6",
"primary": "West region rollout - Completed preview",
"secondary": "8 participants - 3 related records - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "active-workplace-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Workspace health",
"items": [
{
"key": "track",
"label": "On track",
"value": 28,
"maximum": 38,
"displayValue": "28",
"tone": "success",
"status": {
"key": "track",
"label": "On track",
"tone": "success"
}
},
{
"key": "risk",
"label": "At risk",
"value": 7,
"maximum": 38,
"displayValue": "7",
"tone": "warning",
"status": {
"key": "risk",
"label": "At risk",
"tone": "warning"
}
},
{
"key": "blocked",
"label": "Blocked",
"value": 3,
"maximum": 38,
"displayValue": "3",
"tone": "danger",
"status": {
"key": "blocked",
"label": "Blocked",
"tone": "danger"
}
}
]
},
"children": []
},
{
"_id": "active-workplace-trend",
"_type": "core.series-chart",
"props": {
"title": "Workplace activity",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Updates",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Completed items",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "active-workplace-table",
"_type": "core.presentation-grid",
"props": {
"title": "Active workspaces",
"columns": [
{
"key": "workspace",
"label": "Workspace",
"type": "text",
"align": "left"
},
{
"key": "owner",
"label": "Owner",
"type": "text",
"align": "left"
},
{
"key": "participants",
"label": "Participants",
"type": "number",
"align": "right"
},
{
"key": "health",
"label": "Health",
"type": "status",
"align": "left"
},
{
"key": "updated",
"label": "Updated",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "active-workplace-row-1",
"recordId": "active-workplace-record-1",
"cells": {
"workspace": "West region rollout",
"owner": "Maya Chen",
"participants": 8,
"health": {
"key": "risk",
"label": "At risk",
"tone": "warning"
},
"updated": "2026-09-04T17:02:00Z"
}
},
{
"id": "active-workplace-row-2",
"recordId": "active-workplace-record-2",
"cells": {
"workspace": "West region rollout - Follow-up",
"owner": "Maya Chen",
"participants": 9,
"health": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"updated": "2026-09-04T15:42:00Z"
}
},
{
"id": "active-workplace-row-3",
"recordId": "active-workplace-record-3",
"cells": {
"workspace": "West region rollout - West region",
"owner": "Maya Chen",
"participants": 10,
"health": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"updated": "2026-09-04T12:18:00Z"
}
},
{
"id": "active-workplace-row-4",
"recordId": "active-workplace-record-4",
"cells": {
"workspace": "West region rollout - Customer response",
"owner": "Maya Chen",
"participants": 11,
"health": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"updated": "2026-09-03T21:07:00Z"
}
},
{
"id": "active-workplace-row-5",
"recordId": "active-workplace-record-5",
"cells": {
"workspace": "West region rollout - Regional operations review with a deliberately long title",
"owner": "Maya Chen - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"participants": 12,
"health": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"updated": "2026-09-03T16:31:00Z"
}
},
{
"id": "active-workplace-row-6",
"recordId": "active-workplace-record-6",
"cells": {
"workspace": "West region rollout - Completed preview",
"owner": "Maya Chen",
"participants": 13,
"health": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"updated": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 38,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "active-workplace-timeline",
"_type": "core.timeline",
"props": {
"title": "Workspace activity",
"hasMore": true,
"items": [
{
"id": "active-workplace-event-1",
"recordId": "active-workplace-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Blocker added",
"description": "Network approval is blocking the Reno rollout.",
"actor": "Jordan Lee",
"tone": "danger"
},
{
"id": "active-workplace-event-2",
"recordId": "active-workplace-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Blocker added - Follow-up",
"description": "Network approval is blocking the Reno rollout. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "active-workplace-event-3",
"recordId": "active-workplace-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Blocker added - West region",
"description": "Network approval is blocking the Reno rollout. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "active-workplace-event-4",
"recordId": "active-workplace-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Blocker added - Customer response",
"description": "Network approval is blocking the Reno rollout. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "active-workplace-event-5",
"recordId": "active-workplace-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Blocker added - Regional operations review with a deliberately long title",
"description": "Network approval is blocking the Reno rollout. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "active-workplace-event-6",
"recordId": "active-workplace-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Blocker added - Completed preview",
"description": "Network approval is blocking the Reno rollout. 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 ActiveWorkplacePresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface ActiveWorkplacePresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: ActiveWorkplacePresentationData | 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 ActiveWorkplacePresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: ActiveWorkplacePresentationProps) {
if (error) return
The ActiveWorkplace presentation could not be loaded.
;
if (loading || !data) return
Loading ActiveWorkplace presentation...
;
return (
ActiveWorkplace
ActiveWorkplace operations
Coordinated workspaces, health, ownership, dependencies, participation, and recent work.
);
}
```
### 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/active-workplace`, 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": "active-workplace-live",
"_type": "module.active-workplace",
"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 depth 2, at most 100 nodes, conservative stale timing, comments enabled, and playback enabled. Add one default GoClaw watcher only after its represented user and approval policy are correct. Builder settings shape behavior but never replace server authorization.
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/active-workplace.html).
---
title: "Record relations"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/record-relations.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Record relations
Explores authorized records as a bounded relationship graph and supports typed relation creation/removal only when both endpoints are editable.
## In brief
- **Runtime key:** module.record-graph
- **Experience:** Connected context
- **Security:** Server-enforced
Use Record Relations to show how people, companies, work, files, conversations, events, and custom records connect without duplicating those records into a separate graph store.
[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.record-graph. 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
Explores authorized records as a bounded relationship graph and supports typed relation creation/removal only when both endpoints are editable.
## Key capabilities
- Search visible seed records across modules.
- Traverse secured relationships through bounded depth levels.
- Show nodes, typed edges, module, location, status, activity counts, and favorites.
- Add or remove a typed relation only when both record endpoints and the relation operation are authorized.
## Common uses
- Customer/account 360-degree context.
- Case, project, asset, and work dependency exploration.
- Impact analysis and relationship-first CRM experiences.
## How it connects
The graph is a view over canonical Records and RecordRelations. Opening a node returns to the appropriate native or dynamic module; the graph does not become a competing source of record data.
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
Every traversed node is independently checked for SaaS, tenant, DataRole, record, and location visibility. Inaccessible nodes and edges are omitted, and mutation requires edit authority on both endpoints.
- 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.record-graph` 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 the complete Record Relations page
The native block provides secured seed-record search, bounded graph traversal, typed edges, relation add/remove, favorites, dynamic fields, and related/contextual Knowledge Core.
```
{
"blocks": [
{ "_id": "connected-records", "_type": "module.record-graph", "props": {}, "children": [] }
]
}
```
### Custom unstyled React graph explorer
The seed picker uses the stable title/RecordId cursor returned by SQL. The graph itself is deliberately bounded to depth 1–5 and 2–200 nodes. `truncated` means the browser has a safe partial graph, not permission to fetch an unbounded one.
```
import { FormEvent, useEffect, useMemo, useState } from "react";
import type { RecordGraphResponse, RecordGraphSearchResponse } from "@buildwithhq/module-sdk";
import { getRecordGraph, searchRecordGraph } from "./api";
const empty: RecordGraphSearchResponse = { contractVersion: 1, pageNumber: 1, pageSize: 50, totalRecords: 0, totalPages: 0, totalIsExact: false, hasMore: false, modules: [], items: [] };
type Cursor = { afterTitle?: string; afterRecordId?: string };
export function UnstyledRecordRelations({ locationId = "", maxDepth = 2, maxNodes = 100 }) {
const [draft, setDraft] = useState("");
const [search, setSearch] = useState("");
const [moduleKey, setModuleKey] = useState("");
const [cursors, setCursors] = useState([{}]);
const [records, setRecords] = useState(empty);
const [graph, setGraph] = useState(null);
const [error, setError] = useState(null);
const cursor = cursors[cursors.length - 1];
useEffect(() => {
const controller = new AbortController();
searchRecordGraph(moduleKey, search, locationId, controller.signal, cursor.afterTitle, cursor.afterRecordId, 50)
.then(setRecords)
.catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "Records could not be loaded."); });
return () => controller.abort();
}, [cursor.afterRecordId, cursor.afterTitle, locationId, moduleKey, search]);
const byId = useMemo(() => new Map(graph?.nodes.map(node => [node.recordId, node]) || []), [graph]);
function submit(event: FormEvent) { event.preventDefault(); setCursors([{}]); setGraph(null); setSearch(draft.trim()); }
async function open(recordId: string) { setGraph(await getRecordGraph(recordId, maxDepth, undefined, maxNodes)); }
function next() { if (records.hasMore && records.nextRecordId) setCursors(current => [...current, { afterTitle: records.nextTitle || undefined, afterRecordId: records.nextRecordId || undefined }]); }
return
;
}
```
### Add/remove typed relations and favorite the seed
Use only relation keys returned in `graph.relationTypes`. The helper checks the displayed edit flags for a good UI, while SQL independently reauthorizes both endpoints and the relation operation.
```
import type { RecordGraphResponse } from "@buildwithhq/module-sdk";
import { addRecordGraphRelation, getRecordGraph, removeRecordGraphRelation, setRecordGraphFavorite } from "./api";
export async function connectRecords(graph: RecordGraphResponse, toRecordId: string, relationKey: string) {
const from = graph.nodes.find(node => node.recordId === graph.seedRecordId);
const to = graph.nodes.find(node => node.recordId === toRecordId);
if (!from?.canEdit || !to?.canEdit) throw new Error("Both visible records must be editable.");
if (!graph.relationTypes.some(type => type.relationKey === relationKey)) throw new Error("Choose a returned relation type.");
await addRecordGraphRelation(from.recordId, { toRecordId, relationKey });
return getRecordGraph(from.recordId, 2, undefined, 100);
}
export async function disconnectRecords(seedRecordId: string, recordRelationId: string) {
await removeRecordGraphRelation(recordRelationId);
return getRecordGraph(seedRecordId, 2, undefined, 100);
}
export async function toggleGraphFavorite(graph: RecordGraphResponse) {
const seed = graph.nodes.find(node => node.recordId === graph.seedRecordId);
if (!seed) throw new Error("The seed record is not visible.");
await setRecordGraphFavorite(seed.recordId, !seed.isFavorite);
return getRecordGraph(seed.recordId, 2, undefined, 100);
}
```
### Related and contextual Knowledge Core
Direct relations and bounded graph candidates are distinct. Set `requireAiRead: true` when assembling AI context so candidates must pass the additional AI-read gate; normal record visibility alone is not enough.
```
import { getContextualKnowledge, getRelatedKnowledge, linkKnowledgeToRecord, unlinkKnowledgeFromRecord } from "./api";
export async function loadRecordKnowledge(recordId: string) {
const [direct, aiCandidates] = await Promise.all([
getRelatedKnowledge(recordId, 25),
getContextualKnowledge(recordId, true, 25),
]);
return { direct: direct.items, aiCandidates: aiCandidates.items };
}
export async function linkApprovedKnowledge(recordId: string, knowledgeRecordId: string) {
return linkKnowledgeToRecord(recordId, { knowledgeRecordId, relationKey: "related-knowledge" });
}
export async function unlinkApprovedKnowledge(recordRelationId: string) {
return unlinkKnowledgeFromRecord(recordRelationId);
}
```
### Optional starter styling
```
.custom-record-relations { width: 100%; max-width: none; }
.custom-record-relations > form, .relation-columns nav { align-items: end; display: flex; flex-wrap: wrap; gap: .75rem; }
.relation-columns { display: grid; gap: 1rem; grid-template-columns: 280px minmax(0, 1fr); margin-top: 1rem; }
.relation-columns ol { list-style: none; margin: 0; padding: 0; }
.relation-columns aside li button, .relation-columns main section button { background: transparent; border: 0; display: grid; padding: .65rem; text-align: left; width: 100%; }
.relation-columns main { border: 1px solid var(--line, #d8dee8); padding: 1rem; }
.relation-columns main section { border-top: 1px solid var(--line, #d8dee8); padding-top: .75rem; }
@media (max-width: 760px) { .relation-columns { grid-template-columns: 1fr; } }
```
### Exact data path
| Purpose | Route | Procedure |
| --- | --- | --- |
| Cursor seed search | `GET /api/modules/record-graph/records` | `sp_RecordGraph_SearchSecured` |
| Bounded graph | `GET /api/modules/record-graph/{recordId}` | `sp_RecordGraph_GetSecured` |
| Add/remove relation | `PUT /{recordId}/relations`, `DELETE /relations/{recordRelationId}` | `sp_RecordGraph_AddRelationSecured`, `sp_RecordGraph_RemoveRelationSecured` |
| Favorite | `PUT /{recordId}/favorite` | `sp_Favorites_SetSecured` |
| Knowledge reads | `GET /{recordId}/knowledge`, `GET /{recordId}/knowledge/candidates` | `sp_kc_GetRelatedKnowledgeSecured`, `sp_kc_GetContextualKnowledgeCandidatesSecured` |
| Knowledge link/unlink | `PUT /{recordId}/knowledge`, `DELETE /knowledge-relations/{recordRelationId}` | `sp_kc_LinkKnowledgeToRecord`, `sp_kc_UnlinkKnowledgeFromRecord` |
The canonical `RecordRelations` rows are the relationship source. BuildWithHQ does not require a massive pre-expanded user/role/location graph table: each bounded read intersects current record, DataRole, location, direct-conversation, and AI-read policy before returning nodes or knowledge.
## Build a professional Record relations dashboard
These six registry-backed presentation blocks let a designer turn the secured Record relations 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/record-graph`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/record-relations-sample.json`, its executable authenticated page at `pages/first-class/record-relations-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 Record relations projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Visible relations, connected records, types, and recent links |
| `entity-list` | `core.entity-list` | Highly connected records inside the current scope |
| `progress-list` | `core.progress-list` | Distribution by relation type or owning module |
| `series-chart` | `core.series-chart` | Relationship creation and removal |
| `data-grid` | `core.presentation-grid` | One bounded edge page with both endpoints authorized |
| `timeline` | `core.timeline` | Create, update, remove, source loss, and graph traversal 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": "record-relations-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Relationship overview",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "relations",
"label": "Visible relations",
"value": 3218,
"format": "number",
"tone": "neutral"
},
{
"key": "records",
"label": "Connected records",
"value": 1407,
"format": "number",
"tone": "primary"
},
{
"key": "types",
"label": "Relation types",
"value": 18,
"format": "number",
"tone": "success"
},
{
"key": "new",
"label": "Added this week",
"value": 96,
"format": "number",
"tone": "warning"
}
]
},
"children": []
},
{
"_id": "record-relations-recent",
"_type": "core.entity-list",
"props": {
"title": "Highly connected records",
"hasMore": true,
"items": [
{
"id": "record-relations-sample-1",
"recordId": "record-relations-record-1",
"primary": "Acme Field Services",
"secondary": "Contact - 34 visible relations",
"status": {
"key": "connected",
"label": "Connected",
"tone": "success"
},
"trailing": "34 links"
},
{
"id": "record-relations-sample-2",
"recordId": "record-relations-record-2",
"primary": "Acme Field Services - Follow-up",
"secondary": "Contact - 34 visible relations - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "record-relations-sample-3",
"recordId": "record-relations-record-3",
"primary": "Acme Field Services - West region",
"secondary": "Contact - 34 visible relations - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "record-relations-sample-4",
"recordId": "record-relations-record-4",
"primary": "Acme Field Services - Customer response",
"secondary": "Contact - 34 visible relations - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "record-relations-sample-5",
"recordId": "record-relations-record-5",
"primary": "Acme Field Services - Regional operations review with a deliberately long title",
"secondary": "Contact - 34 visible relations - 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": "record-relations-sample-6",
"recordId": "record-relations-record-6",
"primary": "Acme Field Services - Completed preview",
"secondary": "Contact - 34 visible relations - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "record-relations-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Relations by type",
"items": [
{
"key": "customer",
"label": "Customer of",
"value": 1210,
"maximum": 3218,
"displayValue": "1210",
"tone": "primary",
"status": {
"key": "customer",
"label": "Customer of",
"tone": "primary"
}
},
{
"key": "attached",
"label": "Attached to",
"value": 1084,
"maximum": 3218,
"displayValue": "1084",
"tone": "success",
"status": {
"key": "attached",
"label": "Attached to",
"tone": "success"
}
},
{
"key": "depends",
"label": "Depends on",
"value": 924,
"maximum": 3218,
"displayValue": "924",
"tone": "warning",
"status": {
"key": "depends",
"label": "Depends on",
"tone": "warning"
}
}
]
},
"children": []
},
{
"_id": "record-relations-trend",
"_type": "core.series-chart",
"props": {
"title": "Relationship 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": "Removed",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "record-relations-table",
"_type": "core.presentation-grid",
"props": {
"title": "Visible relationships",
"columns": [
{
"key": "from",
"label": "From",
"type": "text",
"align": "left"
},
{
"key": "relation",
"label": "Relationship",
"type": "text",
"align": "left"
},
{
"key": "to",
"label": "To",
"type": "text",
"align": "left"
},
{
"key": "status",
"label": "Status",
"type": "status",
"align": "left"
},
{
"key": "created",
"label": "Created",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "record-relations-row-1",
"recordId": "record-relations-record-1",
"cells": {
"from": "Acme Field Services",
"relation": "Customer of",
"to": "WO-1048",
"status": {
"key": "active",
"label": "Active",
"tone": "success"
},
"created": "2026-09-04T11:11:00Z"
}
},
{
"id": "record-relations-row-2",
"recordId": "record-relations-record-2",
"cells": {
"from": "Acme Field Services - Follow-up",
"relation": "Customer of",
"to": "WO-1048",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"created": "2026-09-04T15:42:00Z"
}
},
{
"id": "record-relations-row-3",
"recordId": "record-relations-record-3",
"cells": {
"from": "Acme Field Services - West region",
"relation": "Customer of",
"to": "WO-1048",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"created": "2026-09-04T12:18:00Z"
}
},
{
"id": "record-relations-row-4",
"recordId": "record-relations-record-4",
"cells": {
"from": "Acme Field Services - Customer response",
"relation": "Customer of",
"to": "WO-1048",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"created": "2026-09-03T21:07:00Z"
}
},
{
"id": "record-relations-row-5",
"recordId": "record-relations-record-5",
"cells": {
"from": "Acme Field Services - Regional operations review with a deliberately long title",
"relation": "Customer of - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"to": "WO-1048",
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"created": "2026-09-03T16:31:00Z"
}
},
{
"id": "record-relations-row-6",
"recordId": "record-relations-record-6",
"cells": {
"from": "Acme Field Services - Completed preview",
"relation": "Customer of",
"to": "WO-1048",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"created": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 3218,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "record-relations-timeline",
"_type": "core.timeline",
"props": {
"title": "Relationship activity",
"hasMore": true,
"items": [
{
"id": "record-relations-event-1",
"recordId": "record-relations-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Relationship created",
"description": "Acme Field Services was related to WO-1048.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "record-relations-event-2",
"recordId": "record-relations-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Relationship created - Follow-up",
"description": "Acme Field Services was related to WO-1048. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "record-relations-event-3",
"recordId": "record-relations-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Relationship created - West region",
"description": "Acme Field Services was related to WO-1048. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "record-relations-event-4",
"recordId": "record-relations-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Relationship created - Customer response",
"description": "Acme Field Services was related to WO-1048. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "record-relations-event-5",
"recordId": "record-relations-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Relationship created - Regional operations review with a deliberately long title",
"description": "Acme Field Services was related to WO-1048. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "record-relations-event-6",
"recordId": "record-relations-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Relationship created - Completed preview",
"description": "Acme Field Services was related to WO-1048. 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 RecordRelationsPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface RecordRelationsPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: RecordRelationsPresentationData | 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 RecordRelationsPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: RecordRelationsPresentationProps) {
if (error) return
The Record relations presentation could not be loaded.
;
if (loading || !data) return
Loading Record relations presentation...
;
return (
Record relations
Relationship overview
Authorized connections, relation types, highly connected records, and graph changes.
);
}
```
### 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/record-graph`, 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": "record-relations-live",
"_type": "module.record-graph",
"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
Keep relation types meaningful and limited. A consistent typed relationship vocabulary produces better navigation, automation, and AI context than many near-duplicate labels.
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/record-relations.html).
---
title: "Record activity"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/record-activity.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Record activity
Shows a paged, filterable, chronological activity timeline for an authorized record with actor, event type/state, date range, relations, and favorite state.
## In brief
- **Runtime key:** module.record-activity
- **Experience:** Timeline & history
- **Security:** Server-enforced
Use Record Activity to give users an understandable operational history of what happened around a record without exposing raw database auditing or internal event payloads.
[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.record-activity. 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
Shows a paged, filterable, chronological activity timeline for an authorized record with actor, event type/state, date range, relations, and favorite state.
## Key capabilities
- Search visible records across modules and select a record timeline.
- Filter activity by event type and date range.
- Group events chronologically with summary, actor, state, and occurrence time.
- Page bounded history and show secured relation/favorite context.
## Common uses
- Customer, project, work-order, case, and asset timelines.
- Operational handoffs and investigation context.
- A user-facing history adjacent to formal compliance/audit evidence.
## How it connects
Activity aggregates the reviewed events emitted by native and dynamic records. Formal security/compliance evidence remains in its audit contracts; the activity module provides the bounded business-facing view.
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 selected record must pass normal scope checks, and only safe activity summaries/metadata are rendered. Private event detail payloads stay outside the browser surface.
- 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.record-activity` 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 the complete Record Activity page
The native block supplies secured record selection, event/date filtering, bounded timeline pages, actor/state summaries, dynamic fields, relation counts, and favorites.
```
{
"blocks": [
{ "_id": "record-history", "_type": "module.record-activity", "props": {}, "children": [] }
]
}
```
### Custom unstyled React record picker and timeline
The record picker uses the stable title/RecordId cursor. After selection, activity uses independent pages of at most 100 safe event summaries. This component deliberately does not render the internal `detail` object.
```
import { FormEvent, useEffect, useState } from "react";
import type { RecordActivityResponse, RecordActivitySearchResponse } from "@buildwithhq/module-sdk";
import { getRecordActivity, searchRecordActivity } from "./api";
const empty: RecordActivitySearchResponse = { contractVersion: 1, pageNumber: 1, pageSize: 50, totalRecords: 0, totalPages: 0, totalIsExact: false, hasMore: false, modules: [], items: [] };
type Cursor = { afterTitle?: string; afterRecordId?: string };
const startOfDayUtc = (date: string) => date ? `${date}T00:00:00.000Z` : "";
const endOfDayUtc = (date: string) => date ? `${date}T23:59:59.999Z` : "";
export function UnstyledRecordActivity({ locationId = "" }) {
const [draft, setDraft] = useState("");
const [search, setSearch] = useState("");
const [moduleKey, setModuleKey] = useState("");
const [cursors, setCursors] = useState([{}]);
const [records, setRecords] = useState(empty);
const [activity, setActivity] = useState(null);
const [activityType, setActivityType] = useState("");
const [fromDate, setFromDate] = useState("");
const [toDate, setToDate] = useState("");
const [error, setError] = useState(null);
const cursor = cursors[cursors.length - 1];
useEffect(() => {
const controller = new AbortController();
searchRecordActivity(moduleKey, search, locationId, controller.signal, cursor.afterTitle, cursor.afterRecordId, 50)
.then(setRecords)
.catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "Records could not be loaded."); });
return () => controller.abort();
}, [cursor.afterRecordId, cursor.afterTitle, locationId, moduleKey, search]);
async function loadTimeline(recordId: string, page = 1) { setActivity(await getRecordActivity(recordId, activityType, startOfDayUtc(fromDate), endOfDayUtc(toDate), page, undefined, 50)); }
function submit(event: FormEvent) { event.preventDefault(); setCursors([{}]); setActivity(null); setSearch(draft.trim()); }
function nextRecords() { if (records.hasMore && records.nextRecordId) setCursors(current => [...current, { afterTitle: records.nextTitle || undefined, afterRecordId: records.nextRecordId || undefined }]); }
return
Record activity
{error &&
{error}
}
{activity ? <>
{activity.title}
{activity.moduleName} - {activity.recordStatus || "No status"} - {activity.relationCount} relations
;
}
```
### Metadata change history
Supported native record pages can show a separate change-history panel. Its signed-in tenant route is `GET /api/modules/record-activity/{recordId}/history`; it is not part of the published Developer API. It returns event ID, change type, actor user ID and occurrence time, without stored field values or before/after snapshots. Use `pageSize` (default 25, maximum 100) and pass the returned `nextBeforeId` string as `beforeId` when `hasMore` is true.
The service rechecks current record authority before and after reading the app's routed Logs database. Personal ownership, DataRole and location rules still apply; access to this page or an old event does not grant access to the record. A missing location label is omitted rather than interpreted as All locations. History coverage depends on the owning module and action: the native Work Order status and assignment actions, Web Link creates/updates and dynamic record creates/updates have been verified; this does not promise that every module action emits a change event.
### Favorite the source record
Activity is read-only by design. There is no generic “create activity” route: Contacts, Work Orders, workflows, Conversations, and other owning modules emit reviewed business events as their real operations occur.
```
import type { RecordActivityResponse } from "@buildwithhq/module-sdk";
import { getRecordActivity, setRecordActivityFavorite } from "./api";
export async function toggleActivityRecordFavorite(current: RecordActivityResponse) {
await setRecordActivityFavorite(current.recordId, !current.isFavorite);
return getRecordActivity(current.recordId, "", "", "", 1, undefined, 50);
}
```
### Optional starter styling
```
.custom-record-activity { width: 100%; max-width: none; }
.custom-record-activity > form, .activity-columns form, .activity-columns nav { align-items: end; display: flex; flex-wrap: wrap; gap: .75rem; }
.activity-columns { display: grid; gap: 1rem; grid-template-columns: 280px minmax(0, 1fr); margin-top: 1rem; }
.activity-columns ol { list-style: none; margin: 0; padding: 0; }
.activity-columns aside li button { background: transparent; border: 0; display: grid; padding: .65rem; text-align: left; width: 100%; }
.activity-columns main { border: 1px solid var(--line, #d8dee8); padding: 1rem; }
.timeline li { border-left: 3px solid var(--accent, #4967d5); display: grid; gap: .2rem; margin: .75rem 0; padding: .25rem .75rem; }
@media (max-width: 760px) { .activity-columns { grid-template-columns: 1fr; } }
```
### Exact data path
| Purpose | Route | Procedure |
| --- | --- | --- |
| Cursor record search | `GET /api/modules/record-activity/records` | `sp_RecordActivity_SearchSecured` |
| Paged safe timeline | `GET /api/modules/record-activity/{recordId}` | `sp_RecordActivity_GetSecured` |
| Metadata change history (native tenant route) | `GET /api/modules/record-activity/{recordId}/history` | Core `sp_RecordActivity_GetSecured` authority checks, then Logs `sp_RecordHistory_ReadInternal` |
| Favorite source | `PUT /api/modules/record-activity/{recordId}/favorite` | `sp_Favorites_SetSecured` |
Record search accepts up to 200 rows but the example uses 50; timeline detail accepts up to 100. This user-facing history is not the immutable security/audit log, and safe summaries should not expose raw event payloads.
## Build a professional Record activity dashboard
These six registry-backed presentation blocks let a designer turn the secured Record activity 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/record-activity`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/record-activity-sample.json`, its executable authenticated page at `pages/first-class/record-activity-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 Record activity projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Event volume, creates, updates, workflow, and attention counts |
| `entity-list` | `core.entity-list` | Records with the newest visible activity |
| `progress-list` | `core.progress-list` | Distribution by module, event type, actor, or state |
| `series-chart` | `core.series-chart` | Created, updated, related, approved, and completed events |
| `data-grid` | `core.presentation-grid` | One bounded safe-summary event page |
| `timeline` | `core.timeline` | Chronological safe summaries without private event payloads |
### 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": "record-activity-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Record activity overview",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "events",
"label": "Events today",
"value": 486,
"format": "number",
"tone": "primary"
},
{
"key": "creates",
"label": "Records created",
"value": 74,
"format": "number",
"tone": "success"
},
{
"key": "updates",
"label": "Records updated",
"value": 361,
"format": "number",
"tone": "neutral"
},
{
"key": "attention",
"label": "Attention events",
"value": 12,
"format": "number",
"tone": "warning"
}
]
},
"children": []
},
{
"_id": "record-activity-recent",
"_type": "core.entity-list",
"props": {
"title": "Recently active records",
"hasMore": true,
"items": [
{
"id": "record-activity-sample-1",
"recordId": "record-activity-record-1",
"primary": "WO-1048 - North Wing closeout",
"secondary": "Work Orders - 8 events",
"status": {
"key": "updated",
"label": "Updated",
"tone": "primary"
},
"trailing": "2 min ago"
},
{
"id": "record-activity-sample-2",
"recordId": "record-activity-record-2",
"primary": "WO-1048 - North Wing closeout - Follow-up",
"secondary": "Work Orders - 8 events - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "record-activity-sample-3",
"recordId": "record-activity-record-3",
"primary": "WO-1048 - North Wing closeout - West region",
"secondary": "Work Orders - 8 events - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "record-activity-sample-4",
"recordId": "record-activity-record-4",
"primary": "WO-1048 - North Wing closeout - Customer response",
"secondary": "Work Orders - 8 events - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "record-activity-sample-5",
"recordId": "record-activity-record-5",
"primary": "WO-1048 - North Wing closeout - Regional operations review with a deliberately long title",
"secondary": "Work Orders - 8 events - 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": "record-activity-sample-6",
"recordId": "record-activity-record-6",
"primary": "WO-1048 - North Wing closeout - Completed preview",
"secondary": "Work Orders - 8 events - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "record-activity-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Activity by module",
"items": [
{
"key": "work",
"label": "Work Orders",
"value": 181,
"maximum": 486,
"displayValue": "181",
"tone": "primary",
"status": {
"key": "work",
"label": "Work Orders",
"tone": "primary"
}
},
{
"key": "contacts",
"label": "Contacts",
"value": 163,
"maximum": 486,
"displayValue": "163",
"tone": "success",
"status": {
"key": "contacts",
"label": "Contacts",
"tone": "success"
}
},
{
"key": "files",
"label": "Files",
"value": 142,
"maximum": 486,
"displayValue": "142",
"tone": "neutral",
"status": {
"key": "files",
"label": "Files",
"tone": "neutral"
}
}
]
},
"children": []
},
{
"_id": "record-activity-trend",
"_type": "core.series-chart",
"props": {
"title": "Activity volume",
"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": "record-activity-table",
"_type": "core.presentation-grid",
"props": {
"title": "Authorized activity events",
"columns": [
{
"key": "record",
"label": "Record",
"type": "text",
"align": "left"
},
{
"key": "module",
"label": "Module",
"type": "text",
"align": "left"
},
{
"key": "event",
"label": "Event",
"type": "text",
"align": "left"
},
{
"key": "actor",
"label": "Actor",
"type": "text",
"align": "left"
},
{
"key": "occurred",
"label": "Occurred",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "record-activity-row-1",
"recordId": "record-activity-record-1",
"cells": {
"record": "WO-1048",
"module": "Work Orders",
"event": "Status changed",
"actor": "Maya Chen",
"occurred": "2026-09-04T17:57:00Z"
}
},
{
"id": "record-activity-row-2",
"recordId": "record-activity-record-2",
"cells": {
"record": "WO-1048 - Follow-up",
"module": "Work Orders",
"event": "Status changed",
"actor": "Maya Chen",
"occurred": "2026-09-04T15:42:00Z"
}
},
{
"id": "record-activity-row-3",
"recordId": "record-activity-record-3",
"cells": {
"record": "WO-1048 - West region",
"module": "Work Orders",
"event": "Status changed",
"actor": "Maya Chen",
"occurred": "2026-09-04T12:18:00Z"
}
},
{
"id": "record-activity-row-4",
"recordId": "record-activity-record-4",
"cells": {
"record": "WO-1048 - Customer response",
"module": "Work Orders",
"event": "Status changed",
"actor": "Maya Chen",
"occurred": "2026-09-03T21:07:00Z"
}
},
{
"id": "record-activity-row-5",
"recordId": "record-activity-record-5",
"cells": {
"record": "WO-1048 - Regional operations review with a deliberately long title",
"module": "Work Orders - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"event": "Status changed",
"actor": "Maya Chen",
"occurred": "2026-09-03T16:31:00Z"
}
},
{
"id": "record-activity-row-6",
"recordId": "record-activity-record-6",
"cells": {
"record": "WO-1048 - Completed preview",
"module": "Work Orders",
"event": "Status changed",
"actor": "Maya Chen",
"occurred": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 486,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "record-activity-timeline",
"_type": "core.timeline",
"props": {
"title": "Recent authorized activity",
"hasMore": true,
"items": [
{
"id": "record-activity-event-1",
"recordId": "record-activity-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Status changed",
"description": "WO-1048 moved from In progress to Ready for signoff.",
"actor": "Maya Chen",
"tone": "primary"
},
{
"id": "record-activity-event-2",
"recordId": "record-activity-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Status changed - Follow-up",
"description": "WO-1048 moved from In progress to Ready for signoff. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "record-activity-event-3",
"recordId": "record-activity-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Status changed - West region",
"description": "WO-1048 moved from In progress to Ready for signoff. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "record-activity-event-4",
"recordId": "record-activity-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Status changed - Customer response",
"description": "WO-1048 moved from In progress to Ready for signoff. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "record-activity-event-5",
"recordId": "record-activity-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Status changed - Regional operations review with a deliberately long title",
"description": "WO-1048 moved from In progress to Ready for signoff. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "record-activity-event-6",
"recordId": "record-activity-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Status changed - Completed preview",
"description": "WO-1048 moved from In progress to Ready for signoff. 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 RecordActivityPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface RecordActivityPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: RecordActivityPresentationData | 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 RecordActivityPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: RecordActivityPresentationProps) {
if (error) return
The Record activity presentation could not be loaded.
;
if (loading || !data) return
Loading Record activity presentation...
;
return (
Record activity
Record activity overview
Safe event summaries, actors, source modules, status changes, and recent authorized history.
);
}
```
### 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/record-activity`, 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": "record-activity-live",
"_type": "module.record-activity",
"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
Write concise activity summaries when workflows change important state. Users should understand the event without needing internal JSON or database knowledge.
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/record-activity.html).
---
title: "Global secured search"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/global-search.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Global secured search
Searches permitted records across modules and also uses permission-filtered vector retrieval to find meaningfully related information in indexed uploads, PDFs, images, screenshots, tables, diagrams, and text.
## In brief
- **Runtime key:** module.global-search
- **Experience:** Cross-module discovery
- **Security:** Server-enforced
Use Global Search as the universal finder when users need to locate customers, files, work, conversations, events, custom records, or related evidence from uploaded content in one place.
[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.global-search. 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
Searches permitted records across modules and also uses permission-filtered vector retrieval to find meaningfully related information in indexed uploads, PDFs, images, screenshots, tables, diagrams, and text.
## Key capabilities
- Run bounded text search across authorized modules and records.
- Search eligible indexed uploads, PDFs, images, screenshots, tables, diagrams, and extracted text in a shared vector space.
- Find conceptually related information even when the source uses different words from the query.
- Retrieve the original text, page, image, and source identity so results can retain evidence and citations.
- Filter by module and page through stable result envelopes.
- Show safe matched-field snippets plus module, status, location, relation, activity, and update context.
- Add or remove favorites without leaving the result list.
## How vector and AI search improve discovery
Global Search has two complementary secured paths. Operational record search is best for exact names, identifiers, fields, statuses, and module records. The AI search path vectorizes eligible indexed content so a query can match by meaning, not only by identical keywords.
| Search path | What it finds | Why it helps |
| --- | --- | --- |
| Record and keyword | Exact or close text in permitted records and fields | Fast, predictable lookup for known business data. |
| Semantic vector | Related passages and documents that express the same idea with different wording | Surfaces useful context that keyword-only search can miss. |
| Multimodal vector | Relevant uploads, PDF pages, images, screenshots, charts, tables, and diagrams | Lets a text question find visual evidence, including information that weak OCR or text extraction may lose. |
| AI relevance and reranking | The strongest authorized candidates, with their original source and page context | Brings more closely related information toward the top while preserving evidence for review. |
For example, a search for *declining renewal risk* can surface a customer record, a differently worded conversation, a chart on an uploaded PDF page, and a screenshot containing relevant evidence—provided each source is indexed, AI-eligible, and visible to the current user.
AI relevance is a discovery aid, not proof that every result is correct. Users can open the original source, page, or visual evidence to verify the result in context.
## Common uses
- App-wide command and search experiences.
- Finding a record when its owning module is unknown.
- Discovering related material across records and uploaded documents.
- Finding a PDF page, image, screenshot, chart, table, or diagram from a natural-language description.
- Cross-module navigation in relationship-heavy applications.
## How it connects
Record results point to the canonical record in its owning module. AI search uses the platform's secured source, document-element, chunk, visual-index, and vector retrieval chain; it returns original source/page/asset references instead of copying the evidence into a new business record.
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 applies SaaS, tenant, CanAiReadRecords, DataRole, location, record, field, and AI-eligibility rules before vector candidates or visual evidence can be returned. Unindexed, deleted, AI-excluded, or otherwise unauthorized content is absent from results. Relevance never grants access, and counts, snippets, citations, and related context must not reveal inaccessible records or fields.
- 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.global-search` 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 the complete Global Search page
The native block gives users bounded cross-module record search, server-provided matched fields, module/location context, relation/activity counts, paging, and favorites.
```
{
"blocks": [
{ "_id": "find-anything", "_type": "module.global-search", "props": {}, "children": [] }
]
}
```
### Custom unstyled React search
Search requests at most 100 already-authorized rows per page, within a 5,000-match browsing window. Refine filters to reach other records. `includeTotal=false` is the default; `hasMore` is the safe next-page signal. The separate secured count endpoint can return the full authorized total without delaying row rendering. See [progressive loading and exact counts](../api/progressive-loading-and-counts.html); a missing or failed count is not zero. Do not download every module and filter it in the browser.
```
import { FormEvent, useEffect, useRef, useState } from "react";
import type { GlobalSearchItem, GlobalSearchResponse } from "@buildwithhq/module-sdk";
import { searchGlobalRecords } from "./api";
const empty: GlobalSearchResponse = { contractVersion: 1, query: "", pageNumber: 1, pageSize: 50, totalRecords: 0, totalPages: 0, totalIsExact: false, hasMore: false, modules: [], items: [] };
export function UnstyledGlobalSearch({ locationId = "", onOpenRecord }: { locationId?: string; onOpenRecord?: (item: GlobalSearchItem) => void }) {
const [draft, setDraft] = useState("");
const [query, setQuery] = useState("");
const [moduleKey, setModuleKey] = useState("");
const [page, setPage] = useState(1);
const [result, setResult] = useState(empty);
const [error, setError] = useState(null);
const request = useRef(0);
useEffect(() => {
const version = ++request.current;
const controller = new AbortController();
searchGlobalRecords(query, moduleKey, page, controller.signal, locationId, 50)
.then(value => { if (version === request.current) setResult(value); })
.catch((caught: unknown) => { if (!controller.signal.aborted && version === request.current) setError(caught instanceof Error ? caught.message : "Search failed."); });
return () => controller.abort();
}, [locationId, moduleKey, page, query]);
function submit(event: FormEvent) {
event.preventDefault();
const normalized = draft.trim();
if (normalized && normalized.length < 2) { setError("Enter at least two characters."); return; }
setError(null); setPage(1); setQuery(normalized);
}
return
)};
}
```
### Favorite a result without broadening access
The result's `moduleKey` tells the shell which owning module should open it. There is intentionally no generic edit call: writes stay with the owning module's typed API.
```
import type { GlobalSearchItem } from "@buildwithhq/module-sdk";
import { searchGlobalRecords, setGlobalSearchFavorite } from "./api";
export async function toggleSearchFavorite(item: GlobalSearchItem) {
return setGlobalSearchFavorite(item.recordId, !item.isFavorite);
}
export function reloadCurrentSearch(query: string, moduleKey: string, page: number, locationId = "") {
return searchGlobalRecords(query, moduleKey, page, undefined, locationId, 50);
}
```
### Optional starter styling
```
.custom-global-search { width: 100%; max-width: none; }
.custom-global-search form, .custom-global-search nav { align-items: end; display: flex; flex-wrap: wrap; gap: .75rem; }
.custom-global-search > ol { list-style: none; margin: 1rem 0; padding: 0; }
.custom-global-search > ol > li { border-bottom: 1px solid var(--line, #d8dee8); padding: 1rem 0; }
.custom-global-search article header { display: flex; gap: .75rem; justify-content: space-between; }
.custom-global-search article dl > div { display: grid; gap: .5rem; grid-template-columns: minmax(110px, 180px) minmax(0, 1fr); }
.custom-global-search article button { margin-top: .75rem; }
```
### Exact data path and the AI-search boundary
| Purpose | Route | Procedure |
| --- | --- | --- |
| Operational record search | `GET /api/modules/global-search` | `sp_GlobalSearch_SearchSecured` |
| Favorite result | `PUT /api/modules/global-search/records/{recordId}/favorite` | `sp_Favorites_SetSecured` |
The component above uses the current operational record-search contract; it does not falsely label a keyword request as vector search. Semantic and multimodal retrieval must use a separately published, validated AI binding that applies `CanAiReadRecords`, DataRole, location, field, source, and AI-eligibility filtering before candidates reach React. Keep interactive lookup bounded; use the governed AI-insights/data-graph path for whole authorized-graph analysis.
## Build a professional Global secured search dashboard
These six registry-backed presentation blocks let a designer turn the secured Global secured search 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/global-search`; 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.70](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.70.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/global-search-sample.json`, its executable authenticated page at `pages/first-class/global-search-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 Global secured search projection |
| --- | --- | --- |
| `metric-set` | `core.metric-strip` | Searches, opened results, visual matches, and zero-result counts |
| `entity-list` | `core.entity-list` | Recent results the current user was allowed to open |
| `progress-list` | `core.progress-list` | Distribution by module, content type, or retrieval lane |
| `series-chart` | `core.series-chart` | Searches, result opens, favorites, and follow-on actions |
| `data-grid` | `core.presentation-grid` | One bounded page of safe result titles and snippets |
| `timeline` | `core.timeline` | Query, retrieval, open, favorite, feedback, and permission-loss 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": "global-search-metrics",
"_type": "core.metric-strip",
"props": {
"title": "Search activity and discovery",
"asOfUtc": "2026-09-04T18:00:00Z",
"items": [
{
"key": "searches",
"label": "Searches today",
"value": 486,
"format": "number",
"tone": "primary"
},
{
"key": "records",
"label": "Record results opened",
"value": 214,
"format": "number",
"tone": "success"
},
{
"key": "visual",
"label": "Visual matches",
"value": 38,
"format": "number",
"tone": "neutral"
},
{
"key": "zero",
"label": "Zero-result queries",
"value": 17,
"format": "number",
"tone": "warning"
}
]
},
"children": []
},
{
"_id": "global-search-recent",
"_type": "core.entity-list",
"props": {
"title": "Recently opened results",
"hasMore": true,
"items": [
{
"id": "global-search-sample-1",
"recordId": "global-search-record-1",
"primary": "North Wing closeout",
"secondary": "Work Orders - exact and semantic match",
"status": {
"key": "relevant",
"label": "Relevant",
"tone": "success"
},
"trailing": "94%"
},
{
"id": "global-search-sample-2",
"recordId": "global-search-record-2",
"primary": "North Wing closeout - Follow-up",
"secondary": "Work Orders - exact and semantic match - Updated two hours ago by the assigned owner",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"trailing": "Today"
},
{
"id": "global-search-sample-3",
"recordId": "global-search-record-3",
"primary": "North Wing closeout - West region",
"secondary": "Work Orders - exact and semantic match - Related to three visible records at the Reno location",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"trailing": "3 related"
},
{
"id": "global-search-sample-4",
"recordId": "global-search-record-4",
"primary": "North Wing closeout - Customer response",
"secondary": "Work Orders - exact and semantic match - Waiting for an external response before work can continue",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"trailing": "Tomorrow"
},
{
"id": "global-search-sample-5",
"recordId": "global-search-record-5",
"primary": "North Wing closeout - Regional operations review with a deliberately long title",
"secondary": "Work Orders - exact and semantic match - 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": "global-search-sample-6",
"recordId": "global-search-record-6",
"primary": "North Wing closeout - Completed preview",
"secondary": "Work Orders - exact and semantic match - Closed after review with its related evidence retained",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"trailing": "Closed"
}
]
},
"children": []
},
{
"_id": "global-search-bystatus",
"_type": "core.progress-list",
"props": {
"title": "Results by source",
"items": [
{
"key": "records",
"label": "Business records",
"value": 267,
"maximum": 486,
"displayValue": "267",
"tone": "primary",
"status": {
"key": "records",
"label": "Business records",
"tone": "primary"
}
},
{
"key": "documents",
"label": "Documents",
"value": 143,
"maximum": 486,
"displayValue": "143",
"tone": "success",
"status": {
"key": "documents",
"label": "Documents",
"tone": "success"
}
},
{
"key": "visuals",
"label": "Images and diagrams",
"value": 76,
"maximum": 486,
"displayValue": "76",
"tone": "neutral",
"status": {
"key": "visuals",
"label": "Images and diagrams",
"tone": "neutral"
}
}
]
},
"children": []
},
{
"_id": "global-search-trend",
"_type": "core.series-chart",
"props": {
"title": "Search and result activity",
"variant": "bar",
"defaultPeriodKey": "d7",
"periods": [
{
"key": "d7",
"label": "7 days",
"labels": [
"Fri",
"Sat",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu"
],
"series": [
{
"key": "primary",
"label": "Searches",
"tone": "primary",
"values": [
8,
5,
4,
12,
15,
11,
17
]
},
{
"key": "secondary",
"label": "Results opened",
"tone": "success",
"values": [
6,
4,
3,
9,
12,
10,
14
]
}
]
}
]
},
"children": []
},
{
"_id": "global-search-table",
"_type": "core.presentation-grid",
"props": {
"title": "Recent authorized discoveries",
"columns": [
{
"key": "result",
"label": "Result",
"type": "text",
"align": "left"
},
{
"key": "module",
"label": "Module",
"type": "text",
"align": "left"
},
{
"key": "match",
"label": "Match",
"type": "text",
"align": "left"
},
{
"key": "status",
"label": "State",
"type": "status",
"align": "left"
},
{
"key": "opened",
"label": "Opened",
"type": "date",
"align": "left"
}
],
"rows": [
{
"id": "global-search-row-1",
"recordId": "global-search-record-1",
"cells": {
"result": "North Wing closeout",
"module": "Work Orders",
"match": "Title and related PDF",
"status": {
"key": "relevant",
"label": "Relevant",
"tone": "success"
},
"opened": "2026-09-04T17:36:00Z"
}
},
{
"id": "global-search-row-2",
"recordId": "global-search-record-2",
"cells": {
"result": "North Wing closeout - Follow-up",
"module": "Work Orders",
"match": "Title and related PDF",
"status": {
"key": "in-review",
"label": "In review",
"tone": "primary"
},
"opened": "2026-09-04T15:42:00Z"
}
},
{
"id": "global-search-row-3",
"recordId": "global-search-record-3",
"cells": {
"result": "North Wing closeout - West region",
"module": "Work Orders",
"match": "Title and related PDF",
"status": {
"key": "on-track",
"label": "On track",
"tone": "success"
},
"opened": "2026-09-04T12:18:00Z"
}
},
{
"id": "global-search-row-4",
"recordId": "global-search-record-4",
"cells": {
"result": "North Wing closeout - Customer response",
"module": "Work Orders",
"match": "Title and related PDF",
"status": {
"key": "scheduled",
"label": "Scheduled",
"tone": "warning"
},
"opened": "2026-09-03T21:07:00Z"
}
},
{
"id": "global-search-row-5",
"recordId": "global-search-record-5",
"cells": {
"result": "North Wing closeout - Regional operations review with a deliberately long title",
"module": "Work Orders - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"match": "Title and related PDF",
"status": {
"key": "needs-attention",
"label": "Needs attention",
"tone": "danger"
},
"opened": "2026-09-03T16:31:00Z"
}
},
{
"id": "global-search-row-6",
"recordId": "global-search-record-6",
"cells": {
"result": "North Wing closeout - Completed preview",
"module": "Work Orders",
"match": "Title and related PDF",
"status": {
"key": "complete",
"label": "Complete",
"tone": "success"
},
"opened": "2026-09-02T19:14:00Z"
}
}
],
"page": {
"pageNumber": 1,
"pageSize": 6,
"totalRecords": 486,
"totalIsExact": true,
"hasMore": true
}
},
"children": []
},
{
"_id": "global-search-timeline",
"_type": "core.timeline",
"props": {
"title": "Search activity",
"hasMore": true,
"items": [
{
"id": "global-search-event-1",
"recordId": "global-search-record-1",
"occurredUtc": "2026-09-04T17:58:00Z",
"title": "Authorized result opened",
"description": "North Wing closeout was opened from a permission-filtered search result.",
"actor": "Maya Chen",
"tone": "primary"
},
{
"id": "global-search-event-2",
"recordId": "global-search-record-2",
"occurredUtc": "2026-09-04T15:42:00Z",
"title": "Authorized result opened - Follow-up",
"description": "North Wing closeout was opened from a permission-filtered search result. Updated two hours ago by the assigned owner.",
"actor": "Avery Patel",
"tone": "primary"
},
{
"id": "global-search-event-3",
"recordId": "global-search-record-3",
"occurredUtc": "2026-09-04T12:18:00Z",
"title": "Authorized result opened - West region",
"description": "North Wing closeout was opened from a permission-filtered search result. Related to three visible records at the Reno location.",
"actor": "Sam Rivera",
"tone": "success"
},
{
"id": "global-search-event-4",
"recordId": "global-search-record-4",
"occurredUtc": "2026-09-03T21:07:00Z",
"title": "Authorized result opened - Customer response",
"description": "North Wing closeout was opened from a permission-filtered search result. Waiting for an external response before work can continue.",
"actor": "Maya Chen",
"tone": "warning"
},
{
"id": "global-search-event-5",
"recordId": "global-search-record-5",
"occurredUtc": "2026-09-03T16:31:00Z",
"title": "Authorized result opened - Regional operations review with a deliberately long title",
"description": "North Wing closeout was opened from a permission-filtered search result. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
"actor": "Automation",
"tone": "danger"
},
{
"id": "global-search-event-6",
"recordId": "global-search-record-6",
"occurredUtc": "2026-09-02T19:14:00Z",
"title": "Authorized result opened - Completed preview",
"description": "North Wing closeout was opened from a permission-filtered search result. 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 GlobalSearchPresentationData {
readonly metrics: unknown;
readonly recent: unknown;
readonly byStatus: unknown;
readonly trend: unknown;
readonly table: unknown;
readonly timeline: unknown;
}
export interface GlobalSearchPresentationProps {
/** Pass only the already-authorized presentation document returned by the API. */
readonly data?: GlobalSearchPresentationData | 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 GlobalSearchPresentation({
data,
loading = false,
error = false,
styled = false,
onOpenRecord,
}: GlobalSearchPresentationProps) {
if (error) return
The Global secured search presentation could not be loaded.
;
if (loading || !data) return
Loading Global secured search presentation...
;
return (
Global secured search
Search activity and discovery
Authorized search usage, result types, saved discoveries, visual evidence, and recent queries.
);
}
```
### 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/global-search`, 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": "global-search-live",
"_type": "module.global-search",
"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
Keep module-specific filters and lists for focused workflows. Use Global Search as a fast cross-module entry point, and clearly distinguish exact record matches from AI-ranked related evidence so users understand why each result appeared.
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/global-search.html).
---
title: "CompanyIQ evidence search"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/company-iq.html"
reviewed_at: "2026-09-14"
authority: Official
---
# CompanyIQ evidence search
Searches approved company records and multimodal sources with citations, and queues only already-authorized records for ingestion.
## In brief
- **Runtime key:** module.company-iq
- **Experience:** Permission-aware company intelligence
- **Security:** Server-enforced
Use the CompanyIQ renderer in a CompanyIQ solution page when analysts need cited company evidence and bounded source ingestion beside complete managed-record lifecycle workspaces.
[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.company-iq. 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
Searches approved company records and multimodal sources with citations, and queues only already-authorized records for ingestion.
## Key capabilities
- Run secured 256-to-1024 two-stage or 1998-dimensional retrieval.
- Preserve source record, page, element, and visual references.
- Find only records visible through secured Record Graph search before ingestion.
- Queue durable extraction and embedding through reviewed AI procedures.
- Keep opportunity activation in the governed action chain.
## Common uses
- Evidence-backed customer opportunity review.
- Company document and screenshot discovery.
- Permission-aware source intake.
## How it connects
The renderer uses the existing Records and Record Graph services plus the canonical AI source, document-element, chunk, visual-index, and 256/1024/1998 embedding chain.
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 derives tenant identity and applies DataRole, location, field, record, source, and CanAiReadRecords policy before retrieval or model context. Search results never grant source access.
- 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. Materialize the reviewed CompanyIQ production solution into an owned disposable or provisioned SaaS.
2. Add the validated `module.company-iq` block to a CompanyIQ page through the page editor.
3. Assign server-enforced DataRoles, locations, and AI-read policy, then exercise denied, empty, cited, visual, and source-revocation paths.
Tip
Treat the renderer as evidence retrieval, not mutation authority; route tasks, opportunities, and external actions through normal permissions and approval policy.
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/company-iq.html).
---
title: "OpsAtlas operating memory"
section: "First Class Modules"
canonical_url: "https://support.buildwithhq.com/first-class-modules/ops-atlas.html"
reviewed_at: "2026-09-14"
authority: Official
---
# OpsAtlas operating memory
Shows Knowledge Core health, answers operating questions from authorized published evidence, captures unsupported questions as gaps, and queues approved records for multimodal indexing.
## In brief
- **Runtime key:** module.ops-atlas
- **Experience:** Owned and cited operating guidance
- **Security:** Server-enforced
Use the OpsAtlas renderer beside the native Knowledge Core lifecycle when employees need current, role-aware guidance with exact source and visual evidence.
[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.ops-atlas. 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
Shows Knowledge Core health, answers operating questions from authorized published evidence, captures unsupported questions as gaps, and queues approved records for multimodal indexing.
## Key capabilities
- Show published, review-due, and gap health from the secured Knowledge Core.
- Return bounded cited answers from current authorized evidence.
- Preserve page, element, source-record, and visual references.
- Capture unsupported questions through the native Knowledge Question workflow instead of guessing.
- Queue only already-visible and AI-readable records for durable indexing.
## Common uses
- Operating-procedure lookup.
- Employee onboarding and policy guidance.
- Knowledge-gap capture and accountable curation.
- Governed agent instruction retrieval.
## How it connects
The renderer uses native Knowledge Core items, immutable revisions, owners, reviews, questions, gaps, files, favorites, relations, and activity plus the canonical multimodal AI pipeline.
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
Only Published, deliberately AI-eligible material within the represented user's tenant, DataRole, location, field, record, and CanAiReadRecords envelope can become answer context. Material actions remain separately authorized and approval governed.
- 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. Materialize the reviewed OpsAtlas production solution into an owned disposable or provisioned SaaS.
2. Add the validated `module.ops-atlas` block and native `module.knowledge-articles` block to the appropriate pages.
3. Assign Knowledge Core permission keys, owners, DataRoles, locations, and AI-read policy, then test draft exclusion, stale revisions, denied evidence, unsupported questions, and cited visual answers.
Tip
A small current corpus with named owners and exact citations is more valuable than a broad unreviewed document dump.
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/ops-atlas.html).
---
# Section: Builder Guide
---
title: "Build and publish SaaS pages"
section: "Builder Guide"
canonical_url: "https://support.buildwithhq.com/builder-guide/page-editor.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Build and publish SaaS pages
Build inside your SaaS with Puck, preview safely, and publish an exact validated draft.
## In brief
- **Primary editor:** Puck
- **Advanced editor:** Monaco
- **Publishes:** Validated drafts only
Page design happens inside the SaaS application itself. Open **Design**, select a User Type, then select one of its pages to edit. The same page document can be edited visually in Puck or, when needed, as canonical JSON in Monaco.
Page behavior is anchored to stable identity, not the visible name: BuildWithHQ keeps the page ID internally and uses the reviewed `PageKey` on current authoring and routing contracts. `PageName` is display text and can change without becoming authorization or routing proof.
## Puck is the primary editor
Puck is the normal visual workflow. Add React blocks from the component library, arrange them on the canvas, and edit their validated properties. Styling comes from the app's design system so shared blocks remain consistent. The saved canonical page JSON is independent of the editor, so switching tools does not create a second version of the page.
Use [Monaco advanced JSON mode](raw-json-mode.html) for precise structural edits or inspection. It edits that same canonical document; it is not a separate page or an authorization shortcut.
## Preview without changing the live app
**Preview** renders the draft in a non-interactive preview surface. You can simulate a selected User Type to check presentation and navigation, but the preview continues to use your current signed-in identity for every data request. It cannot grant the simulated type's DataRoles or Locations, and preview mutations are disabled.
Note
Role simulation answers 'what would this layout show?' It does not answer 'what data may that person access?' Test the published page with a representative signed-in user before release.
## Draft, validate, publish
1. **Save draft** stores the current canonical page only when it has no validation errors.
2. **Validate** checks the latest saved draft against the current component and page contracts.
3. Resolve every error and warning. Publishing requires a valid draft with zero warnings.
4. **Publish** promotes that exact validated draft and version. The public runtime keeps serving the previous published version until promotion succeeds.
Unsaved User Type menu changes block publishing so a page cannot go live against an ambiguous navigation state. Revision history records each saved and published version for inspection and recovery.
## Edit leases and version fences
Only one active editor owns a page's edit lease at a time. The lease renews while you work, and expected-version checks prevent an older browser tab from overwriting newer work. If a lease or version becomes stale, reload the authoring map and review the latest draft before continuing.
## Data-bound blocks stay permission-aware
Data blocks use approved data bindings. Assigning a page to a User Type makes the route eligible for that audience, but it does not grant records, fields, locations, exports, AI context, or actions. Runtime services still authorize each request with the signed-in person's DataRoles, Locations, and other record and action rules.
For the complete creation-to-publish sequence, continue with [Menus and User Types](menus-user-types.html).
## Arrange feature parts
Feature blocks can be split into parts you place anywhere, and one feature can follow the record selected in another. See [Build pages from feature parts](feature-parts.html).
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/builder-guide/page-editor.html).
---
title: "Build pages from feature parts"
section: "Builder Guide"
canonical_url: "https://support.buildwithhq.com/builder-guide/feature-parts.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Build pages from feature parts
Split a feature into pieces you place anywhere, put one feature inside another, and build complete solutions such as a customer page with its jobs, appointments and files.
## In brief
- **Where:** Design, in Puck or advanced JSON
- **Applies to:** 14 features
- **Data access:** Unchanged: every piece uses the feature's own permission checks
Every feature block on a page can be split into **parts**: its header, filters, list, detail, form and other pieces. You place the parts wherever your design needs them, and one feature can sit inside another. That is how you build a complete solution, for example a customer page for a home-services business that shows the customer, their jobs, appointments, files and events together.
## Workspaces and parts
A feature block is either its **workspace** or one of its **parts**, chosen by the block property `part`. The workspace loads the data, saves changes and keeps every safety check. Parts only show the piece the workspace hands them, so splitting a screen never splits its data or its permissions.
- Add the feature block once, with no `part`. That is the workspace.
- Inside it, through any layout blocks you like, add the same feature block again with `part` set, for example `"list"` or `"detail"`.
- If you place no parts, the workspace shows its built-in layout exactly as before.
| Feature | Block type | Parts (`props.part`) | Can follow a record |
| --- | --- | --- | --- |
| Contacts | `module.contacts` | header, filters, notices, list, pager, detail, form | Yes |
| Work Orders | `module.work-orders` | header, filters, notices, list, pager, detail, form, payments | Yes |
| Reservations | `module.reservations` | header, filters, notices, list, pager, detail, form, payments | Yes |
| Calendar | `module.calendar` | header, views, filters, notices, list, detail, form | Yes |
| Files | `module.files` | header, filters, notices, list, pager, detail, form, upload | Yes |
| Company News | `module.company-news` | header, filters, notices, list, pager, detail, form | No |
| Web Links | `module.web-links` | header, filters, notices, list, pager, detail, form | No |
| Favorites | `module.favorites` | header, filters, notices, list, pager, detail | No |
| Knowledge Articles | `module.knowledge-articles` | header, notices, health, review, filters, list, pager, detail, form | No |
| Checklists & Sign-offs | `module.checklists-signoffs` | header, notices, filters, list, pager, detail, form, templates | No |
| Chat Rooms | `module.chat-rooms` | header, filters, notices, list, pager, detail, form, thread, members | No |
| Discussion Boards | `module.discussion-boards` | header, filters, notices, list, pager, detail, form, topics, thread | No |
| Conversations | `module.conversations` | header, filters, notices, list, pager, detail, thread, form | No |
| Dynamic Records | `module.dynamic-records` | header, filters, notices, list, pager, detail, form | No |
A few placement rules keep every action reachable:
- A form opens in the `form` part if you placed one, otherwise in `detail`, otherwise in `list`.
- Messages and errors appear at the top of the workspace when you don't place a `notices` part.
- Feature-specific parts you leave out (for example `thread` or `members`) appear inside `detail`.
- A part placed outside its feature's workspace explains where it belongs and loads nothing.
- An unknown `part` value is treated as the workspace.
## Arrange parts in a grid
Use a **Container** block (`core.container`) with `layout` set to `"grid"` and `gridMode` set to `"twelve-column"`. Give each child a `responsive` span per screen size, for example 4 columns for the list and 8 for the detail on desktop, and 12 on tablet and phone so they stack.
Tip
Without `layout: "grid"` and `gridMode: "twelve-column"` a container stacks its children, and the spans have no effect.
## Put one feature inside another
Each workspace shares the record selected in it with the rest of the page under a **context key**. By default the key is the feature's own name (`contacts`, `work-orders` and so on); set `contextKey` to name it yourself, for example `"customer"`.
Another feature follows that record when you set its `recordFrom` to the same key. It then lists only the records linked to the selected one:
- **Work Orders**: jobs whose customer is the selected contact, or that are linked to the selected record.
- **Reservations**: reservations booked for the selected contact, or linked to it.
- **Files**: files attached to the selected record, or linked to it.
- **Calendar**: events linked to the selected record.
- **Contacts**: the customer of a selected work order or the contact of a selected reservation, or contacts linked to it.
Until something is selected, a following workspace loads nothing and says what to select. Set `followEmptyText` to use your own wording. When the selection changes, it reloads from scratch, so nothing from the previous record carries over.
Note
Following never widens access. The person must be able to see the selected record; if they can't, or it was deleted, the following list is empty. Every row still goes through the feature's own permission checks.
Only the workspace that drives the page reads and writes the page address's `?record=` deep link. Following workspaces ignore it, so opening a shared link to a customer never tries to open that customer as a job or a file.
### Example: a home-services customer page
A customer list and detail side by side, with the selected customer's jobs, appointments, files and events below. Paste it into [advanced JSON mode](raw-json-mode.html) as a starting point.
```
{
"blocks": [
{ "_id": "customers", "_type": "module.contacts",
"props": { "title": "Customers", "singular": "customer", "contextKey": "customer", "pageSize": 10 },
"children": [
{ "_id": "customers-header", "_type": "module.contacts", "props": { "part": "header" }, "children": [] },
{ "_id": "customers-filters", "_type": "module.contacts", "props": { "part": "filters" }, "children": [] },
{ "_id": "customers-row", "_type": "core.container",
"props": { "layout": "grid", "gridMode": "twelve-column" },
"children": [
{ "_id": "customers-list", "_type": "module.contacts",
"props": { "part": "list", "responsive": { "desktop": { "span": 4 }, "tablet": { "span": 12 }, "mobile": { "span": 12 } } },
"children": [] },
{ "_id": "customers-detail", "_type": "module.contacts",
"props": { "part": "detail", "responsive": { "desktop": { "span": 8 }, "tablet": { "span": 12 }, "mobile": { "span": 12 } } },
"children": [] } ] },
{ "_id": "customers-pager", "_type": "module.contacts", "props": { "part": "pager" }, "children": [] } ] },
{ "_id": "jobs", "_type": "module.work-orders",
"props": { "title": "Jobs", "recordFrom": "customer", "contextKey": "job",
"followEmptyText": "Select a customer to see their jobs." }, "children": [] },
{ "_id": "appointments", "_type": "module.reservations",
"props": { "title": "Appointments", "recordFrom": "customer",
"followEmptyText": "Select a customer to see their appointments." }, "children": [] },
{ "_id": "customer-files", "_type": "module.files",
"props": { "recordFrom": "customer", "contextKey": "customer-file" }, "children": [] },
{ "_id": "customer-events", "_type": "module.calendar",
"props": { "recordFrom": "customer", "contextKey": "customer-event" }, "children": [] }
]
}
```
## Create in context
When a workspace follows a contact, new records start linked to that contact:
- **New work order** starts with the selected contact as its customer.
- **New reservation** books for the selected contact; the form keeps the email used for follow-ups and skips the new-contact fields.
## Link types
A link type names how two records relate, for example *Jobs* from a customer and *Customer* back from a job. The **Related** panel uses link types when people link records by hand, and the app enforces each type's rules on every new link:
- **Features**: a type can be limited to links from one feature to another, for example from Contacts to Work Orders. A link between other kinds of records is refused.
- **One only**: a type can allow a single linked record per record. A second link of that type is refused until the first is removed; repeating the same link is accepted.
- **Active**: a turned-off type is refused like an unknown one and is hidden from people choosing a link.
The built-in link types (for example the customer of a work order and the contact of a reservation) can't be changed. Links a feature creates itself, such as a new work order's customer, keep working whatever rules you set. Changing a type's rules applies to links made from then on; existing links are kept.
## Rename features
Each app can give a feature its own name, singular and plural, for example *Customer* and *Customers* instead of *Contact* and *Contacts*, or *Student* and *Students* for a school band app. Only the words on screen change; the feature, its data and its permissions stay the same. Clearing both names returns the feature to its built-in name. A block's own `title` still wins on that block.
Set both in **Design**, on the **Links & names** tab. Built-in link types show there read-only; if someone else changed a link type since you opened it, saving asks you to reload first.
Important
Defining link types and renaming features is limited to people who can design the app. Everyone signed in sees the names and the active link types.
## Do the same through the Developer API
- Build and publish these pages with the app design routes under `/v1/apps/{saasAppId}/design`, using your builder keys, exactly as in the designer.
- The Contacts, Work Orders, Reservations, Calendar and Files list operations take an optional `relatedRecordId` query parameter with the same rules as following a record: only linked rows, and an empty list when the caller can't read that record. An all-zero ID is refused with 400. Contacts reports `hasMore` instead of an exact total for a linked list.
- Read and change link types and feature names with your builder keys: `GET /v1/apps/{saasAppId}/design/link-types` returns every link type and every feature with its app name; `PUT /v1/apps/{saasAppId}/design/link-types/{relationKey}` creates or changes a link type (send the `updatedUtc` you were shown when changing one); `PUT /v1/apps/{saasAppId}/design/vocabulary/{moduleKey}` sets or clears a feature's names. Saves change the live app, so they need a live key. Inside the app, the same routes are `/api/runtime/design/link-types` and `/api/runtime/design/vocabulary/{moduleKey}`, and any signed-in user reads the active link types and names from `GET /api/runtime/vocabulary`.
- Bad input is 400; a change to a link type that changed since it was shown, or to a built-in type, is 409.
- Link records with the record graph relation operations; the link type rules above apply. Linking the wrong kinds of records is 422; a second link of a one-only type is 409.
## Not supported yet
- Only Contacts, Work Orders, Reservations, Calendar and Files can follow a record. The other features can be split into parts but don't follow.
- Checklists & Sign-offs shares the selected checklist run, not a record, so other features can't follow a checklist yet.
- Part, context key and follow settings are edited as component properties (or in advanced JSON); dedicated pickers in the designer are still to come.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/builder-guide/feature-parts.html).
---
title: "Build custom data modules"
section: "Builder Guide"
canonical_url: "https://support.buildwithhq.com/builder-guide/dynamic-module-system.html"
reviewed_at: "2026-09-14"
authority: Official
---
# Build custom data modules
Model your own business data with Dynamic Records, custom fields and RecordId, then use the same secured records in pages and through the API.
## In brief
- **Data model:** Custom fields on canonical Records
- **Presentation:** Dynamic Records
- **Access:** Personal or DataRole/location permissions
**Dynamic modules let you model business data inside your SaaS without building a specialized feature for every kind of record.** Think of it as a database builder within your application: a module defines a kind of thing, custom fields describe its business information, and each item has a permanent `RecordId`. An asset register, an inspection log or an equipment inventory can use this model.
The business information can consist entirely of custom fields. Standard record identity, title, status, timestamps and visibility still belong to the platform. This is a logical data model within your SaaS database; it does not create a separate SQL database for each module.
## How the pieces fit together
| Piece | What it represents | Example |
| --- | --- | --- |
| Module | A configured business record type, identified by a stable module key | `assets` |
| Field definition | A named, typed piece of business information and its rules | Asset tag, purchase cost, inspection date |
| Record | One item with its own `RecordId`, title and visibility | Warehouse forklift 12 |
| Field value | The value of a defined field on that record | Asset tag FL-012 |
| Page | A published interface for working with the configured module | Asset register list and form |
| UserType | Which menu, homepage and layout a user receives | Employees, Contractors or Customers |
A Contractors UserType and a dynamic module named Contractors are independent choices. A record does not create a login, and putting a page on a UserType menu does not grant access to its records.
## Plan a module from custom fields
Start with one clear record type. For an asset register, the standard title could be *Warehouse forklift 12*, with these custom fields:
| Business field | Type | Example |
| --- | --- | --- |
| Asset tag | Text | FL-012 |
| Purchase cost | Decimal | 24500.00 |
| Next inspection | Date and time | A scheduled inspection date |
| Operational | Yes / no | Yes |
| Condition | Choice | Good, Needs service, Retired |
The current field editor offers Text, Whole number, Decimal, Date and time, Yes / no, Choice and Structured JSON. Choose field keys carefully, use clear labels, and set required behavior, length limits, choices, grouping and order where appropriate. Use record relationships for connections to other records rather than assuming that a text field containing an ID creates a relationship.
## Configure fields and display the module
1. **Start with a configured custom module.** Confirm the module exists in the target app and organization and has its own key. The current product does not provide a standalone create-module wizard or module-definition-create API. Entering a new key in Custom fields does not create a module. Ask the app builder which configured modules are available before adding fields.
2. **Define the shared model.** In **Design**, open a page associated with the module. Its custom-field panel lets authorized designers add builder fields and control whether organizations may add their own fields, with configured limits.
3. **Add organization-specific fields when allowed.** Open **Custom fields**, choose the page and existing module, then choose **Open module fields**. Supply a Key, Label and Type, configure the behavior, and choose **Add field**. Organization field keys use the `c_` prefix. Every read and save checks field-management permission and builder policy.
4. **Display it with Dynamic Records.** Use the `module.dynamic-records` block with the actual configured `moduleKey`. Its standard list, detail and form use the returned field schema. See [Dynamic Records](../first-class-modules/dynamic-records.html) for the renderer and developer examples, and [Build pages from feature parts](feature-parts.html) for arranging the workspace.
5. **Publish and test.** Place the page in the intended UserType menus and homepage configuration. Test with users who have different DataRoles and locations, including permitted and denied reads, creates, edits and removals. Field definitions and page publication are separate operations; a page preview alone is not a permission test.
Note
A module may currently have no additional fields. That is an empty custom schema, not a complete business-specific solution. The editor and renderer operate configured modules; this guide does not imply an unrestricted table designer or arbitrary SQL access.
## RecordId connects the data
The same canonical record can participate in permitted relationships, favorites and activity without a second copy of the business item. Use the supported record relationship tools to connect an asset to other records. Each connected record keeps its own permissions; a link never grants access to a hidden record. Availability of search, automation or AI also depends on that feature's configuration and permission rules.
## Use dynamic records through the API
Integrations can discover a configured module's public schema and work with its records. The public Developer API base is `/v1/apps/{saasAppId}/modules/dynamic`. Browser clients use the corresponding signed-in routes under `/api/modules/dynamic`.
| Action | Method and path after the base |
| --- | --- |
| Discover available modules | `GET` the base path |
| Read field schema and capabilities | `GET /{moduleKey}/schema` |
| List, search and page records | `GET /{moduleKey}` |
| Read one record | `GET /{moduleKey}/{recordId}` |
| Create a record | `POST /{moduleKey}` |
| Edit a record | `PUT /{moduleKey}/{recordId}` |
| Remove a record | `DELETE /{moduleKey}/{recordId}?expectedUpdatedUtc=...` |
| Set a favorite | `PUT /{moduleKey}/{recordId}/favorite` |
For example, after configuring an `assets` module and an organization text field named `c_asset_tag`, this body creates a Personal record through `POST /{moduleKey}`:
```
{
"title": "Warehouse forklift 12",
"isPersonal": true,
"fields": [
{ "fieldKey": "c_asset_tag", "valueText": "FL-012" }
]
}
```
Read the actual schema before sending values; the example field must already exist, and any other required fields must also be supplied. For a shared record, explicitly select a permitted DataRole and, when active locations exist, an authorized location or All locations. All locations requires all-location authority; an omitted choice is not All. Personal records have no DataRole or location.
List requests support `search`, `status`, `pageNumber`, `pageSize` and `sort`. The supported list orders are `created_asc`, `created_desc`, `title_asc` and `title_desc`; a field's Sortable setting does not add an arbitrary field-sort parameter to this route. Page size is at most 100; a write accepts at most 200 field inputs, with configured limits and validation still applying.
Read current details before editing or removing and send the returned `updatedUtc` as `expectedUpdatedUtc`. A stale change is refused so an integration does not silently overwrite someone else's work. The current edit contract preserves the record's Personal/DataRole scope; do not send a new visibility choice in an ordinary update.
These are record-data operations. Builder field definitions and field policy have separate design operations under `/v1/apps/{saasAppId}/design/fields/{moduleKey}`. A record-create request does not create a module or add field definitions. Use the [current API reference](../api/interactive-api-reference.html) and [Developer API guides](../api/index.html) for authentication, operation scopes and the exact request contracts.
## Permissions apply everywhere
The API uses verified app, organization and user identity and the same secured services and stored procedures as the browser. Developer credentials need the appropriate API scope and an authorized tenant principal. Reads and mutations still enforce Personal ownership or current DataRole and location permissions, with separate create, edit and remove authority. Each SaaS retains its own database set.
Hidden and encrypted fields are excluded from the generic public dynamic schema. Sensitive fields require their supported secure-field controls and permission-checked reveal/edit flow; this generic field API is not a way to reveal or overwrite them.
Tip
Use Dynamic Records when the main requirement is a configurable data model. Use a specialized module when the workflow needs its own behavior, such as scheduling, billing or conversation delivery. Both can participate in the same secured RecordId system.
---
Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/builder-guide/dynamic-module-system.html).
---
title: "React component catalog and copy/paste kit"
section: "Builder Guide"
canonical_url: "https://support.buildwithhq.com/builder-guide/react-component-catalog.html"
reviewed_at: "2026-09-14"
authority: Official
---
# React component catalog and copy/paste kit
Copy styled or unstyled React starters for all first-class capabilities, native page modules, Universal Inbox, GoClaw, menus, shell layouts, data bindings and exact API contracts.
## In brief
- **Capabilities:** 25
- **Page renderers:** 30
- **Cataloged routes:** 419
- **Page-module gaps:** 0
This is the source-driven inventory and copy/paste kit for BuildWithHQ React pages and the tenant shell around them. It covers every documented first-class capability, core blocks, native page modules, Universal Inbox, GoClaw review work, menus, page bootstrap, data bindings, exported browser functions, HTTP routes, VB service members, and reviewed SQL procedures. The generated files live under `catalog-generator/generated/`.
[Capabilities](#capabilities) [Start a page](#start) [Menus](#menus) [Data bindings](#bindings) [Tenant shell](#shell) [Page modules](#all-modules) [All APIs](#all-apis) [Complete React source](#source)
Note
The current inventory contains 25 first-class capabilities, 32 page SDK/API surfaces, 30 bound page renderers, 304 page-module server operations, 115 additional tenant/design routes, 307 tenant API exports, 33 Design API exports, and 22 Inbox/GoClaw client methods. All 304 page-module operations resolve to reviewed stored-procedure contracts; the generated page-module gap count is 0.
## Choose native or custom presentation
| Approach | Use it when | What you own |
| --- | --- | --- |
| **Native first-class block** | You want the complete module behavior. | Page composition, menu placement, design tokens and permitted block props. BuildWithHQ owns SDK parsing, API calls, authorization-aware states and writes. |
| **Unstyled starter** | You need your own markup or design system. | Semantic presentation, accessibility and state rendering. Inject the official authenticated loader and keep its parser. |
| **Styled starter** | You want a production-shaped starting layout. | The same contract as unstyled, plus opt-in CSS. The supplied desktop catalog is left aligned at 280px / flexible / 280px. |
Important
Do not turn a React prop into an authorization decision. The server derives SaaS app, tenant account, user, DataRole and location from verified identity. Browser-supplied record IDs, page keys, filters and binding keys are query targets only.
## Every first-class capability
The capability catalog separates native page blocks, tenant-shell components, and background services. Every capability has a generated unstyled and styled explanatory React export. Those cards tell a developer what is available; they do not imitate or replace the secured native behavior.
[Connected work](#capability-connected-work) [Knowledge finder](#capability-knowledge-finder) [Universal Inbox](#capability-universal-inbox) [Checklists & Signoffs](#capability-checklists-signoffs) [GoClaw](#capability-goclaw) [AI Insights](#capability-ai-insights) [Contacts](#capability-contacts) [Work Orders](#capability-work-orders) [Reservations](#capability-reservations) [Calendar](#capability-calendar) [Files](#capability-files) [Conversations](#capability-conversations) [Company news](#capability-company-news) [Knowledge Core](#capability-knowledge-core) [Web links](#capability-web-links) [Chat rooms](#capability-chat-rooms) [Discussion Boards](#capability-discussion-boards) [Mail reader](#capability-mail-reader) [Favorites](#capability-favorites) [Notifications](#capability-notifications) [Dynamic Records](#capability-dynamic-records) [ActiveWorkplace](#capability-active-workplace) [Record relations](#capability-record-relations) [Record activity](#capability-record-activity) [Global secured search](#capability-global-search)
****Connected work** `product-solution-renderer`**
Find records you can read, explore their visible connections, and open their native pages to work on them.
| Experience | Read-only record discovery |
| --- | --- |
| Native page key | `module.connected-work` |
| React implementation | `ConnectedWorkModule` |
| API roots | `/api/modules/record-graph` |
#### What developers can build
- Search by saved record title with Search records, then use Load more records for additional results.
- Choose a result to see its visible related work. Explore follows a connected record without copying it.
- Open native record and Open record use persisted page associations. When several destinations are available, choose the page you want; no destination is guessed.
- A display-limit notice means the shown connections are bounded, not the complete relationship count. Explore a connected record to continue.
- If loading fails, Try again repeats the requested search or connection lookup under your current access.
#### Common uses
- Follow a meeting to its related work order or file.
- Find the native page for a record discovered through another module.
- Review visible relationships before opening the owning record.
**Security boundary:** Every search and connection read uses the verified user, tenant organization, and SaaS app. Personal ownership, DataRole, location, private-record, and action rules remain enforced by the secured services. A visible relationship or page link never grants access to its target.
[Open the complete Connected work implementation guide](../first-class-modules/connected-work.html)
****Knowledge finder** `product-solution-renderer`**
Search accessible Knowledge Core articles, filter publication state, preview current text, and open the native article page.
| Experience | Read-only article discovery |
| --- | --- |
| Native page key | `module.knowledge-finder` |
| React implementation | `KnowledgeFinderModule` |
| API roots | `/api/modules/knowledge-core` |
#### What developers can build
- Enter a search and choose All accessible articles, Published, Draft, or Archived in Publication state, then select Search articles.
- Use Previous article page and Next article page to browse the applied search and publication-state filter.
- Preview an article to read its current publication state, authority level, revision, summary, and article text.
- Draft and archived previews explicitly say they are unavailable as published citations. Selecting a filter never grants access to those articles.
- Open article resolves the record's persisted native page choices. If no destination is available, use the displayed unavailable-state guidance rather than guessing a route.
- Try again retries the failed search page or article preview using current permissions.
#### Common uses
- Find an operating procedure by its title or search text.
- Review an accessible draft before opening Knowledge Core.
- Check an article's current revision and publication state.
**Security boundary:** The verified user, tenant organization, SaaS app, and persisted page identity determine the current request. Knowledge Core applies its record, Personal ownership, DataRole, location, private-record, and action-specific permissions. Search terms, publication filters, menu labels, and client-supplied IDs are query targets, never authorization proof.
[Open the complete Knowledge finder implementation guide](../first-class-modules/knowledge-finder.html)
****Universal Inbox** `shell-and-page-component`**
Collects configured external email, optional support tickets, SMS, web-form submissions, and other inbound communications into one secured attention layer where unsorted work stays visible and routing rules can classify, prioritize, assign, and queue it for action.
| Experience | Inbound work & triage |
| --- | --- |
| Native page key | `core.universal-inbox` |
| React implementation | `UniversalInbox`, `UniversalInboxBell`, `UniversalInboxPageModule` |
| API roots | `/api/inbox`, `/api/inbox/items` |
#### What developers can build
- Receive configured inbound email and SMS through native Conversations and messages.
- Let an account owner enable Ticketing per inbound email address and assign stable mailbox-scoped ticket numbers.
- Use New, Open, WaitingCustomer, WaitingInternal, Snoozed, Resolved, and Closed as one controlled ticket-resolution lifecycle.
- Receive web-form submissions and other authenticated inbound source events without losing their native source record.
- Keep unmatched or unsorted items visible for human triage instead of dropping them into a hidden channel silo.
- Apply deterministic routing rules and permitted AI classification to assign queues, owners, tags, priorities, and due/SLA attention.
- Open the full native source context, prepare replies or actions, approve governed suggestions, snooze, reassign, and complete work.
- Increment ActivityVersion when new source activity arrives so stale drafts or actions cannot execute.
#### Common uses
- One intake queue for support, sales, service, operations, and customer requests.
- A technical-support mailbox with durable ticket identity and resolution state.
- Sorting unknown inbound communication before it reaches a team or person.
- Shared ownership, SLA, escalation, and GoClaw-assisted preparation across external channels.
**Security boundary:** An item is visible only when the current user can access its source record and effective queue scope. Routing never grants source access. GoClaw preparation, approval, and execution reapply the user's record/DataRole/location envelope, AI-read gate, action permission, and current ActivityVersion.
[Open the complete Universal Inbox implementation guide](../first-class-modules/universal-inbox.html)
****Checklists & Signoffs** `page-module`**
Attaches reusable, versioned checklists to authorized records and lets each item require notes, documents, or photos before the completed run enters the existing Workflow signoff path.
| Experience | Evidence-backed work acceptance |
| --- | --- |
| Native page key | `module.checklists-signoffs` |
| React implementation | `ChecklistsSignoffsModule` |
| API roots | `/api/modules/checklists-signoffs` |
#### What developers can build
- Create reusable versioned templates with ordered required or optional items.
- Attach a template snapshot to any authorized universal record.
- Track up to 1,000 ordered run items with completion actor, time, note, and revision.
- Require one or more native File documents or photos as proof for selected items.
- Calculate bounded progress and refuse submission while required work or evidence is missing.
- Submit exactly one current run to the existing Workflow and Universal Inbox approval path.
- Support rejection, editing, proof replacement, resubmission, approval, and replayable history.
- Reject stale revisions and make duplicate completion, submission, and decision requests idempotent.
#### Common uses
- Work-order inspection and closeout proof.
- Project stage-gate and quality checklists.
- Support-ticket diagnostic or escalation checkoffs.
- Safety, compliance, onboarding, handoff, and document-review signoffs.
**Security boundary:** Every list, detail, item update, evidence link/download, submission, and decision derives tenant, user, DataRole, Location, and source-record scope from server identity. Evidence links do not grant File access, and Workflow permissions—not checklist UI state—control signoff authority.
[Open the complete Checklists & Signoffs implementation guide](../first-class-modules/checklists-signoffs.html)
****GoClaw** `shell-and-page-component`**
Adds a dedicated virtual assistant to a user account that researches, summarizes, classifies, drafts, and proposes work without ever receiving more access or action authority than that user.
| Experience | Permission-bound AI assistance |
| --- | --- |
| Native page key | `module.goclaw` |
| React implementation | `GoClawApprovalQueue`, `GoClawModule` |
| API roots | `/api/goclaw/agents`, `/api/goclaw/approvals` |
#### What developers can build
- Read only records, fields, locations, and Inbox context the represented user may read—and only when AI access is also allowed.
- Research, classify, summarize, draft replies, and prepare structured next actions.
- Create suggestion batches whose evidence, risk, confidence, required permission, and inputs can be reviewed.
- Route material or customer-facing actions through configured policy or human approval before deterministic execution.
- Respect action budgets, leases, retries, stale-context fences, and idempotent execution.
- Record attribution, approval/refusal, tool/action evidence, results, failures, ratings, and correlation history.
#### Common uses
- Triage and response preparation for permitted Universal Inbox work.
- Account, case, project, support, or operations research and follow-up suggestions.
- A personal daily assistant that reduces preparation work while the user retains decision authority.
**Security boundary:** GoClaw operates on behalf of one user. Its effective access is the intersection of that user's tenant, DataRole, record, field, location, and action permissions with AI-specific CanAiReadRecords, secure-field exclusions, tool policy, quotas, approval policy, and the current source/Inbox activity version. The result can be narrower than the user's normal access, never wider.
[Open the complete GoClaw implementation guide](../first-class-modules/goclaw.html)
****AI Insights** `page-module`**
Runs permission-aware AI analysis in the background each day to create prioritized, evidence-backed insights and governed next actions from meaningful changes in authorized business records.
| Experience | Proactive daily findings |
| --- | --- |
| Native page key | `module.ai-insights` |
| React implementation | `AiInsightsModule` |
| API roots | `/api/modules/ai-insights` |
#### What developers can build
- Run a durable daily background cycle for configured insight types and source scopes.
- Steer each nightly run with the app's starter directives and the organization's own "look for" directives; the organization owner turns AI Insights on in Account admin > AI Insights.
- Read only AI-eligible records and evidence through tenant, DataRole, field, location, and CanAiReadRecords controls.
- Create structured insights with source record, type/category, title, summary, recommended action, priority, confidence, severity, evidence, model/profile provenance, and lifecycle dates.
- Use source version/context fingerprints and current-state checks to avoid repeatedly presenting stale or duplicate findings.
- Deliver each insight only to users who can currently access its source record; losing source access removes the insight from their secured feed.
- Let each user view, pin, snooze, hide, dismiss, or advance an insight without changing another user's personal feed state.
- Turn a recommended next step into a governed action that still requires the current user's action permission and configured policy or approval.
#### Common uses
- Daily account risk, opportunity, renewal, follow-up, and customer-health findings.
- Operational exceptions, overdue work, recurring issues, quality changes, and material KPI movement.
- Proactive company-knowledge, case, service, sales, support, or asset insights with evidence links.
**Security boundary:** Insight generation applies the AI-read envelope before model context is assembled. Insight visibility is inherited from the source record and rechecked at read time. Evidence, recommendations, and actions cannot reveal or operate on records, fields, locations, or tools outside the current user's permissions; material execution uses action-specific permission and policy/approval.
[Open the complete AI Insights implementation guide](../first-class-modules/ai-insights.html)
****Contacts** `page-module`**
Provides a secured contact directory with configurable contact types, search, create/detail experiences, custom fields, relationships, activity, and favorites.
| Experience | People & organizations |
| --- | --- |
| Native page key | `module.contacts` |
| React implementation | `ContactsModule` |
| API roots | `/api/modules/contacts` |
#### What developers can build
- Search and browse only the contacts visible to the current user.
- Create contacts with name, company, email, phone, and configurable type fields.
- Show secured contact detail, custom fields, related records, activity, and favorite state.
- Reuse the same contact identity across other modules instead of creating duplicate people records.
#### Common uses
- Customer and prospect directories.
- Members, vendors, partners, or project stakeholders.
- The person/company context behind conversations, events, work orders, files, and cases.
**Security boundary:** Contact lists and details apply SaaS, tenant, DataRole, location, record, and field rules. Create and edit controls appear only when secured responses grant the corresponding capability; hidden or encrypted dynamic fields are not returned.
[Open the complete Contacts implementation guide](../first-class-modules/contacts.html)
****Work Orders** `page-module`**
Tracks work requests through a secured lifecycle with number, customer, location, summary, priority, status, assignments, related records, and service evidence.
| Experience | Operational work |
| --- | --- |
| Native page key | `module.work-orders` |
| React implementation | `WorkOrdersModule` |
| API roots | `/api/modules/work-orders` |
#### What developers can build
- Search and filter work orders by text and status.
- Create work with summary, description, priority, and optional customer context.
- Move work through the supported status lifecycle with optimistic concurrency.
- Assign team members, designate primary responsibility, and expose related-record counts.
- Let the tenant choose whether Work Order payment collection is disabled, optional, required, manual, at creation, or required before completion.
- Show secured payment history and queue idempotent charges against protected, consented payment methods without exposing card or provider secrets.
#### Common uses
- Field service and maintenance dispatch.
- Facilities, asset, repair, installation, or inspection follow-up.
- Internal service requests and operational task queues that need a stronger lifecycle than a simple task.
**Security boundary:** The service limits rows by SaaS app, tenant, DataRole, and location and separately checks create, transition, assignment, and payment authority. The account owner manages payment policy and external connector references. Collection requires the existing CanBillRecords capability plus edit and record scope; customer and assignee identifiers are query/action targets, not proof of access.
[Open the complete Work Orders implementation guide](../first-class-modules/work-orders.html)
****Reservations** `page-module`**
Turns a bookable product purchase into one secured workflow: contact identity, capacity-protected reservation, protected charge, calendar event, tenant-user assignment, resource booking, and scheduled before/after-service follow-up.
| Experience | Sell, schedule & follow up |
| --- | --- |
| Native page key | `module.reservations` |
| React implementation | `ReservationsModule` |
| API roots | `/api/modules/reservations` |
#### What developers can build
- Define bookable products with duration, price/currency, party limits, concurrent capacity, buffers, hold time, and default location, resource, assignee, and follow-up timing.
- Create or safely reuse an authorized Contact and preserve the purchaser's name, email, phone, and notes.
- Create a capacity-protected Reservation and linked Calendar event, with an optional Resource booking and an active tenant user assigned to deliver the service.
- Queue the existing idempotent charge-intent pipeline using a protected payment-method reference; the API never receives card numbers, security codes, processor credentials, or raw payment tokens.
- Keep paid bookings pending and private until the charge worker reports success, then promote the reservation, event, resource, and follow-ups transactionally.
- Track Pending Payment, Requires Action, Confirmed, Checked In, Completed, Cancelled, No Show, and Payment Failed with optimistic concurrency and an audit timeline.
- Schedule separate pre-service and post-service follow-up date/time, recipient email, subject, message, delivery state, attempts, sent time, and outbound-message reference.
- Use idempotency keys to make checkout retries safe and prevent duplicate contacts, charges, calendar events, and reservations.
- Let the tenant account owner choose disabled, optional, or required collection; creation, manual, or completion timing; full or deposit collection; partial-payment rules; currency; minimum charge; and paid-before-completion enforcement.
- Give users with the existing billing capability a secured balance, payment history, charge history, and protected charge action for the reservation record.
#### Common uses
- Paid consultations and professional-service appointments.
- Tours, classes, lessons, events, and capacity-limited sessions.
- Installations, inspections, salon/wellness visits, rentals, and service calls.
- Free appointments that still need contact, calendar, assignment, capacity, and follow-up.
**Security boundary:** List/detail access is inherited from secured universal Records. Product management and mutations require current DataRole capability. Locations and contact targets are revalidated in the user's scope; assignees must be active users assigned to the same app and selected location. Product capacity and opt-in consultation/appointment assignee exclusivity are rechecked transactionally on purchase, reschedule, confirmation, and delayed payment success. Existing shared-capacity products remain non-exclusive by default. Identity comes from verified server context. The tenant account owner controls payment policy and connector references; collection requires the existing CanBillRecords capability plus record scope. Raw card data and provider credentials are never accepted or persisted by Reservations.
[Open the complete Reservations implementation guide](../first-class-modules/reservations.html)
****Calendar** `page-module`**
Provides secured month/agenda scheduling for events, attendees, resources, reminders, locations, private events, custom fields, relationships, activity, and favorites.
| Experience | Time & scheduling |
| --- | --- |
| Native page key | `module.calendar` |
| React implementation | `CalendarModule` |
| API roots | `/api/modules/calendar` |
#### What developers can build
- Browse monthly event ranges and search visible events.
- Create timed or all-day events with location, reminder, color, and privacy settings.
- Show attendee responses, resource bookings, custom fields, related records, and activity.
- Favorite important events and connect them to customers, projects, work orders, or cases.
#### Common uses
- Customer meetings and project milestones.
- Technician visits, inspections, shifts, or resource schedules.
- Renewal dates, campaign launches, approval deadlines, and case hearings.
**Security boundary:** Calendar queries enforce tenant, DataRole, location, and private-event rules. Attendees and related records are independently secured, and event creation/editing requires the corresponding server permission.
[Open the complete Calendar implementation guide](../first-class-modules/calendar.html)
****Files** `page-module`**
Provides private file upload, download, metadata, tags, record attachments, comments, locks, custom fields, relationships, activity, and favorites without exposing storage coordinates.
| Experience | Documents & evidence |
| --- | --- |
| Native page key | `module.files` |
| React implementation | `FilesModule` |
| API roots | `/api/modules/files` |
#### What developers can build
- Search and filter secured file metadata by name and content type.
- Upload, describe, tag, edit, and privately download authorized files.
- Keep personal files only you can see, up to the per-person limit the organization owner sets within the app's ceiling.
- Attach one file to multiple permitted records without duplicating the binary.
- Show comments, locks, custom fields, relations, activity, and favorite state.
#### Common uses
- Customer and project documents.
- Inspection photos, work-order evidence, manuals, and invoices.
- Case evidence, knowledge source files, and signed approvals.
**Security boundary:** Download and upload flow through authenticated backend endpoints. The browser never receives provider credentials, tenant database identity, storage paths, or unrestricted object-store locations; attachment targets are reauthorized independently.
[Open the complete Files implementation guide](../first-class-modules/files.html)
****Conversations** `page-module`**
Provides native channel threads with participants, messages, status, priority, internal notes, outbound replies, related context, activity, dynamic fields, and favorites.
| Experience | Customer & team communication |
| --- | --- |
| Native page key | `module.conversations` |
| React implementation | `ConversationsModule` |
| API roots | `/api/modules/conversations` |
#### What developers can build
- Search and filter conversation threads by status.
- Create internal conversations and manage subject, priority, and lifecycle.
- Read escaped message history and post internal notes or permitted outbound replies.
- Expose unread/favorite state, dynamic fields, related records, and activity.
#### Common uses
- Customer support and success communication.
- Project, sales, case, and operations correspondence.
- The native thread behind Universal Inbox triage and governed AI suggestions.
**Security boundary:** Conversation, message, channel, DataRole, and location access is enforced server-side. Outbound capability and edit rights are explicit; provider credentials and direct channel endpoints never reach the browser.
[Open the complete Conversations implementation guide](../first-class-modules/conversations.html)
****Company news** `page-module`**
Publishes searchable internal announcements with draft, scheduled, published, and expired visibility, author controls, custom fields, relationships, activity, and favorites.
| Experience | Announcements |
| --- | --- |
| Native page key | `module.company-news` |
| React implementation | `CompanyNewsModule` |
| API roots | `/api/modules/company-news` |
#### What developers can build
- Search visible announcements and open their secured detail.
- Author and edit announcements only when the response grants edit permission.
- Control draft, scheduled, published, and expired visibility.
- Connect announcements to records, activity, custom fields, and personal favorites.
#### Common uses
- Internal news and release announcements.
- Location-specific closures, safety notices, or operating updates.
- Policy and program communications linked to supporting records or files.
**Security boundary:** Publication state, SaaS, tenant, DataRole, and location are evaluated by secured reads. Announcement bodies render as escaped text, and authoring controls depend on server-reported permission.
[Open the complete Company news implementation guide](../first-class-modules/company-news.html)
****Knowledge Core** `page-module`**
Turns tenant knowledge into owned, revisioned, reviewable records that connect to business context, answer questions, support permission-aware AI/RAG, and learn safely from resolved support work.
| Experience | Maintained, cited knowledge |
| --- | --- |
| Native page key | `module.knowledge-articles` |
| React implementation | `KnowledgeArticlesModule` |
| API roots | `/api/modules/knowledge-articles`, `/api/modules/knowledge-core` |
#### What developers can build
- Create Draft knowledge and publish or archive it through permission-checked lifecycle actions.
- Preserve immutable revisions, publication history, authority, AI eligibility, and optimistic-concurrency evidence.
- Assign one primary accountable owner to a user or DataRole and schedule recurring review work.
- Relate knowledge to any authorized RecordId and retrieve bounded contextual knowledge without copying the business record.
- Capture unanswered questions, repeated occurrences, usage, feedback, and secured knowledge-gap signals.
- Register only Published, AI-eligible revisions for secured semantic/multimodal retrieval with exact citations.
- Bring current support knowledge into fenced GoClaw Inbox context and turn resolved cases into sanitized Draft candidates.
#### Common uses
- Owned product documentation, policies, procedures, FAQs, and employee help.
- Customer-safe help centers and support answers with exact current citations.
- Contextual playbooks linked to customers, products, cases, work orders, files, and conversations.
- Permission-aware AI/RAG sources whose authority and freshness must be inspectable.
- A controlled support learning loop that turns reusable resolutions into reviewable Drafts.
**Security boundary:** Every list, item, relation, review, question, feedback, support-context, and AI path derives the SaaS app, tenant, user, DataRole, location, record, field, and permission scope from verified server identity. CanAiReadRecords is required before AI context. Publication, ownership, review, support learning, and archive actions have separate permission checks; customer-visible context excludes internal-only sources.
[Open the complete Knowledge Core implementation guide](../first-class-modules/knowledge-core.html)
****Web links** `page-module`**
Maintains searchable, categorized, location-aware external links with descriptions, custom fields, relationships, activity, and favorites.
| Experience | Curated external resources |
| --- | --- |
| Native page key | `module.web-links` |
| React implementation | `WebLinksModule` |
| API roots | `/api/modules/web-links` |
#### What developers can build
- Search links and filter them by category.
- Create and edit title, URL, description, category, and custom fields.
- Open only contract-validated HTTP or HTTPS destinations in an isolated browsing context.
- Relate and favorite links while preserving location-aware access.
#### Common uses
- Team resource directories.
- Customer/vendor portals and external documentation.
- Research links connected to cases, projects, accounts, or knowledge.
**Security boundary:** The server accepts strict host-based HTTP(S) URLs without embedded credentials. The runtime opens destinations with an isolated external context and applies normal tenant, DataRole, and location checks to the link record.
[Open the complete Web links implementation guide](../first-class-modules/web-links.html)
****Chat rooms** `page-module`**
Provides secured team rooms with messages, unread state, membership, room administration, mute settings, reactions, archives, custom fields, relations, activity, and favorites.
| Experience | Members-only team discussion |
| --- | --- |
| Native page key | `module.chat-rooms` |
| React implementation | `ChatRoomsModule` |
| API roots | `/api/modules/chat-rooms` |
#### What developers can build
- Search active or archived rooms and see unread counts.
- Create/edit rooms, post messages, and react with bounded reaction types.
- Manage members, room administrators, mute state, and archive state when authorized.
- Show location, dynamic fields, related records, activity, and favorites.
#### Common uses
- Project or operations team chat.
- Location, department, incident, or customer team rooms.
- Informal collaboration adjacent to records that retain the formal work state.
**Security boundary:** Rooms are shared-only: explicitly select one eligible DataRole and an authorized location or All when available. Room and message reads require current membership plus DataRole/location access, including Graph, Activity, Favorites and direct record links. Native Chat Rooms search remains available; general and AI search exclude chat content. Saves recheck current authority. Member administration, room edits and archiving require the appropriate room capability; members can post and edit their own messages without gaining room-administration rights.
[Open the complete Chat rooms implementation guide](../first-class-modules/chat-rooms.html)
****Discussion Boards** `page-module`**
Provides boards, topics, threaded posts, replies, pin/lock moderation, archives, owned-post editing, custom fields, relations, activity, and favorites.
| Experience | Structured community discussion |
| --- | --- |
| Native page key | `module.discussion-boards` |
| React implementation | `DiscussionBoardsModule` |
| API roots | `/api/modules/discussion-boards` |
#### What developers can build
- Search active or archived boards and browse topic/post counts.
- Create boards and topics, reply to topics, and edit owned or otherwise authorized content.
- Pin or lock topics and archive boards with the required moderation capability.
- Favorite boards/topics and show secured relations, activity, location, and dynamic fields.
#### Common uses
- Customer or partner communities.
- Internal Q&A, proposals, and long-running technical discussions.
- Program, project, or policy forums that benefit from topic-level history.
**Security boundary:** Board, topic, and post operations enforce tenant, DataRole, location, ownership, and moderation rules. Locked topics refuse ordinary replies, and edit controls reflect secured response capabilities.
[Open the complete Discussion Boards implementation guide](../first-class-modules/discussion-boards.html)
****Mail reader** `page-module`**
Provides a read-focused view across authorized personal/shared mail accounts with search, unread filtering, plain-text message detail, read state, relations, activity, dynamic fields, and favorites.
| Experience | Secured inbound mail |
| --- | --- |
| Native page key | `module.mail-reader` |
| React implementation | `MailReaderModule` |
| API roots | `/api/modules/mail-reader` |
#### What developers can build
- Select authorized personal or shared accounts and view unread counts.
- Search messages and filter unread mail.
- Read safe plain-text headers, previews, and bodies; raw HTML is not rendered.
- Mark permitted messages read/unread and connect them to favorites, relations, activity, and custom fields.
#### Common uses
- Shared operations or support mailbox review.
- Account/project email research linked to business records.
- Read-focused compliance or case correspondence views.
**Security boundary:** Account ownership/shared access, SaaS, tenant, DataRole, and location are enforced server-side. Incoming-server settings, credential references, raw HTML, and private provider details are excluded from browser responses.
[Open the complete Mail reader implementation guide](../first-class-modules/mail-reader.html)
****Favorites** `page-module`**
Gives each user a secured cross-module list of saved records with search, module filters, pinning, change tracking, related context, activity, and dynamic fields.
| Experience | Personal workspace |
| --- | --- |
| Native page key | `module.favorites` |
| React implementation | `FavoritesModule` |
| API roots | `/api/modules/favorites` |
#### What developers can build
- Search personal favorites and filter by source module.
- Pin, reorder, remove, and optionally track changes on saved records.
- Show current secured status, location, relations, activity, and dynamic fields.
- Collapse duplicate favorite targets into a coherent personal workspace.
#### Common uses
- Pinned customers, projects, cases, files, or articles.
- A personal watch list for records whose changes matter.
- Cross-module navigation for frequently used work.
**Security boundary:** Only the current user's favorite rows are returned. Target records, related records, fields, activity, and location are independently secured, so a favorite never preserves access after permission is removed.
[Open the complete Favorites implementation guide](../first-class-modules/favorites.html)
****Notifications** `page-module`**
Provides recipient-scoped alerts with unread filtering, detail, dismissal, source-record context, relations, activity, and source favorites.
| Experience | Personal alerts |
| --- | --- |
| Native page key | `module.notifications` |
| React implementation | `NotificationsModule` |
| API roots | `/api/modules/notifications` |
#### What developers can build
- Search current-recipient notifications and filter unread items.
- Open bounded detail and mark or dismiss notification state.
- Show a permitted source title, location, relations, and activity.
- Favorite an authorized source record directly from the notification.
#### Common uses
- Assignment, mention, publication, and status-change alerts.
- Workflow completion, failure, or approval attention.
- Personal reminders that link back to the canonical source record.
**Security boundary:** Only rows for the current recipient and app/account are returned. Linked source metadata appears only after its normal DataRole and location checks; the browser cannot submit recipient or permission authority.
[Open the complete Notifications implementation guide](../first-class-modules/notifications.html)
****Dynamic Records** `page-module`**
Renders tenant-owned custom modules from their validated field schema with secured list, search, create, detail, edit, status, relations, activity, and favorites.
| Experience | Builder-defined data |
| --- | --- |
| Native page key | `module.dynamic-records` |
| React implementation | `DynamicModulesModule` |
| API roots | `/api/modules/dynamic` |
#### What developers can build
- 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.
**Security 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.
[Open the complete Dynamic Records implementation guide](../first-class-modules/dynamic-records.html)
****ActiveWorkplace** `page-module`**
Pins any permitted Record into a collaborative workplace over its secured recursive relationship graph, with people, GoClaws, workflows, Record Pulse, Inbox attention, activity, and read-only Graph Playback.
| Experience | Recursive work coordination |
| --- | --- |
| Native page key | `module.active-workplace` |
| React implementation | `ActiveWorkplaceModule` |
| API roots | `/api/modules/active-workplace` |
#### What developers can build
- Pin any currently permitted Record once as the workplace root.
- Capture a cycle-safe, bounded Active Graph over existing typed Record Relations.
- Coordinate named users, GoClaw assistants, and published workflows with explicit responsibilities.
- Track Active, Waiting, At Risk, Stale, Dormant, and Resolved Record Pulse states.
- Create or touch one Universal Inbox attention item when a workplace becomes At Risk or Stale.
- Record meaningful comments, participant changes, status transitions, related-record changes, and graph refreshes in an immutable activity stream.
- Reconstruct retained workplace, participant, graph, and activity state through unmistakably read-only Graph Playback while applying current permissions.
- Accept bounded builder defaults for root modules, graph depth/nodes, stale timing, participant types, GoClaw watchers, comments, and playback.
#### Common uses
- Complex customer, case, opportunity, project, asset, incident, contract, or collections coordination.
- A Wave-like shared work surface around connected records without copying those records.
- Cross-team handoffs where responsibilities, inactivity, dependencies, and history must stay visible.
- GoClaw monitoring and workflow participation under the same permissions and approvals as the rest of the app.
**Security boundary:** The server derives app, account, user, DataRole, and locations. Root creation requires current read/edit access; each displayed graph node is reauthorized; GoClaw assignment requires independent AI-read access; and playback applies current authorization before returning any retained identity or event. Direct table access is denied to runtime and Pulse-worker roles.
[Open the complete ActiveWorkplace implementation guide](../first-class-modules/active-workplace.html)
****Record relations** `page-module`**
Explores authorized records as a bounded relationship graph and supports typed relation creation/removal only when both endpoints are editable.
| Experience | Connected context |
| --- | --- |
| Native page key | `module.record-graph` |
| React implementation | `RecordGraphModule` |
| API roots | `/api/modules/record-graph` |
#### What developers can build
- Search visible seed records across modules.
- Traverse secured relationships through bounded depth levels.
- Show nodes, typed edges, module, location, status, activity counts, and favorites.
- Add or remove a typed relation only when both record endpoints and the relation operation are authorized.
#### Common uses
- Customer/account 360-degree context.
- Case, project, asset, and work dependency exploration.
- Impact analysis and relationship-first CRM experiences.
**Security boundary:** Every traversed node is independently checked for SaaS, tenant, DataRole, record, and location visibility. Inaccessible nodes and edges are omitted, and mutation requires edit authority on both endpoints.
[Open the complete Record relations implementation guide](../first-class-modules/record-relations.html)
****Record activity** `page-module`**
Shows a paged, filterable, chronological activity timeline for an authorized record with actor, event type/state, date range, relations, and favorite state.
| Experience | Timeline & history |
| --- | --- |
| Native page key | `module.record-activity` |
| React implementation | `RecordActivityModule` |
| API roots | `/api/modules/record-activity` |
#### What developers can build
- Search visible records across modules and select a record timeline.
- Filter activity by event type and date range.
- Group events chronologically with summary, actor, state, and occurrence time.
- Page bounded history and show secured relation/favorite context.
#### Common uses
- Customer, project, work-order, case, and asset timelines.
- Operational handoffs and investigation context.
- A user-facing history adjacent to formal compliance/audit evidence.
**Security boundary:** The selected record must pass normal scope checks, and only safe activity summaries/metadata are rendered. Private event detail payloads stay outside the browser surface.
[Open the complete Record activity implementation guide](../first-class-modules/record-activity.html)
****Global secured search** `page-module`**
Searches permitted records across modules and also uses permission-filtered vector retrieval to find meaningfully related information in indexed uploads, PDFs, images, screenshots, tables, diagrams, and text.
| Experience | Cross-module discovery |
| --- | --- |
| Native page key | `module.global-search` |
| React implementation | `GlobalSearchModule` |
| API roots | `/api/modules/global-search` |
#### What developers can build
- Run bounded text search across authorized modules and records.
- Search eligible indexed uploads, PDFs, images, screenshots, tables, diagrams, and extracted text in a shared vector space.
- Find conceptually related information even when the source uses different words from the query.
- Retrieve the original text, page, image, and source identity so results can retain evidence and citations.
- Filter by module and page through stable result envelopes.
- Show safe matched-field snippets plus module, status, location, relation, activity, and update context.
- Add or remove favorites without leaving the result list.
#### Common uses
- App-wide command and search experiences.
- Finding a record when its owning module is unknown.
- Discovering related material across records and uploaded documents.
- Finding a PDF page, image, screenshot, chart, table, or diagram from a natural-language description.
- Cross-module navigation in relationship-heavy applications.
**Security boundary:** The server applies SaaS, tenant, CanAiReadRecords, DataRole, location, record, field, and AI-eligibility rules before vector candidates or visual evidence can be returned. Unindexed, deleted, AI-excluded, or otherwise unauthorized content is absent from results. Relevance never grants access, and counts, snippets, citations, and related context must not reveal inaccessible records or fields.
[Open the complete Global secured search implementation guide](../first-class-modules/global-search.html)
## Styled preview
**Modules**
- Universal Inbox
- Contacts
- Files
- Work Orders
First-class module
### Universal Inbox
**SUP-000123**
Waiting for customer
**Website request**
New and unassigned
**Bounded data path**
1. Authenticated API
2. Typed service
3. Reviewed procedure
4. Scoped JSON
The unstyled exports contain the same landmarks, labels, lists, tables, loading state, empty state and error state without any CSS dependency.
## Start a complete page
The safest custom page starts with the official runtime bootstrap and renderer. The browser supplies only the page key. The server returns the applicable shell, menu, component contracts and authorized page document.
```
import { useEffect, useState, type ReactNode } from "react";
import type { RuntimeBootstrap } from "@buildwithhq/page-runtime";
export interface RuntimePageStarterProps {
readonly pageKey: string;
readonly loadBootstrap: (pageKey: string) => Promise;
readonly renderBootstrap: (bootstrap: RuntimeBootstrap) => ReactNode;
readonly styled?: boolean;
}
export const RuntimePageStarter = ({ pageKey, loadBootstrap, renderBootstrap, styled = false }: RuntimePageStarterProps) => {
const [bootstrap, setBootstrap] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let active = true;
setBootstrap(null); setError(null);
void loadBootstrap(pageKey).then((value) => { if (active) setBootstrap(value); })
.catch((failure: unknown) => { if (active) setError(failure instanceof Error ? failure.message : "Page unavailable."); });
return () => { active = false; };
}, [loadBootstrap, pageKey]);
return
{!bootstrap && !error &&
Loading page…
}
{error &&
{error}
}
{bootstrap && renderBootstrap(bootstrap)}
;
};
/* In apps/tenant-runtime, wire this to loadRuntimeBootstrap and RuntimePageRenderer.
Authentication is already held by the official API client; do not add SaaSAppId,
AppAccountId, UserId, DataRole or database-name props to this component. */
```
Tip
Inside apps/tenant-runtime, pass loadRuntimeBootstrap as loadBootstrap and render with RuntimePageRenderer. Keep the existing in-memory authenticated client; do not create a second token store.
## Core page blocks
| Component key | Purpose | Registry source |
| --- | --- | --- |
| `core.button` | Button | `packages/ui/src/index.tsx` |
| `core.container` | Container | `packages/ui/src/index.tsx` |
| `core.data-grid` | Data Grid | `packages/ui/src/index.tsx` |
| `core.form` | Form | `packages/ui/src/index.tsx` |
| `core.record-list` | Record List | `packages/ui/src/index.tsx` |
| `core.text` | Text | `packages/ui/src/index.tsx` |
Every block requires stable `_id`, `_type`, `props`, and `children`. The current runtime accepts up to 5,000 blocks and 60 levels of depth. A block type must have a returned component contract or it fails closed as unavailable.
## Menus and user types
A menu item is a Page, Group, Module, or External target. Page items point to shared `pageKey` values; they do not copy page content. User Types choose the intended menu and dashboard, while DataRoles and Locations independently authorize the resulting data.
```
const menu = [
{ id: "work", label: "Work", targetType: "Group", sortOrder: 10 },
{ id: "inbox", parentId: "work", label: "Inbox", targetType: "Page", pageKey: "inbox", sortOrder: 10 },
{ id: "contacts", parentId: "work", label: "Contacts", targetType: "Page", pageKey: "contacts", sortOrder: 20 },
] satisfies readonly BuildWithHQMenuItem[];
`/?page=${encodeURIComponent(key)}`}
styled
/>
```
**Copy the complete styled/unstyled menu component**
```
import type { ReactNode } from "react";
export interface BuildWithHQMenuItem {
readonly id: string;
readonly parentId?: string | null;
readonly label: string;
readonly iconKey?: string | null;
readonly targetType: "Page" | "Group" | "Module" | "External";
readonly pageKey?: string | null;
readonly url?: string | null;
readonly sortOrder: number;
}
export interface BuildWithHQMenuProps {
readonly items: readonly BuildWithHQMenuItem[];
readonly activePageKey?: string;
readonly resolvePageHref: (pageKey: string) => string;
readonly icon?: (iconKey: string) => ReactNode;
readonly styled?: boolean;
}
const safeExternalUrl = (value: string | null | undefined): string | undefined => {
if (!value || value.length > 2000) return undefined;
try {
const url = new URL(value);
return url.protocol === "https:" && !url.username && !url.password ? url.toString() : undefined;
} catch { return undefined; }
};
export const BuildWithHQMenu = ({ items, activePageKey, resolvePageHref, icon, styled = false }: BuildWithHQMenuProps) => {
const roots = items.filter((item) => !item.parentId).sort((left, right) => left.sortOrder - right.sortOrder);
const renderItem = (item: BuildWithHQMenuItem): ReactNode => {
const children = items.filter((candidate) => candidate.parentId === item.id).sort((left, right) => left.sortOrder - right.sortOrder);
if (item.targetType === "Group") return