# 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); }} >
Attention workspace

Universal Inbox

)} ); } ``` #### Minimal custom-shell layout CSS ``` .bwhq-inbox-shell-overlay { position: fixed; z-index: 1000; inset: 0; display: grid; place-items: center; padding: 24px; background: rgb(15 23 42 / 55%); } .bwhq-inbox-shell-dialog { display: grid; grid-template-rows: auto minmax(0, 1fr); width: min(1440px, 100%); height: min(900px, calc(100vh - 48px)); overflow: hidden; border-radius: 18px; background: white; box-shadow: 0 30px 90px rgb(15 23 42 / 30%); } .bwhq-inbox-shell-dialog > header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 20px; border-bottom: 1px solid #e2e8f0; } .bwhq-inbox-shell-dialog h2 { margin: 2px 0 0; } .bwhq-inbox-shell-dialog .bwhq-inbox { min-height: 0; } @media (max-width: 760px) { .bwhq-inbox-shell-overlay { padding: 0; } .bwhq-inbox-shell-dialog { width: 100%; height: 100vh; border-radius: 0; } } ``` ### Unstyled headless list, detail, and resolution This version owns only presentation. Authentication remains an injected application concern and every list, detail, and mutation is still reauthorized by the server. The client deliberately accepts same-origin API paths only; reverse-proxy `/api/inbox` through the headless application's origin instead of putting SQL credentials, database names, or cross-origin service secrets in the browser. ``` import { useEffect, useMemo, useState } from "react"; import { createUniversalInboxHttpClient, type InboxItemDetail, type InboxQueueItem, type UniversalInboxClient, } from "@buildwithhq/universal-inbox"; interface HeadlessInboxAuth { readonly accessToken: () => string | null | undefined; readonly csrfToken: () => string | null | undefined; readonly authenticatedFetch?: typeof fetch; } interface UnstyledInboxProps { readonly client: UniversalInboxClient; } const itemTitle = (item: InboxQueueItem) => item.subject?.trim() || item.source.sourceKey || item.source.sourceType; export function UnstyledHeadlessInbox({ client }: UnstyledInboxProps) { const [ticketingOnly, setTicketingOnly] = useState(false); const [assignedToMe, setAssignedToMe] = useState(false); const [items, setItems] = useState([]); const [selected, setSelected] = useState(null); const [reload, setReload] = useState(0); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(); useEffect(() => { const request = new AbortController(); setLoading(true); setError(undefined); client.list({ ticketingOnly, assignedToMe, topN: 100 }, request.signal) .then(setItems) .catch((failure: unknown) => { if (!request.signal.aborted) { setError(failure instanceof Error ? failure.message : "Inbox unavailable."); } }) .finally(() => { if (!request.signal.aborted) setLoading(false); }); return () => request.abort(); }, [assignedToMe, client, reload, ticketingOnly]); const open = async (inboxItemId: string) => { setError(undefined); try { setSelected(await client.get(inboxItemId)); } catch (failure) { setError(failure instanceof Error ? failure.message : "Item unavailable."); } }; const resolveSelected = async () => { if (!selected) return; setBusy(true); setError(undefined); try { await client.setStatus(selected.inboxItemId, "Resolved"); setSelected(await client.get(selected.inboxItemId)); setReload((value) => value + 1); } catch (failure) { setError(failure instanceof Error ? failure.message : "Status was not changed."); } finally { setBusy(false); } }; return (

Universal Inbox

Visible work
{loading &&

Loading inbox...

} {error &&

{error}

} {!loading && !error && items.length === 0 &&

No visible work.

}
    {items.map((item) => (
  • ))}
{selected && (

{selected.ticket ? `Support ticket ${selected.ticket.key}` : selected.source.sourceType}

{selected.subject || selected.source.sourceKey || "Inbox item"}

{selected.previewText || "No preview available."}

{selected.aiSummary &&

Prepared context: {selected.aiSummary}

}

Activity

    {selected.activity.map((event) => (
  1. {event.eventType} {event.summary || "Activity recorded"}
  2. ))}
)}
); } 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

} {feed?.items.length === 0 &&

No checklists match this secured view.

}
    {feed?.items.map((run) => (
  1. ))}
{selected && (

{selected.title}

{selected.status} / revision {selected.revision}

    {selected.items.map((item) => (
  1. {item.title} {item.status}{item.isRequired ? " / required" : " / optional"} {item.responseText &&

    {item.responseText}

    } {item.evidence.length > 0 && (
      {item.evidence.map((proof) =>
    • {proof.proofKind}: {proof.fileName}
    • )}
    )}
  2. ))}
)}
); } ``` ### 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) => (
  1. ))}
); } ``` ### 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

    {feed.items.map((insight) => (
  1. {insight.insightCategory || insight.insightType}

    {insight.title}

    {insight.summary}

    {insight.sourceRecordTitle || insight.sourceModuleKey} {insight.severity ? ` / ${insight.severity}` : ""} {` / Priority ${insight.priorityScore}`}
  2. ))}
); } ``` ### 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 (

Work orders

{totalLabel}

{error &&

{error}

} {!loading && result.items.length === 0 &&

No visible work orders match these filters.

}
    {result.items.map((workOrder) => (
  • ))}
{selected && (

{selected.workOrderNumber} · {selected.summary}

{selected.description || "No description"}

Status
{selected.status}
Priority
{selected.priority}
Assignments
{selected.assignmentCount}
Related records
{selected.relationCount}
    {selected.assignments.map((assignment) => (
  • {assignment.assignedUserName || assignment.assignedUserId} · {assignment.assignmentRole} {assignment.isPrimary ? " · Primary" : ""}
  • ))}
)}
); } ``` ### 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

Reservations

{error &&

{error}

}
    {result.items.map(item =>
  • )}
{selected &&

{selected.reservationNumber} - {selected.productName}

{selected.contactName} - {selected.contactEmail || "No email"}

{selected.status} - {selected.paymentStatus || "No payment required"}

Follow-ups

    {selected.followups.map(item =>
  • {item.followupType}: {new Date(item.scheduledUtc).toLocaleString()} - {item.deliveryStatus}
  • )}
}
; } ``` ### 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 =>
  1. )}
{selected &&

{selected.title}

{selected.description}

{selected.attendeeCount} attendees - {selected.resourceBookingCount} resources - {selected.relationCount} related records

    {selected.attendees.map(person =>
  • {person.displayName || person.externalEmail} - {person.responseStatus}
  • )}
}
; } ``` ### 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

Files

{error &&

{error}

}
    {result.items.map(file =>
  • )}
{selected &&

{selected.fileName}

{selected.description}

{selected.tags.join(", ")}

Attached to

    {selected.attachments.map(link =>
  • {link.sourceTitle} ({link.sourceModuleKey || "record"})
  • )}
}
; } ``` ### Upload, replace metadata, attach, and detach Upload is multipart and capped at 25 MiB. The backend normalizes the filename and content type, streams through the configured security scanner, validates the measured size, and removes failed storage writes. Browser MIME metadata is never trusted as a security decision. ``` import type { FileDetail, FileDynamicField } from "@buildwithhq/module-sdk"; import { attachFile, detachFile, getFile, setFileFavorite, updateFile, uploadFile } from "./api"; const fieldInput = (field: FileDynamicField) => ({ 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 uploadEvidence(file: File, sourceRecordId: string) { return uploadFile(file, { description: "Completion evidence", tags: ["evidence"], sourceRecordId, sourceModuleKey: "checklists-signoffs" }); } export async function renameFile(current: FileDetail, fileName: string) { await updateFile(current.recordId, { expectedUpdatedUtc: current.updatedUtc, fileName, locationId: current.locationId || undefined, description: current.description || "", tags: current.tags, dynamicFields: current.dynamicFields.map(fieldInput), }); return getFile(current.recordId); } export async function linkFile(current: FileDetail, targetRecordId: string, targetModuleKey: string) { await attachFile(current.recordId, targetRecordId, targetModuleKey); return getFile(current.recordId); } export async function unlinkFile(current: FileDetail, attachmentId: string) { await detachFile(current.recordId, attachmentId); return getFile(current.recordId); } export async function toggleFileFavorite(current: FileDetail) { await setFileFavorite(current.recordId, !current.isFavorite); return getFile(current.recordId); } ``` ### Optional starter styling ``` .custom-files { width: 100%; max-width: none; } .custom-files form, .custom-files nav { align-items: end; display: flex; gap: .75rem; } .custom-files > ul { list-style: none; margin: 1rem 0; padding: 0; } .custom-files > ul button { background: transparent; border: 0; display: grid; gap: .25rem; padding: .75rem 0; text-align: left; width: 100%; } .custom-files article { border: 1px solid var(--line, #d8dee8); margin-top: 1rem; padding: 1rem; } ``` ### Exact data path | Purpose | Route | Procedure/storage behavior | | --- | --- | --- | | List/detail | `GET /api/modules/files`, `GET /api/modules/files/{recordId}` | `sp_Files_ListSecured`, `sp_Files_GetSecured` | | Upload | `POST /api/modules/files/upload` | Scan + private storage + `sp_Files_CreateSecured` | | Metadata replace | `PUT /api/modules/files/{recordId}` | `sp_Files_UpdateSecured`, optimistic concurrency | | Attach/detach | `POST/DELETE /api/modules/files/{recordId}/attachments` | `sp_Files_AttachSecured`, `sp_Files_DetachSecured` | | Download | `GET /api/modules/files/{recordId}/content` | `sp_Files_GetDownloadSecured` authorizes before private streaming | | Favorite | `PUT /api/modules/files/{recordId}/favorite` | `sp_Files_SetFavoriteSecured` | Attachment targets are independently reauthorized. A file visible to a user does not make every linked record visible, and a record identifier does not authorize attachment or download. ## Build a professional Files dashboard These six registry-backed presentation blocks let a designer turn the secured Files 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/files`; 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/files-sample.json`, its executable authenticated page at `pages/first-class/files-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 Files projection | | --- | --- | --- | | `metric-set` | `core.metric-strip` | Visible, newly uploaded, pending-scan, and blocked counts | | `entity-list` | `core.entity-list` | Recent authorized documents and photos | | `progress-list` | `core.progress-list` | Distribution by type, scan state, or attachment use | | `series-chart` | `core.series-chart` | Upload, scan, download, and link activity | | `data-grid` | `core.presentation-grid` | One bounded page without storage routes or secrets | | `timeline` | `core.timeline` | Upload, scan, metadata edit, relation, download, and deletion | ### 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": "files-metrics", "_type": "core.metric-strip", "props": { "title": "File operations", "asOfUtc": "2026-09-04T18:00:00Z", "items": [ { "key": "visible", "label": "Visible files", "value": 932, "format": "number", "tone": "neutral" }, { "key": "uploaded", "label": "Uploaded this month", "value": 114, "format": "number", "tone": "success" }, { "key": "pending", "label": "Scan pending", "value": 3, "format": "number", "tone": "warning" }, { "key": "blocked", "label": "Blocked", "value": 1, "format": "number", "tone": "danger" } ] }, "children": [] }, { "_id": "files-recent", "_type": "core.entity-list", "props": { "title": "Recent files", "hasMore": true, "items": [ { "id": "files-sample-1", "recordId": "files-record-1", "primary": "north-wing-closeout.pdf", "secondary": "PDF - 2.4 MB - WO-1048", "status": { "key": "available", "label": "Available", "tone": "success" }, "trailing": "2 links" }, { "id": "files-sample-2", "recordId": "files-record-2", "primary": "north-wing-closeout.pdf - Follow-up", "secondary": "PDF - 2.4 MB - WO-1048 - Updated two hours ago by the assigned owner", "status": { "key": "in-review", "label": "In review", "tone": "primary" }, "trailing": "Today" }, { "id": "files-sample-3", "recordId": "files-record-3", "primary": "north-wing-closeout.pdf - West region", "secondary": "PDF - 2.4 MB - WO-1048 - Related to three visible records at the Reno location", "status": { "key": "on-track", "label": "On track", "tone": "success" }, "trailing": "3 related" }, { "id": "files-sample-4", "recordId": "files-record-4", "primary": "north-wing-closeout.pdf - Customer response", "secondary": "PDF - 2.4 MB - WO-1048 - Waiting for an external response before work can continue", "status": { "key": "scheduled", "label": "Scheduled", "tone": "warning" }, "trailing": "Tomorrow" }, { "id": "files-sample-5", "recordId": "files-record-5", "primary": "north-wing-closeout.pdf - Regional operations review with a deliberately long title", "secondary": "PDF - 2.4 MB - WO-1048 - 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": "files-sample-6", "recordId": "files-record-6", "primary": "north-wing-closeout.pdf - Completed preview", "secondary": "PDF - 2.4 MB - WO-1048 - Closed after review with its related evidence retained", "status": { "key": "complete", "label": "Complete", "tone": "success" }, "trailing": "Closed" } ] }, "children": [] }, { "_id": "files-bystatus", "_type": "core.progress-list", "props": { "title": "Files by type", "items": [ { "key": "documents", "label": "Documents", "value": 531, "maximum": 932, "displayValue": "531", "tone": "primary", "status": { "key": "documents", "label": "Documents", "tone": "primary" } }, { "key": "images", "label": "Images", "value": 327, "maximum": 932, "displayValue": "327", "tone": "success", "status": { "key": "images", "label": "Images", "tone": "success" } }, { "key": "other", "label": "Other", "value": 74, "maximum": 932, "displayValue": "74", "tone": "neutral", "status": { "key": "other", "label": "Other", "tone": "neutral" } } ] }, "children": [] }, { "_id": "files-trend", "_type": "core.series-chart", "props": { "title": "File activity", "variant": "bar", "defaultPeriodKey": "d7", "periods": [ { "key": "d7", "label": "7 days", "labels": [ "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu" ], "series": [ { "key": "primary", "label": "Uploaded", "tone": "primary", "values": [ 8, 5, 4, 12, 15, 11, 17 ] }, { "key": "secondary", "label": "Downloaded", "tone": "success", "values": [ 6, 4, 3, 9, 12, 10, 14 ] } ] } ] }, "children": [] }, { "_id": "files-table", "_type": "core.presentation-grid", "props": { "title": "Authorized files", "columns": [ { "key": "file", "label": "File", "type": "text", "align": "left" }, { "key": "type", "label": "Type", "type": "text", "align": "left" }, { "key": "size", "label": "Size (KB)", "type": "number", "align": "right" }, { "key": "scan", "label": "Scan", "type": "status", "align": "left" }, { "key": "updated", "label": "Updated", "type": "date", "align": "left" } ], "rows": [ { "id": "files-row-1", "recordId": "files-record-1", "cells": { "file": "north-wing-closeout.pdf", "type": "PDF", "size": 2458, "scan": { "key": "clean", "label": "Available", "tone": "success" }, "updated": "2026-09-04T14:08:00Z" } }, { "id": "files-row-2", "recordId": "files-record-2", "cells": { "file": "north-wing-closeout.pdf - Follow-up", "type": "PDF", "size": 2459, "scan": { "key": "in-review", "label": "In review", "tone": "primary" }, "updated": "2026-09-04T15:42:00Z" } }, { "id": "files-row-3", "recordId": "files-record-3", "cells": { "file": "north-wing-closeout.pdf - West region", "type": "PDF", "size": 2460, "scan": { "key": "on-track", "label": "On track", "tone": "success" }, "updated": "2026-09-04T12:18:00Z" } }, { "id": "files-row-4", "recordId": "files-record-4", "cells": { "file": "north-wing-closeout.pdf - Customer response", "type": "PDF", "size": 2461, "scan": { "key": "scheduled", "label": "Scheduled", "tone": "warning" }, "updated": "2026-09-03T21:07:00Z" } }, { "id": "files-row-5", "recordId": "files-record-5", "cells": { "file": "north-wing-closeout.pdf - Regional operations review with a deliberately long title", "type": "PDF - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.", "size": 2462, "scan": { "key": "needs-attention", "label": "Needs attention", "tone": "danger" }, "updated": "2026-09-03T16:31:00Z" } }, { "id": "files-row-6", "recordId": "files-record-6", "cells": { "file": "north-wing-closeout.pdf - Completed preview", "type": "PDF", "size": 2463, "scan": { "key": "complete", "label": "Complete", "tone": "success" }, "updated": "2026-09-02T19:14:00Z" } } ], "page": { "pageNumber": 1, "pageSize": 6, "totalRecords": 932, "totalIsExact": true, "hasMore": true } }, "children": [] }, { "_id": "files-timeline", "_type": "core.timeline", "props": { "title": "File activity", "hasMore": true, "items": [ { "id": "files-event-1", "recordId": "files-record-1", "occurredUtc": "2026-09-04T17:58:00Z", "title": "File scan completed", "description": "The uploaded PDF passed content scanning.", "actor": "File security worker", "tone": "success" }, { "id": "files-event-2", "recordId": "files-record-2", "occurredUtc": "2026-09-04T15:42:00Z", "title": "File scan completed - Follow-up", "description": "The uploaded PDF passed content scanning. Updated two hours ago by the assigned owner.", "actor": "Avery Patel", "tone": "primary" }, { "id": "files-event-3", "recordId": "files-record-3", "occurredUtc": "2026-09-04T12:18:00Z", "title": "File scan completed - West region", "description": "The uploaded PDF passed content scanning. Related to three visible records at the Reno location.", "actor": "Sam Rivera", "tone": "success" }, { "id": "files-event-4", "recordId": "files-record-4", "occurredUtc": "2026-09-03T21:07:00Z", "title": "File scan completed - Customer response", "description": "The uploaded PDF passed content scanning. Waiting for an external response before work can continue.", "actor": "Maya Chen", "tone": "warning" }, { "id": "files-event-5", "recordId": "files-record-5", "occurredUtc": "2026-09-03T16:31:00Z", "title": "File scan completed - Regional operations review with a deliberately long title", "description": "The uploaded PDF passed content scanning. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.", "actor": "Automation", "tone": "danger" }, { "id": "files-event-6", "recordId": "files-record-6", "occurredUtc": "2026-09-02T19:14:00Z", "title": "File scan completed - Completed preview", "description": "The uploaded PDF passed content scanning. 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 FilesPresentationData { readonly metrics: unknown; readonly recent: unknown; readonly byStatus: unknown; readonly trend: unknown; readonly table: unknown; readonly timeline: unknown; } export interface FilesPresentationProps { /** Pass only the already-authorized presentation document returned by the API. */ readonly data?: FilesPresentationData | 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 FilesPresentation({ data, loading = false, error = false, styled = false, onOpenRecord, }: FilesPresentationProps) { if (error) return

The Files presentation could not be loaded.

; if (loading || !data) return

Loading Files presentation...

; return (

Files

File operations

Visible documents, scan state, attachment coverage, file types, and controlled 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/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

Conversations

{error &&

{error}

}
    {result.items.map(thread =>
  • )}
{selected &&

{selected.subject}

{selected.channelName} - {selected.status}

{selected.messages.map(message =>
{message.isPrivateNote ? "Internal note" : message.direction}

{message.bodyText || "HTML message"}

{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

Company news

{result.canEdit && New announcement}
{error &&

{error}

}
    {result.items.map(item =>
  1. )}
{selected &&

{selected.title}

{selected.body}

{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 =>
  1. )}
{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 && }
; } ``` ### 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

Team chat

{ e.preventDefault(); setPage(1); setSearch(draft.trim()); }}> setDraft(e.target.value)} />
    {rooms.items.map(room =>
  • )}
{selected &&

{selected.roomName}

{selected.messages.map(message =>

{message.postedByDisplayName || "Member"}: {message.messageText}

)}{selected.canPost &&