|
| 1 | +--- |
| 2 | +title: Validating Metadata |
| 3 | +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. |
| 4 | +--- |
| 5 | + |
| 6 | +# Validating Metadata |
| 7 | + |
| 8 | +ObjectStack metadata is data, not code paths — so most mistakes are **not** caught |
| 9 | +by the TypeScript compiler. They pass `tsc`, load fine, and then fail **silently |
| 10 | +at runtime**. The fix is one command you run after every metadata edit: |
| 11 | + |
| 12 | +```bash |
| 13 | +os validate # schema + CEL predicates + widget bindings — no artifact |
| 14 | +``` |
| 15 | + |
| 16 | +In a scaffolded project this is wired as `npm run validate`. Your generated |
| 17 | +`AGENTS.md` instructs coding agents (Claude Code, Cursor, Copilot) to run it |
| 18 | +after editing metadata. |
| 19 | + |
| 20 | +## Why typecheck isn't enough |
| 21 | + |
| 22 | +Two classes of bug type-check cleanly but break at runtime: |
| 23 | + |
| 24 | +### 1. Bare-field predicates |
| 25 | + |
| 26 | +Predicates — an action's `visible`/`disabled`, a field's `requiredWhen`, a |
| 27 | +validation rule, a flow condition, a sharing rule — are **CEL expressions**, and |
| 28 | +they reference record fields through the `record.` scope: |
| 29 | + |
| 30 | +```ts |
| 31 | +// ✗ Wrong — `done` is a bare reference. It type-checks (it's just a string), |
| 32 | +// but at runtime it resolves to null → the action is hidden on EVERY record. |
| 33 | +{ name: 'mark_done', visible: '!done' } |
| 34 | + |
| 35 | +// ✓ Right |
| 36 | +{ name: 'mark_done', visible: '!record.done' } |
| 37 | +``` |
| 38 | + |
| 39 | +This is the trap behind the recurring "the button never shows / the rule never |
| 40 | +fires" bugs (#2183/#2185). `os validate` parses every predicate and checks that |
| 41 | +each `record.<field>` exists on the target object, so the bare ref fails the |
| 42 | +gate with a located, did-you-mean message instead of shipping. |
| 43 | + |
| 44 | +### 2. Dangling widget bindings |
| 45 | + |
| 46 | +A dashboard widget points at a `dataset` and reads `dimensions`/`values` from it. |
| 47 | +If a name doesn't resolve, the chart renders **empty** — no error (ADR-0021). |
| 48 | +`os validate` resolves every binding against the declared datasets and fails on a |
| 49 | +dangling one. |
| 50 | + |
| 51 | +## The one gate, two entry points |
| 52 | + |
| 53 | +`os validate` and `os build` (alias `os compile`) run the **same** validator: |
| 54 | + |
| 55 | +| | `os validate` | `os build` | |
| 56 | +|---|---|---| |
| 57 | +| Protocol schema (Zod) | ✓ | ✓ | |
| 58 | +| CEL / predicate validation | ✓ | ✓ | |
| 59 | +| Widget-binding integrity | ✓ | ✓ | |
| 60 | +| Emits `dist/objectstack.json` | — | ✓ | |
| 61 | + |
| 62 | +So `os validate` is the fast inner-loop check (no artifact); `os build` is what |
| 63 | +you run when you need the deployable artifact. A config that passes `os validate` |
| 64 | +will not fail `os build` on schema/predicate/binding grounds. |
| 65 | + |
| 66 | +<Callout type="info"> |
| 67 | +`os lint` is a **separate** pass — style and convention checks (snake_case |
| 68 | +naming, required labels, namespace prefixes, data-model patterns). Run it too, |
| 69 | +but it does not replace `os validate`, and `os validate` does not replace it. |
| 70 | +</Callout> |
| 71 | + |
| 72 | +## The workflow |
| 73 | + |
| 74 | +```bash |
| 75 | +# after editing any *.object.ts / *.view.ts / *.action.ts / *.flow.ts / *.dashboard.ts |
| 76 | +npm run validate # os validate — schema + predicates + bindings |
| 77 | +npm run typecheck # tsc --noEmit — types against @objectstack/spec |
| 78 | +``` |
| 79 | + |
| 80 | +**Rule of thumb: never report a metadata change as done until `npm run validate` |
| 81 | +passes.** In the example apps the equivalent is `pnpm --filter <pkg> validate` |
| 82 | +(`pnpm verify` in `app-showcase`, which chains validate + typecheck + test). |
| 83 | + |
| 84 | +### Checking a single expression |
| 85 | + |
| 86 | +To validate one CEL expression *before* you write it into a file — for example |
| 87 | +inside an AI build loop — call the `validate_expression` agent tool, which runs |
| 88 | +the same predicate validator inline. See the |
| 89 | +[objectstack-formula skill](/docs/guides/formula). |
| 90 | + |
| 91 | +## In CI |
| 92 | + |
| 93 | +Both commands support `--json` and exit non-zero on failure: |
| 94 | + |
| 95 | +```yaml |
| 96 | +- name: Validate ObjectStack metadata |
| 97 | + run: npx objectstack validate --strict --json |
| 98 | +``` |
| 99 | +
|
| 100 | +## See also |
| 101 | +
|
| 102 | +- [Command Line Interface](/docs/getting-started/cli) — full `os validate` / `os build` reference |
| 103 | +- [Formula & Predicates](/docs/guides/formula) — the CEL surface and `record.` scope |
| 104 | +- [Data Modeling](/docs/guides/data-modeling) — objects, fields, validations, sharing |
0 commit comments