Uh oh!
There was an error while loading. Please reload this page.
fix(runtime): declared type: 'flow' endpoints answer the #9378 flow-dispatch status table, from the one shared definition - #9676
Conversation
…-dispatch status table (#9462) Door 3 of the #9446 ruling. `executeFlow` ended with an unconditional `successAnswer(await automation.execute(...))`, so every refusal left the seam as `200 {success:true,data:{success:false,...}}` — the double envelope #3962 removed from /actions, on the surface an app publishes as its own API. It now reads `flow-dispatch-status.ts`, the one shared definition the trigger door and /actions already read: 404 / 409 FLOW_DISABLED / 422 FLOW_NO_START_NODE / 400 FLOW_FAILED. Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not seeCoarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e03df8ab1fb5e7faafbfb5839a773552bdf83ea2 && git checkout e03df8ab1fb5e7faafbfb5839a773552bdf83ea2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c07d6e8b9382d6e6b08603e4335d5a60747081de 45c2720d286222c30b42eeb9327e72c9dd7054f2 && git checkout -B drift-repro c07d6e8b9382d6e6b08603e4335d5a60747081de && git merge --no-ff 45c2720d286222c30b42eeb9327e72c9dd7054f2
node scripts/docs-audit/affected-docs.mjs --json c07d6e8b9382d6e6b08603e4335d5a60747081de
|
os-zhuang
commented
Aug 18, 2026
PM review — accepted, ready + auto-merge armed. ✅ A confirmed (it is the status quo). ⛔ B and C are not mine to rule.
The card's central claim, verified structurally rather than from the reportThe whole point of this card is call the shared definition, do not write a third copy. I re-derived it:
⭐ That last row is the one worth checking rather than assuming: two status-code literals surviving in the file is exactly what a third private copy would look like. They are documentation pointing at the shared source, and no branching code duplicates the table. 2 → 3 doors on one definition. The staged convergence is therefore observable in the repo, not merely asserted in prose — which is the evidence I relied on for a BREAKING change, over and above the cited ruling. Landing probe registered: ✅ Q — A confirmed, and the reason is narrowKeeping the unclassified ⛔ B is not. Refusing that residual ⛔ C is not either. "Make the residual a fifth row, or have the producer classify it" is a change to a shared contract governing all three doors. That is a design decision for the maintainer, not a defect. ⭐ The dev's judgement to take A now and not file C as a backlog issue was correct: "it is a decision, not a defect." Filing it as a card would have put an unruled design question into a queue that dispatches things. It belongs in the maintainer's decision channel, and I am raising it there rather than closing over it. What I am NOT treating as settledThe residual leaves a real asymmetry on a public surface: for that one outcome the double envelope survives, and Also verified
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9462
Door 3 of 3.
packages/runtime/src/endpoint-executor.ts's flow branch ended with oneunconditional line:
so a declared
type: 'flow'endpoint answered200for every outcome, with the rawengine result in
data. A flow that was disabled, had no start node, could not be found,or ran and was rejected all reached the caller as
{"success":true,"data":{"success":false, ...}}— the double envelope #3962 removed from/actions, still standing on the surface an app publishes as its own public API. A clientbranching on the HTTP status read every one of those failures as a success.
It now answers the #9378 table, by calling the one shared definition#9446 created
(
packages/runtime/src/flow-dispatch-status.ts) rather than by writing a third privatecopy of the rule:
404409FLOW_DISABLED422FLOW_NO_START_NODE400FLOW_FAILEDMaintainer ruling, 2026-08-18, verbatim 「同意」: the table is a property of the
flow-dispatch CONTRACT rather than of the trigger route, converged in stages. This is the
last stage; all three doors now read one definition.
envelope. There is no inner
data.successleft to read; the same fact is inerror.code.400carries the run's own artefacts inerror.details(errorMessage,summary), byte-identical toPOST /api/v1/automation/:name/trigger. The threenever-dispatched rows carry neither — no run happened to describe them.
200with the result indata.outputMappingis no longer applied to a failure body. No change was needed forthis:
api-endpoint-step.tsalready keys the projection onanswer.status < 400, sothe refusal rows fall outside it by the rule that was already written. It closes a real
hole — the projection used to be applied to the
200-wrapped FAILURE body and couldpresent a refused dispatch as data.
cacheTtl'sCache-Controlrides the samestatus < 400test, so it no longer rides aflow failure either. The
rateLimit/authRequiredchain is untouched: it runsupstream of execution, so a refusal here is only ever reached by a request that already
passed it.
Consumer enumeration — the real greps, and what they found
The card requires consumers be enumerated before flipping. Every query below is reported
with a control query proving the search works, because a zero-match grep exits 0 and
reads exactly like a pass.
objectstackapi/v1/apps/(the declared-endpoint mount, ADR-0121 D1) across*.ts,*.tsx,*.mdx,*.json,*.yml,*.mjsappEndpointMountPrefixresolves in 4 filesobjectstack.data.success/data?.success/actions, package-install, and automation-API schema tests)/actionssites it shouldobjectuiapi/v1/appsapps/(the UI router prefix) 1109 ·v1/automation33cloudapi/v1/apps/api/v1/1102packages/client(SDK)api/v1/apps/APP_ENDPOINTv1/actions4There is no objectui half to coordinate. objectui has zero references to the
declared-endpoint mount; all 1109 of its
apps/hits are the console's own router paths(
/apps/{app_id}/{object}), andinterpretFlowResponse— its double-envelope reader — isdriven only by
FlowRunner,RecordDetailViewanduseConsoleActionRuntime, i.e. thetrigger route and
/actions, both of which already answer the table.Falsified: the SDK is not a consumer. The dispatch's starting measurement named
packages/client/src/index.tsplus two test files as matching the double-envelope shape.They do — but for
POST /api/v1/actions/...(normalizeActionResult) and for the generic{success, data, meta}unwrap. The SDK exposes no method that calls a declaredendpoint at all, so nothing there changes.
The only in-repo consumers of a declared flow endpoint's response are:
packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts— asserts thesuccess path (
200,body.success === true). Unchanged by this PR.docs/qa/platform-checklist/areas/api-backend.json— its clauses cover theobject_operationfeed,authRequired, the 501 and the transport 404. It pins nothingabout the flow failure envelope, so it needs no edit.
Docs
content/docs/protocol/kernel/http-protocol.mdx— the boundary wording spec: addFLOW_DISABLED/FLOW_NO_START_NODEtoAutomationResult.codeand deliver the trigger ruling's remaining two rows (409/422) #9415 correctedto describe the divergence flips back to "the same response contract", as the card's
own constraints require.
content/docs/automation/flows.mdx— a second page carrying the same sentence,found by the enumeration above and not named in the card. It read "Declared endpoints
(
type: 'flow') are the remaining exception: they still answer200for every outcome,so read
data.successthere rather than the status." Leaving it would ship a documentedfalsehood created by this very diff, so it is corrected here, in the same defect class
and with the correct form already pinned by the card's constraint. Flagged rather than
folded in silently; the claim's file surface was amended on the issue in the same round.
Reverse verification — predicted, then measured
The ablation restores the unconditional
successAnswer(dropping both theflowIsUnknownprobe and the
classifyFlowRefusalmapping). Predictions were written to a file beforethe run; direction predicted: RED.
Predicted 11 fail / 4 pass. Actual 11 fail / 4 pass — and the same four survivors.
The four that stay green are deliberate CONTROLS, not cases that fail to pin the defect:
a successful run is UNCHANGED,an UNCLASSIFIED refusal keeps today's 200, and their twocross-door twins. Each pins behaviour this change must NOT alter, so green under the
ablation is their correct result.
endpoint-executor.test.ts(49 cases, the pre-changesuite) also stays fully green under the ablation — the control proving the ablation touched
only what this card changes.
Excerpt, ablated tree:
The fix was restored from the branch (
git checkout {branch} -- {path}), verifiedbyte-identical to the commit by an empty
git diff HEAD.Verification — all at
45c2720d2, the final commitpnpm build@objectstack/runtimetest@objectstack/runtimetypecheck (tsc --noEmit)showcase-declarative-endpoints+declarative-endpoint-policy)check:type-check-coverage·check:type-check-debt·check:nul-bytes·check:route-envelope·check:error-status-conformance·check:error-code-casing·check:dispatcher-error-vocabularycheck:cross-package-test-inputs·check:engine-double-contract·check:where-matcher·check:query-options-erasure·check:docs-audit-scope·check:docs-redirects·check:role-word· affected-docs drift ·check-adr-0087-registration·check-changeset-no-major·check-empty-changeset--filter '...@objectstack/runtime'alone andreported four red suites. Every one was
Failed to resolve entry for package @objectstack/service-settings/-automation/-storage/-realtime— packagesoutside that filter closure and therefore unbuilt. That is NOT MEASURED, not red; the
table above is the re-run on a fully built tree.
Not addressed here
The unclassified-refusal residual — an
AutomationResultwithsuccess: falseand nocodeand nostatus— keeps today's200at this door, and is pinned as such. #5040 §4(this module's opening rule) makes a declared
flowendpoint a stable URL plus a policylayer over
POST /automation/:name/trigger: same context builder, sameexecutecall, sothe same answer.
/actionsrefuses that residual under its own #3962 ruling about itsroute; adopting that reading here would promote an exit the producer never classified —
the one thing the shared table's note says a door must not do — and would do it by
borrowing a ruling about a different door. If the residual should speak HTTP everywhere,
that is one change to the shared table for all three doors, not a fourth reading invented
at this one. Raised in the report for the maintainer.
Generated by Claude Code
Generated by Claude Code