First Class Modules
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.
core.universal-inboxUse 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.
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
ActivityVersionwhen 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
UniversalInboxBellin the top bar and opens the completeUniversalInboxpanel. No custom React is required. Builders may also place the registeredcore.universal-inboxblock 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
- 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.
- If users also need a permanent workspace, create a shared page with PageKey
Inbox, add the registeredcore.universal-inboxblock, save, validate, and publish it. - 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 makes the route eligible but does not grant Inbox data. - Connect each supported external channel through its reviewed email, SMS, web-form, or inbound-endpoint contract.
- For a support mailbox, open Email ticketing setup as the tenant account owner, check Ticketing, and select a unique operational prefix.
- Define the default Unsorted/Triage queue so unmatched communication remains visible and owned operationally.
- Add routing rules for source, recipient/line, form, intent, customer, location, priority, or other permitted context.
- Configure queues, assignment, SLA/escalation, and the existing permissions for the people who will triage or act.
- 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.
- 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 (
<main aria-label="Inbox workspace" style={{ minWidth: 0, width: "100%" }}>
<UniversalInbox
key={initialInboxItemId ?? "all"}
client={inboxClient}
initialInboxItemId={initialInboxItemId}
/>
</main>
);
}
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<string>();
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 (
<>
<UniversalInboxBell
client={inboxClient}
onOpenInbox={(inboxItemId) => {
setInitialInboxItemId(inboxItemId);
setOpen(true);
}}
/>
{open && (
<div
className="bwhq-inbox-shell-overlay"
onMouseDown={(event) => {
if (event.target === event.currentTarget) setOpen(false);
}}
>
<section
aria-label="Universal Inbox"
aria-modal="true"
className="bwhq-inbox-shell-dialog"
role="dialog"
>
<header>
<div><small>Attention workspace</small><h2>Universal Inbox</h2></div>
<button type="button" onClick={() => setOpen(false)}>Close</button>
</header>
<UniversalInbox
key={initialInboxItemId ?? "all"}
client={inboxClient}
initialInboxItemId={initialInboxItemId}
/>
</section>
</div>
)}
</>
);
}
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<readonly InboxQueueItem[]>([]);
const [selected, setSelected] = useState<InboxItemDetail | null>(null);
const [reload, setReload] = useState(0);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string>();
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 (
<main>
<h1>Universal Inbox</h1>
<fieldset>
<legend>Visible work</legend>
<label><input type="checkbox" checked={ticketingOnly} onChange={(event) => setTicketingOnly(event.target.checked)} /> Support tickets only</label>
<label><input type="checkbox" checked={assignedToMe} onChange={(event) => setAssignedToMe(event.target.checked)} /> Assigned to me</label>
<button type="button" onClick={() => setReload((value) => value + 1)}>Refresh</button>
</fieldset>
{loading && <p role="status">Loading inbox...</p>}
{error && <p role="alert">{error}</p>}
{!loading && !error && items.length === 0 && <p>No visible work.</p>}
<div>
<ul aria-label="Inbox queue">
{items.map((item) => (
<li key={item.inboxItemId}>
<button type="button" onClick={() => void open(item.inboxItemId)}>
<strong>{item.ticket ? `${item.ticket.key} - ` : ""}{itemTitle(item)}</strong>
<span>{item.status} - {item.previewText || "No preview"}</span>
</button>
</li>
))}
</ul>
{selected && (
<article aria-live="polite">
<p>{selected.ticket ? `Support ticket ${selected.ticket.key}` : selected.source.sourceType}</p>
<h2>{selected.subject || selected.source.sourceKey || "Inbox item"}</h2>
<p>{selected.previewText || "No preview available."}</p>
{selected.aiSummary && <p><strong>Prepared context:</strong> {selected.aiSummary}</p>}
<button
type="button"
disabled={busy || selected.status === "Resolved" || selected.status === "Closed"}
onClick={() => void resolveSelected()}
>
{busy ? "Saving..." : "Resolve"}
</button>
<h3>Activity</h3>
<ol>
{selected.activity.map((event) => (
<li key={event.activityVersion}>
<strong>{event.eventType}</strong> {event.summary || "Activity recorded"}
<time dateTime={event.createdUtc}>{event.createdUtc}</time>
</li>
))}
</ol>
</article>
)}
</div>
</main>
);
}
export function HeadlessInboxApp({
accessToken,
csrfToken,
authenticatedFetch,
}: HeadlessInboxAuth) {
const client = useMemo(
() => createUniversalInboxHttpClient({
accessToken,
csrfToken,
...(authenticatedFetch ? { fetchImplementation: authenticatedFetch } : {}),
}),
[accessToken, authenticatedFetch, csrfToken],
);
return <UnstyledHeadlessInbox client={client} />;
}
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/ticketingPUT /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 for generated starters and the headless guide 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.
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, 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 <p role="alert">The Universal Inbox presentation could not be loaded.</p>;
if (loading || !data) return <p role="status">Loading Universal Inbox presentation...</p>;
return (
<main className={styled ? "bwhq-api-example" : undefined}>
<header>
<p>Universal Inbox</p>
<h1>Inbox operations</h1>
<p>Inbound attention, support tickets, queues, SLA state, ownership, and resolution activity.</p>
</header>
<MetricStrip data={data.metrics} styled={styled} />
<div className={styled ? "bwhq-api-example__split" : undefined}>
<ProgressList data={data.byStatus} styled={styled} />
<EntityList data={data.recent} styled={styled} onOpenRecord={onOpenRecord} />
</div>
<SeriesChart data={data.trend} styled={styled} />
<PresentationGrid data={data.table} styled={styled} onOpenRecord={onOpenRecord} />
<Timeline data={data.timeline} styled={styled} onOpenRecord={onOpenRecord} />
</main>
);
}
Copy the live, authenticated module page
This document has no placeholder key and needs no invented endpoint. Save it to a page and add that page to a User Type menu. The registered native component calls /api/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.
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.
A renderer being bundled in the tenant application does not make its data visible in every app. The server returns only components and records authorized for the current app and signed-in user; unavailable or unauthorized blocks fail closed.
Capability review: 2026-09-14. For exact current technical availability, use the generated API Map and first-class module inventory.