First Class Modules
Company news
Publishes searchable internal announcements with draft, scheduled, published, and expired visibility, author controls, custom fields, relationships, activity, and favorites.
module.company-newsUse Company News for controlled organization-wide or location-scoped announcements such as policy changes, releases, leadership updates, closures, and internal notices.
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
- Open the SaaS app in the Developer Console and identify the user journey and page where this module belongs.
- Add the validated
module.company-newsmodule block through the supported page/template authoring flow. - 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.
- Place the page in the correct user-type menus and assign existing DataRole, record, field, and location permissions.
- Test list, detail, search, empty, denied, stale-update, and cross-location behavior before publishing an exact version.
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, 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<CompanyNewsList>(empty);
const [selected, setSelected] = useState<CompanyNewsDetail | null>(null);
const [error, setError] = useState<string | null>(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 <section className="custom-news">
<header><h1>Company news</h1>{result.canEdit && <a href="#news-composer">New announcement</a>}</header>
<form role="search" onSubmit={submit}><label>Search<input value={draft} onChange={e => setDraft(e.target.value)} /></label><button>Search</button></form>
{error && <p role="alert">{error}</p>}
<ol>{result.items.map(item => <li key={item.recordId}><button onClick={() => void open(item.recordId)}><strong>{item.title}</strong><span>{item.isPublished ? "Published" : "Draft"} - {item.locationName || "All locations"}</span></button></li>)}</ol>
<nav><button disabled={page <= 1} onClick={() => setPage(value => value - 1)}>Previous</button><span>Page {result.pageNumber} of {Math.max(1, result.totalPages)}</span><button disabled={page >= result.totalPages} onClick={() => setPage(value => value + 1)}>Next</button></nav>
{selected && <article><h2>{selected.title}</h2><p>{selected.body}</p><small>{selected.status} - {selected.createdByDisplayName || "Company news"}</small></article>}
</section>;
}
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";
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, locationId?: string) {
return createCompanyNews({ title, body, locationId, 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.
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, 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 <p role="alert">The Company news presentation could not be loaded.</p>;
if (loading || !data) return <p role="status">Loading Company news presentation...</p>;
return (
<main className={styled ? "bwhq-api-example" : undefined}>
<header>
<p>Company news</p>
<h1>Company news overview</h1>
<p>Published announcements, drafts, schedules, audience reach, and editorial 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/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.
Use Company News for timely announcements and Knowledge Articles for maintained reference material. Link the two when an announcement introduces a lasting policy.
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.