Repository navigation
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
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #269
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/cli17.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_recordrather 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 bypnpm 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 theINVALID_FIELD 400backstop.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, andupdated: 2026-10-23to match the slot. If the slot moves, move bothdateand thatupdatedtogether.Minted term: reviewable as data vs reviewable as code. One number: seven rules on
examples/app-crmat@objectstack/cli@17.7.0(3 validations oncrm_opportunity, 2 oncrm_lead, 1 hook, 1 flow), re-counted at the tag.Runs on what readers install
Every transcript, count and
os validaterun was produced on the published@objectstack/cli17.7.0 (npm view @objectstack/cli@17.7.0 version→17.7.0), againstexamples/app-crmexported from the git tag@objectstack/cli@17.7.0(commit4e4e8814) with itsworkspace:*pins rewritten to17.7.0published 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 andorigin/main(checked withgit 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.content/docs/automation/hooks.mdx:20-22(tag = main)hooks.mdx:24-31(tag = main)beforeFind/afterFind) | Hook — flows have no read events"hooks.mdx:35-44(tag = main)update_recordnode a gating error (flow-node-write-unknown-field) — itsfieldsis a literal map next to a literalobjectName, so nothing could have mis-read it."hooks.mdx:52-54(tag = main)hooks.mdx:56-59(tag = main)scriptnode calling a function registered viadefineStack({ functions })keeps the orchestration in checked metadata and the code in one named, testable unit."content/docs/data-modeling/validation.mdx:7(tag = main)beforeDelete; no delete eventvalidation.mdx:13-15(tag = main)beforeDeletelifecycle hook. Validation rules run only on insert/update (a delete carries no record payload to validate), so there is no'delete'validation event"VALIDATION_FAILEDvalidation.mdx:112at tag (:132-134at main)VALIDATION_FAILED, naming the rule"transitionsgovern updates,initialStatesthe createvalidation.mdx:181-211at tag (:202-232at main)transitionsgoverns updates;initialStatesgoverns the create."cross_field=scriptwith a different error anchorvalidation.mdx:232at tag (:253at main)cross_fieldevaluates identically toscript— both run the same CEL predicate. The only difference isfields[0], which labels the field the error attaches to"content/docs/automation/hook-bodies.mdx:180at tag (:184at main)hook-body-write-unknown-field— a warning carrying a did-you-mean suggestion, which never blocks a build."hook-bodies.mdx:193,200at tag (:197,204at main)const doc = ctx.input; doc.x = 1) — v1 does no data-flow analysis"INVALID_FIELD / 400, same on every driverhook-bodies.mdx:207-217at tag (:211-221at main)hook-bodies.mdx:221-223at tag (:225-227at main)update_recordnode when the write set is fixed — and for this check most of all."crm_opportunityvalidations (quoted code)examples/app-crm/src/objects/opportunity.object.ts:98-137at tagname: 'discount_cap'…condition: P\record.discount_percent != null && record.discount_percent > 40`…type: 'state_machine'…closed_won: []`crm_leadvalidationsexamples/app-crm/src/objects/lead.object.ts:94-119at tagname: 'lead_status_transitions'…name: 'lead_score_range'ctx.input)examples/app-crm/src/hooks/opportunity.hook.ts:9-19at tagconst input = ctx.input as { stage?: string; probability?: number }; if (input.stage === 'closed_won') input.probability = 100;update_recordnode (quoted code)examples/app-crm/src/flows/convert-lead.flow.ts:139-152at tag (:147-164at 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}' }proof/server.log: "Dev admin: admin@objectos.ai / admin123 · seeded on empty DB") and objectstack rootREADME.md:150-156atorigin/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.proof/validate-baseline.logsrc/lib/positioning.tsT2, verbatim per localeProof artifact (measured, 17.7.0)
os validateon the untouched example:✓ Validation passed, 9field-no-consumerswarnings, exit 0.os validate17.7.0update_recordfieldconverted_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'?"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-fieldconst input = ctx.input; … input.probabilty = 100)Each mutation was restored from a pristine copy and verified byte-identical with
cmpbefore 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):POST /api/v1/data/crm_opportunitywithdiscount_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.POSTwithclose_date: 2026-01-15→400 VALIDATION_FAILED,fields[0].field = "close_date"(thecross_fieldanchor).PATCH …/1YgCIjOainwOGgaR {"stage":"closed_won"}on the seededproposaldeal (probability 70) →200,probability: 100;GETreads back100.PATCH …/Ifv5H8Vukee1ecja {"stage":"closed_won"}on the seededprospectingdeal →400 VALIDATION_FAILED,fields[0] = {"field":"stage","code":"invalid_transition","constraint":{"from":"prospecting","to":"closed_won"}};→ qualification→200.PATCH … {"probabilty":100}→400 {"code":"INVALID_FIELD","error":"Unknown field 'probabilty' on object 'crm_opportunity'"}.DELETE …→200 {"success":true}.Gates (all on the committed tree,
55eae4b)pnpm content:lintdate/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 --publishedpnpm build)pnpm svg:lintpnpm build)pnpm check(viaos-verify-lock.sh)VERDICT command-exit 0pnpm build(viaos-verify-lock.sh)[build] Complete!;content lint --distpassed (494 built blog pages; quotation marks 0 wrong; 99,466 internal links resolve) —VERDICT command-exit 0;git statusclean afterwardspnpm seo:smokeBrowser check (looked at)
astro previewof the builtdist/on127.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#657872text over the amber card; muted text was darkened to#5e716band the build and check were re-run). Screenshots:scratchpad/www/shots-269/(103 files,results.json).Deviations from the card, for the reviewer
discount_cap". On 17.7.0 the envelope carries the rule's authored message andfield: "_record"; the rule name is not in the response. The post says exactly that.ctx.input.x = …shape. The CRM's hook, as shipped, writes through a local alias ofctx.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:200at tag), not a 17.7.0 divergence, so it was written up rather than escalated.README:150-156is the objectstack root README atorigin/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.localhost, not127.0.0.1(403 INVALID_ORIGINotherwise); the post uses the banner's wording and does not mention the origin rule.how-to-review-ai-generated-app-changesalready carriesupdated: 2026-10-15, andautomation-trigger-modelwould need zh-Hans edits and a regeneration; both are linked from the new post. Add either back-link on request.'{account_id}'templates;origin/mainhas 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