Skip to content

content(blog): validation rule, hook, or flow? where a business rule lives in an AI-written app (slot 2026-10-21) - #279

Open
objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1-issue-269
Open

objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1-issue-269

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #269

Re-slotted to Wed 2026-10-21 (head 80331fb). Daily cadence switched on 2026-10-17 (ruling B, #267), and the queued posts were re-dated onto consecutive days from 10-18. The re-date changes the post's date and the back-linked updated stamps, with zh-Hant regenerated. Gates are green on the new head. Slot dates further down this body are superseded by this line. Merge on 2026-10-21 only.

Slot: Fri 2026-10-23. Merge on the slot day only. The slot is on the shared calendar on #267 (ruling B, daily cadence). Seat review. The seat read the post and looked at its screenshots: en 1440 light, zh-Hans 360 dark, and the zh three-answers diagram on dark. Every transcript was produced on the published @objectstack/cli 17.7.0. The two places where 17.7.0 differs from the card's wording are stated in the post rather than smoothed over: the 400 envelope names _record rather than the rule, and the aliased hook typo draws no warning. ACCEPT.

What this adds

A new post for the Fri 2026-10-23 slot, content/blog/validation-rule-hook-or-flow/, in all eight locales (zh-Hant generated by pnpm gen:zh-hant), with an English-text cover and two diagrams plus the zh pair:

  • cover-en.svg / cover.svg — the three homes and the title number (5 validations · 1 hook · 1 flow = 7).
  • three-homes-en.svg / three-homes.svg — the decision as two questions, with what each home cannot do (no delete event; a hook cannot pause).
  • same-typo-three-answers-en.svg / same-typo-three-answers.svg — the measured result of one misspelled field in the flow node (error, exit 1), the literal hook body (warning, exit 0) and the hook as shipped (silence), with the INVALID_FIELD 400 backstop.

One back-link: how-to-build-a-crm-with-an-ai-agent/index.mdx (en only — that post has no other locales) gains a sentence after its requirement-3 paragraph pointing at the new post, and updated: 2026-10-23 to match the slot. If the slot moves, move both date and that updated together.

Minted term: reviewable as data vs reviewable as code. One number: seven rules on examples/app-crm at @objectstack/cli@17.7.0 (3 validations on crm_opportunity, 2 on crm_lead, 1 hook, 1 flow), re-counted at the tag.

Runs on what readers install

Every transcript, count and os validate run was produced on the published @objectstack/cli 17.7.0 (npm view @objectstack/cli@17.7.0 version → 17.7.0), against examples/app-crm exported from the git tag @objectstack/cli@17.7.0 (commit 4e4e8814) with its workspace:* pins rewritten to 17.7.0 published packages (runtime, spec, service-i18n, plugin-hono-server, connector-rest/openapi/mcp, dev: cli, driver-sql, formula, objectql). The post names the version in its first paragraph and in the closing section. No unreleased behaviour is described: the three doc passages quoted are byte-identical between the tag and origin/main (checked with git diff @objectstack/cli@17.7.0 origin/main -- content/docs/automation/hooks.mdx content/docs/automation/hook-bodies.mdx content/docs/data-modeling/validation.mdx; the only hunks are outside the quoted lines).

Sources, with the lines quoted

Line numbers are given at the tag first; the card cited origin/main, whose numbers follow where they differ.

Claim in the post Source Quoted line
"a flow is the default surface for application logic; a hook is the backstop" objectstack content/docs/automation/hooks.mdx:20-22 (tag = main) "The working rule: a flow is the default surface for application logic; a hook is the backstop for what a node graph cannot express."
The six-row table (side effects → flow; pauses → flow; schedule → flow; mutate pending record → before hook; cross-object invariant → hook; read-side → hook) hooks.mdx:24-31 (tag = main) "Mutating the pending record in the same write, before it is saved | Before hook" … "Anything that pauses: approvals, screens, timers, signals | Flow — a hook runs inline and cannot pause" … "Read-side interception (beforeFind / afterFind) | Hook — flows have no read events"
"A flow's writes are structured metadata; a hook body is code" / warning vs gating error hooks.mdx:35-44 (tag = main) "a hook body gets a warning, an update_record node a gating error (flow-node-write-unknown-field) — its fields is a literal map next to a literal objectName, so nothing could have mis-read it."
"A flow reviews as data … a hook body reviews as code only" hooks.mdx:52-54 (tag = main) "A flow reviews as data. The node graph diffs field-by-field and renders in the Console designer with per-node run history; a hook body reviews as code only."
Code that still need not be a hook hooks.mdx:56-59 (tag = main) "a flow script node calling a function registered via defineStack({ functions }) keeps the orchestration in checked metadata and the code in one named, testable unit."
Validation rule definition; runs on insert/update content/docs/data-modeling/validation.mdx:7 (tag = main) "They run automatically on the write path (insert/update) … A validation rule is a deterministic, synchronous, side-effect-free predicate over a single record — it must be decidable from the incoming write (and, on update, the prior record) with no I/O."
Async → form layer; custom handler → before hook; delete guard → beforeDelete; no delete event validation.mdx:13-15 (tag = main) "Delete-time guards → a beforeDelete lifecycle hook. Validation rules run only on insert/update (a delete carries no record payload to validate), so there is no 'delete' validation event"
Unevaluable predicate rejects with VALIDATION_FAILED validation.mdx:112 at tag (:132-134 at main) "rejects the write with VALIDATION_FAILED, naming the rule"
transitions govern updates, initialStates the create validation.mdx:181-211 at tag (:202-232 at main) "transitions governs updates; initialStates governs the create."
cross_field = script with a different error anchor validation.mdx:232 at tag (:253 at main) "cross_field evaluates identically to script — both run the same CEL predicate. The only difference is fields[0], which labels the field the error attaches to"
Write-set lint is literal-only and advisory content/docs/automation/hook-bodies.mdx:180 at tag (:184 at main) "An unknown field raises hook-body-write-unknown-field — a warning carrying a did-you-mean suggestion, which never blocks a build."
"A missing warning is not a clean bill of health" / aliased input hook-bodies.mdx:193,200 at tag (:197,204 at main) "A missing warning is not a clean bill of health. The rule bails silently on everything it cannot resolve statically" … "aliased input (const doc = ctx.input; doc.x = 1) — v1 does no data-flow analysis"
Runtime backstop INVALID_FIELD / 400, same on every driver hook-bodies.mdx:207-217 at tag (:211-221 at main) "INVALID_FIELD / 400 / Unknown field 'stagee' on object 'deal'" … "The runtime refusal is now the backstop; the advisory warning is still the earliest signal you get, and the one that names the mistake where it was made."
Treat the warning as a build failure; prefer a flow node for a fixed write set hook-bodies.mdx:221-223 at tag (:225-227 at main) "Prefer a flow update_record node when the write set is fixed — and for this check most of all."
The three crm_opportunity validations (quoted code) examples/app-crm/src/objects/opportunity.object.ts:98-137 at tag name: 'discount_cap' … condition: P\record.discount_percent != null && record.discount_percent > 40`…type: 'state_machine'…closed_won: []`
The two crm_lead validations examples/app-crm/src/objects/lead.object.ts:94-119 at tag name: 'lead_status_transitions' … name: 'lead_score_range'
The hook (quoted code; aliased ctx.input) examples/app-crm/src/hooks/opportunity.hook.ts:9-19 at tag const input = ctx.input as { stage?: string; probability?: number }; if (input.stage === 'closed_won') input.probability = 100;
The flow's update_record node (quoted code) examples/app-crm/src/flows/convert-lead.flow.ts:139-152 at tag (:147-164 at main uses CEL value envelopes instead of '{…}' templates; the tag's form is what 17.7.0 ships, so the tag's form is quoted) fields: { status: 'converted', account: '{account_id}', converted_opportunity: '{opportunity_id}' }
Dev admin sign-in 17.7.0 boot banner (proof/server.log: "Dev admin: admin@objectos.ai / admin123 · seeded on empty DB") and objectstack root README.md:150-156 at origin/main (curl -c cookies.txt -X POST http://localhost:3000/api/v1/auth/sign-in/email …). The tag's own README has no sign-in snippet.
"49 author-time rules" is not stated in the post; the validate output "Running author-time rules (49)" is in proof/validate-baseline.log — —
Positioning sentence in the CTA src/lib/positioning.ts T2, verbatim per locale "One executable business ontology. AI writes it, the runtime runs it, agents operate it, you own it."

Proof artifact (measured, 17.7.0)

os validate on the untouched example: ✓ Validation passed, 9 field-no-consumers warnings, exit 0.

Mutation os validate 17.7.0 Exit
Flow update_record field converted_opportunity → converted_oportunity ✗ Author-time rules failed (1 issue) · rule: flow-node-write-unknown-field at flows[0].nodes[6].config.fields.converted_oportunity · "Did you mean 'converted_opportunity'?" 1
Hook body, literal form ctx.input.probabilty = 100 ✓ Validation passed + ⚠ hook "opportunity_stage_probability" › body: body writes 'probabilty' to its input, but object 'crm_opportunity' declares no such field … (judged on the metadata body lowered from the inline handler) · rule: hook-body-write-unknown-field 0
Hook body as shipped (alias const input = ctx.input; … input.probabilty = 100) output identical to the clean run; no warning 0
Control: literal form, correct spelling no hook warning 0

Each mutation was restored from a pristine copy and verified byte-identical with cmp before the next run (proof/validate-*.log, proof/validate-*.json).

REST transcript (objectstack dev --compile --port 49101, signed in as the dev admin; proof/rest-transcript.log):

  1. POST /api/v1/data/crm_opportunity with discount_percent: 45 → 400 {"code":"VALIDATION_FAILED","error":"Discount cannot exceed 40% without an approved exception.","fields":[{"field":"_record","code":"rule_violation",…}]}; the same at 40 → 201, probability: 50.
  2. POST with close_date: 2026-01-15 → 400 VALIDATION_FAILED, fields[0].field = "close_date" (the cross_field anchor).
  3. PATCH …/1YgCIjOainwOGgaR {"stage":"closed_won"} on the seeded proposal deal (probability 70) → 200, probability: 100; GET reads back 100.
  4. PATCH …/Ifv5H8Vukee1ecja {"stage":"closed_won"} on the seeded prospecting deal → 400 VALIDATION_FAILED, fields[0] = {"field":"stage","code":"invalid_transition","constraint":{"from":"prospecting","to":"closed_won"}}; → qualification → 200.
  5. PATCH … {"probabilty":100} → 400 {"code":"INVALID_FIELD","error":"Unknown field 'probabilty' on object 'crm_opportunity'"}.
  6. DELETE … → 200 {"success":true}.

Gates (all on the committed tree, 55eae4b)

Gate Result
pnpm content:lint ✓ 355 files, 46 glossary terms, exit 0. Warnings only: future date/updated (the slot), title width (the card's title), and the description-width notice (search snippets show about 160 columns) on every locale; all descriptions are within the 230-char house limit (en 224, zh 89, ja 116, ko 120, de 220, es 219, fr 222).
pnpm content:lint --published ✓ (inside pnpm build)
pnpm svg:lint ✓ 6 SVGs, 192 text boxes, 0 crossings (124 SVGs / 2665 boxes repo-wide inside pnpm build)
pnpm check (via os-verify-lock.sh) ✓ 160 files, 0 errors, 0 warnings — VERDICT command-exit 0
pnpm build (via os-verify-lock.sh) ✓ [build] Complete!; content lint --dist passed (494 built blog pages; quotation marks 0 wrong; 99,466 internal links resolve) — VERDICT command-exit 0; git status clean afterwards
pnpm seo:smoke ✓ 910 pages, 594 sitemap URLs, 5,679 canonical/hreflang annotations

Browser check (looked at)

astro preview of the built dist/ on 127.0.0.1:49110, Chromium via Playwright: 231/231 assertions pass. en and zh-Hans article at 1366×625, 1440×750, 1512×830, 1920×945, 390×844, 360×800 in light and dark (lazy images scrolled into view first): HTTP 200, no horizontal overflow outside scroll containers, cover renders, every image loads, no repeated image. Blog list en and zh-Hans at 1440 and 390, light and dark: the new post is listed. The back-linked CRM post at 1440 and 390 shows "Updated Oct 23, 2026" and the new link. Each of the six SVGs rendered at native size in light and dark: lowest text contrast 4.59:1 (the first run measured 4.3:1 on muted #657872 text over the amber card; muted text was darkened to #5e716b and the build and check were re-run). Screenshots: scratchpad/www/shots-269/ (103 files, results.json).

Deviations from the card, for the reviewer

  • Proof 1 wording. The card expected the 400 to "name discount_cap". On 17.7.0 the envelope carries the rule's authored message and field: "_record"; the rule name is not in the response. The post says exactly that.
  • Proof 4 has three answers, not two. The hook-body warning fires only on the literal ctx.input.x = … shape. The CRM's hook, as shipped, writes through a local alias of ctx.input, which the docs list as a shape the rule bails on silently; the typo planted in the hook as it exists in the repository produces no warning. The post reports all three outcomes with the real output; this is the docs' own documented blind spot (hook-bodies.mdx:200 at tag), not a 17.7.0 divergence, so it was written up rather than escalated.
  • README:150-156 is the objectstack root README at origin/main; the 17.7.0 tag's README has no sign-in snippet. The 17.7.0 boot banner prints the same credentials, and that is what the post relies on.
  • Sign-in must be called on localhost, not 127.0.0.1 (403 INVALID_ORIGIN otherwise); the post uses the banner's wording and does not mention the origin rule.
  • Only one back-link was added (the CRM post). how-to-review-ai-generated-app-changes already carries updated: 2026-10-15, and automation-trigger-model would need zh-Hans edits and a regeneration; both are linked from the new post. Add either back-link on request.
  • The flow at the tag spells its node values as '{account_id}' templates; origin/main has since moved to CEL value envelopes. The tag's form is what 17.7.0 installs, so it is the form quoted.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr

New post for the Fri 2026-10-23 slot, in all eight locales, with an
English-text cover and two diagrams plus the zh pair. Every transcript,
count and `os validate` run was produced on the published
@objectstack/cli 17.7.0 against examples/app-crm at that tag: seven
rules (five validations, one hook, one flow), one typo planted in the
flow node (gating error, exit 1), the hook body (warning, exit 0) and
the aliased hook as shipped (silence), and the INVALID_FIELD backstop.

The CRM build post gets a back-link from its requirement-3 paragraph
and `updated: 2026-10-23` to match the slot.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…dnesday 2026-10-21 (daily cadence, #267)

Ruling B on #267 switches on now that seven approved posts are queued.
This post (#269) is the seventh, and the seat starts the daily run on
2026-10-18. The post is unpublished, so its date moves from
Fri 2026-10-23 to Wed 2026-10-21 in every authored locale (en, de, es,
fr, ja, ko, zh-Hans). The updated stamp it writes on the back-linked
published post how-to-build-a-crm-with-an-ai-agent (en) moves with it
and stays in its bare-date form. zh-Hant is regenerated with
pnpm gen:zh-hant. No published post's date changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
@objectstack-fleet objectstack-fleet Bot changed the title content(blog): validation rule, hook, or flow? where a business rule lives in an AI-written app (slot 2026-10-23) content(blog): validation rule, hook, or flow? where a business rule lives in an AI-written app (slot 2026-10-21) Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Post (slot Wed 2026-10-21): Validation Rule, Hook, or Flow? Where a Business Rule Should Live in an AI-Written App — One CRM, Seven Rules, Three Homes

2 participants