| title | Validating Metadata |
|---|---|
| description | Why ObjectStack metadata mistakes fail silently at runtime, and the one command that catches them at author time — run it after every metadata edit. |
ObjectStack metadata is data, not code paths — so most mistakes are not caught
by the TypeScript compiler. They pass tsc, load fine, and then fail silently
at runtime. The fix is one command you run after every metadata edit:
os validate # schema + CEL predicates + widget bindings — no artifactIn a scaffolded project this is wired as npm run validate. Your generated
AGENTS.md instructs coding agents (Claude Code, Cursor, Copilot) to run it
after editing metadata.
Two classes of bug type-check cleanly but break at runtime:
Predicates — an action's visible/disabled, a field's requiredWhen, a
validation rule, a flow condition, a sharing rule — are CEL expressions, and
they reference record fields through the record. scope:
// ✗ Wrong — `done` is a bare reference. It type-checks (it's just a string),
// but at runtime it resolves to null → the action is hidden on EVERY record.
{ name: 'mark_done', visible: '!done' }
// ✓ Right
{ name: 'mark_done', visible: '!record.done' }This is the trap behind the recurring "the button never shows / the rule never
fires" bugs (#2183/#2185). os validate parses every predicate and checks that
each record.<field> exists on the target object, so the bare ref fails the
gate with a located, did-you-mean message instead of shipping.
A dashboard widget points at a dataset and reads dimensions/values from it.
If a name doesn't resolve, the chart renders empty — no error (ADR-0021).
os validate resolves every binding against the declared datasets and fails on a
dangling one.
The same gate also checks dashboard-level filter fields. A dashboard's
dateRange and each globalFilters[] entry are broadcast into every widget's
analytics query, so a filter field that doesn't exist on a bound widget's object
emits invalid SQL (no such column: …) and crashes that widget at render time.
os validate fails when a filter's effective field — after any per-widget
filterBindings re-target — is absent on the widget's dataset object, naming the
dashboard, widget, filter, field, and object. Opt a widget out with
filterBindings: { <name>: false }, or re-target the filter to one of that
object's own fields.
A dashboard header.actions[] button (and a widget's actionUrl) names a target:
a script/modal action, or a url route. Nothing in the schema checks that the
target exists, so a button can ship pointing at an action defined nowhere — it
renders and then silently does nothing when clicked. This is ADR-0049's
"declared ≠ enforced" gate applied to references.
header: {
actions: [
// ✗ script/modal target resolves to no defined action → error
{ label: 'Export PDF', actionType: 'script', actionUrl: 'export_dashboard_pdf' },
// ✓ modal <verb>_<object> against a real `opportunity` object
{ label: 'New Deal', actionType: 'modal', actionUrl: 'create_opportunity' },
// ⚠ url path matching no in-app route → warning
{ label: 'Forecast', actionType: 'url', actionUrl: '/reports/forecast' },
],
}script/modal targets that resolve nowhere fail validation; a url path
whose objects/reports/dashboards/pages/views route is unregistered is warned
(external, interpolated, and opaque routes are skipped).
os validate and os build (alias of os compile) run the same validator:
os validate |
os build |
|
|---|---|---|
| Protocol schema (Zod) | ✓ | ✓ |
| CEL / predicate validation | ✓ | ✓ |
| Widget-binding integrity | ✓ | ✓ |
| Dashboard action/route references (ADR-0049) | ✓ | ✓ |
Security posture (ADR-0090 — e.g. every custom object declares sharingModel) |
✓ | ✓ |
Emits dist/objectstack.json |
— | ✓ |
So os validate is the fast inner-loop check (no artifact); os build is what
you run when you need the deployable artifact. A config that passes os validate
will not fail os build on schema/predicate/binding grounds. Both entry points
also check SDUI styling (ADR-0065), and os validate additionally runs a set of
view- and page-shape checks — list-view navigation modes (ADR-0053), view
container shape, and JSX/React page sources (ADR-0080/0081) — that catch UI
metadata which would otherwise be silently dropped.
A clean run walks each gate and reports timing:
◆ Validate
────────────────────────────────────────
→ Loading configuration...
Config: /path/to/support-desk/objectstack.config.ts
Load time: 21ms
→ Validating against ObjectStack Protocol...
→ Validating expressions (ADR-0032)...
→ Checking list-view navigation modes (ADR-0053)...
→ Checking view container shape...
→ Checking dashboard widget bindings (ADR-0021)...
→ Checking dashboard action references (ADR-0049)...
→ Checking SDUI styling (ADR-0065)...
→ Checking JSX-source pages (ADR-0080)...
→ Checking React-source pages (ADR-0081)...
→ Checking React-source page props (ADR-0081)...
→ Checking source-page styling (ADR-0065)...
→ Checking capability references (ADR-0066)...
→ Checking flow trigger wiring...
→ Checking security posture (ADR-0090 D7)...
✓ Validation passed (64ms)
Support Desk v0.1.0
Data: 1 Objects 6 Fields
UI: 1 Apps 1 Views 1 Actions
On failure the exit code is non-zero and the error is located and corrective — see the gate in action for the bare-reference example verbatim.
`os lint` is a **separate** pass — style and convention checks (snake_case naming, required labels, namespace prefixes, data-model patterns). Run it too, but it does not replace `os validate`, and `os validate` does not replace it.# after editing any *.object.ts / *.view.ts / *.action.ts / *.flow.ts / *.dashboard.ts
npm run validate # os validate — schema + predicates + bindings
npm run typecheck # tsc --noEmit — types against @objectstack/specRule of thumb: never report a metadata change as done until npm run validate
passes.
To validate one CEL expression before you write it into a file — for example
inside an AI build loop — call the validate_expression agent tool, which runs
the same predicate validator inline. See the
objectstack-formula skill.
Both commands support --json and exit non-zero on failure:
- name: Validate ObjectStack metadata
run: npx objectstack validate --strict --json- Command Line Interface — full
os validate/os buildreference - Formula & Predicates — the CEL surface and
record.scope - Data Modeling — objects, fields, validations, sharing