Manifest
One JSON file per view, submitted with the build. It says who wrote the view, where it’s served from, which surfaces it wants to appear on, and what it needs permission to touch — the unit the review pipeline works on.
{
"id": "revenue-timeline",
"name": "Revenue Timeline",
"version": "1.0.0",
"description": "A timeline of closed deals.",
"publisher": { "name": "Acme", "email": "dev@acme.example" },
"entry": "https://apps.acme.example/timeline/index.html",
"views": [
{ "id": "timeline", "name": "Timeline", "surface": "module", "defaultHeight": 600 }
],
"scopes": ["records:read", "values:read"],
"permissionsRationale": {
"records:read": "To place each deal on the timeline."
}
}Fields
| Field | Rule |
|---|---|
id | Kebab-case, unique across the marketplace, permanent. |
name | Required, 60 characters or fewer. |
version | Semver, e.g. "1.0.0". A new version is a new review. |
publisher.name / .email | Required — a contactable name and email. |
entry | Absolute https URL. The host pins this origin for postMessage — a security value, not merely a location. No fragment; a query string draws a warning (the host appends its own cx* parameters). |
views | At least one. Each needs a kebab-case id unique within the manifest, a name, and a surface. |
views[].surface | "module" (a tab beside the grid — the monday-style custom view), "record" (a panel in the record view), or "workspace" (a full page). |
views[].defaultHeight | 120–2000px. The view can ask to resize once running. |
views[].settings | Fields the host renders in a settings pane for an installed view — text, number, boolean, select, or column (lets the person pick a column from the mounted module). |
scopes | [] if the view needs no data access at all — never omitted. |
permissionsRationale | One sentence per requested scope, shown to the reviewer and to the person installing. |
Validating
import { parseManifest, reviewSummary } from "@conexus-x/sdk/manifest";
const result = parseManifest(json); // https entry required
const local = parseManifest(json, { allowLocalhostEntry: true }); // test env only
if (!result.ok) console.error(result.errors); // every problem at once, not the firstCollects every problem in one pass rather than throwing on the first — an author fixing one error per submission round is the slowest possible way to publish a view, and a reviewer wants the whole picture at once too. warnings come back alongside errors even on a passing result — e.g. a :write scope requested without its :read counterpart, which almost always means the view can change something but not read back what it just did.
allowLocalhostEntry exists only for the test environment. A submission with an http://localhost entry is never valid — the whole point of pinning an origin is that customers reach the exact code that was reviewed.The approval screen
reviewSummary(manifest) turns each requested scope into the plain-English list of endpoints it actually unlocks (the same summaries from Scopes & routes), alongside the developer’s own permissionsRationale — so an admin approves a list of concrete capabilities, never a bare string like records:write whose reach they have to take on trust.
The pipeline
- Build the view against the SDK, using a manifest that validates.
- Point it at a sandbox (test) workspace to develop against real data with no customer risk.
- Submit — the same
parseManifestruns again server-side; local validation is a courtesy, not the rule. - Automated test cases and admin approval, reviewing exactly the scopes above.
- Live on the CRM, installable into a real workspace.