feat(spec,service-automation): a flow run's result carries the flow's authored label as flowLabel - #20633
Conversation
…d label Every result of an evaluation of a registered flow (paused, terminal success including the skip exits, failed, stranded, refused, and a resumed parent whose delegated child failed) now carries `flowLabel`, the flow definition's `label` copied verbatim the way `successMessage` / `errorMessage` are. Refusals carrying a `code` and unknown flows carry none. A subflow chain answers with the addressed (parent) run's label. The trigger response schema mirrors the new member, as its compile-time parity guard with `AutomationResult` requires. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Present on every evaluation's result (paused, terminal success, skip, failed, retry exhausted, retry-attempt success and pause, stranded, refused), absent on every code-bearing refusal and on an unknown flow; a subflow chain answers with the parent's label; an empty label is served verbatim, never replaced by the API name. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
The paused launch, a resume that completes and a terminal launch each carry data.flowLabel; the resume door's 400 FLOW_FAILED details keep their fixed set (no flowLabel), fenced so a widening is deliberate. translateFlow's docblock now says where the authored label the client falls back to comes from, and that the translation key is still unread. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 137 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 a0c35106ccb2fdf34a4fbcf701612fb928ae6692 && git checkout a0c35106ccb2fdf34a4fbcf701612fb928ae6692
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a918fe7fd0150578db13e6ae8d5cff24582e213e 01cd34c7e4e3f416c44b5a1139f4e44ec4375ba1 && git checkout -B drift-repro a918fe7fd0150578db13e6ae8d5cff24582e213e && git merge --no-ff 01cd34c7e4e3f416c44b5a1139f4e44ec4375ba1
node scripts/docs-audit/affected-docs.mjs --json a918fe7fd0150578db13e6ae8d5cff24582e213e
|
Contract reviewServed-tier: Inputs: card #20318 (body; comments ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #20318 — stage 1 of 2 (the served authored label). Stage 2, the objectui reader of
flows.FLOW.labelin the runner header and completion toast, stays open on the card; this PR does not flip the ledger row.Clause-②: yes (widening:
AutomationResultgainsflowLabel)What changes
AutomationResult(@objectstack/spec/contracts) gains one optional member,flowLabel?: string: the flow definition's authoredlabel, copied verbatim byservice-automationat the same exits that copysuccessMessage/errorMessage.TriggerFlowResponseSchema.data(@objectstack/spec/api) mirrors it, which its compile-time parity guard withAutomationResultrequires. The generated automation API reference is regenerated.translateFlow's docblock now says where the fallback label comes from and that the translation key is still unread.This is option (b) of seat 4's fork, taken by the claim from standing text: the result already carries flow-level strings copied from the definition for a runner's benefit.
The served shape (for the stage-2 sub-issue)
flowLabel, typestring, optional, onAutomationResultand onTriggerFlowResponse.data.status: 'paused'(first attempt, a retry attempt, a resume that pauses again, a subflow chain that pauses again);status, including the two skip exits (condition_not_met,reentrancy_loop_guard);'failed'(the trigger exit and the exhausted-retry exit),'stranded','refused';status,success: false).code(FLOW_DISABLED,FLOW_NO_START_NODE,FLOW_INPUT_SCHEMA_INVALIDand all six resume refusals, a nested child's screen refusal included), and when the flow is not registered.ScreenFlowState.flowNameis the launched flow, and the delegated pause answers with the parent'srunId.FlowSchemarequireslabel, so the member is always present on the set above and is always what the author wrote, an empty string included. It is never replaced by the API name.200, sodata.flowLabelarrives onPOST /api/v1/automation/NAME/trigger(paused, finished, refused or skipped launch) and onPOST /api/v1/automation/NAME/runs/RUN_ID/resume(a further pause, the completion, a refusal). A400 FLOW_FAILEDanswer does not carry it: the trigger door andclassifyResumeResultbuilderror.detailsfrom their own key set (errorMessage,summary, and on resume the stranded verdict), inpackages/runtime, and this PR leaves that set alone. See the open question below.Premise, verified before the first edit (on
origin/mainc1d8051)successMessage/errorMessagecopy site is inengine.tsand reads the local parsedflow, whoselabelis a required string. The one exception,retryExecution's exhausted exit, receivesflow.errorMessageas a parameter fromexecute(), which holdsflow; the label is now passed beside it. The hits insubflow-node.ts/map-node.tsare comments; node executors return aNodeExecutionResult, never anAutomationResult.respondToFlowTriggeranswers the paused arm and the terminal arm withdeps.success(result), the resume door answers everysuccess !== falseresult the same way, and the dispatcher'ssuccess()is{ success: true, data, meta }with nothing projected. The400 FLOW_FAILEDarms projectdetailsfrom their own key set (above).Verification (at
01cd34c7e4)service-automation: newflow-label-on-result.test.ts, 16 tests (every PRESENT arm, the subflow chain, verbatim'', and the ABSENT refusals). Full packagevitest run: 152 files, 1867 tests passed.typecheck: exit 0, test layer 0 debt.scripts/ablation-replace.mjs(anchor must hit, restore proved by blob == HEAD): blanking the 13 direct stamps turned 10 tests red and left 6 green (the refused, retry-exhausted and 4 absent tests), as predicted; blanking the refused chokepoint turned exactly the refused test red; blanking the retry-exhausted exit turned exactly that test red.flowLabel: 42at the refused chokepoint makesservice-automation'stsc --noEmitred with TS2322 atengine.ts:9060, so it reads the rebuilt spec.d.ts. Renaming the mirror member inautomation-api.zod.tsmakes spec's test typecheck red with 3 errors inautomation-api.zod.test.ts, so the mirror is load-bearing.spec: the touched test files (3 files, 340 tests) pass,typecheckexit 0,check:generated --fixregenerated onlycontent/docs/references/api/automation-api.mdx.@objectstack/verify(the one package depending on both the doors and the engine): newautomation-trigger-flow-label.test.tsplus the four sibling automation suites, 5 files, 17 tests passed. The existing paused-run parity test still deep-compares the two routes' bodies, now withflowLabelon both.AutomationResultoutside this package. Consumer suites run green:plugin-approvals(791),trigger-record-change(101),trigger-schedule(150).dispatch-gates --commandsderived 111;--ranreconciles 109 run and 2 NOT MEASURED.check:livenessexit 0.check:dual-build-cjs-loads, reason: its prerequisite is every package built (33 had no dist), which is CI-owned.check:type-check-debt, reason:--re-measurere-runs tsc for every debt-ledger entry repo-wide, none of which is a package this diff touches, and a 300s ceiling fired.check:type-check-coverageran green.Acceptance notes
packages/spec/src/api/automation-api.zod.ts(+ its test and the regenerated reference page) is the trigger transport's declared response schema, bound toAutomationResultbyTriggerFlowDataMatchesContract, and itsz.objectstrips undeclared keys, so a client parsing the response would drop the label.packages/verify/src/automation-trigger-flow-label.test.tsis the route pin the dispatch asked for. It lives inverifybecause nothing else depends on both packages.reentrancy_loop_guard(needs a same-record re-entrant dispatch) andexecuteWithoutRetry's failed exit (the retry loop consumes it and answers from the exhausted exit). Both are stamped, and the first ablation leg blanked them.distlevel. Its PRESENT assertions aretoBe(label), which an absent member cannot satisfy, and the engine-side legs above already show the mutation reds the equivalent pins.seedRunVariablesstill writes$flowLabelasflow.label ?? flowName. The??arm is unreachable becauselabelis required. Noted only.errorMessage. The contract sayserrorMessageis set on failure, so a runner would show the raw subflow error there instead of the parent author's text. The probe at the resume door never got the shared verify lock, so this stays a note and no card is filed. Carrier: none.translations.mdxbullet, the ledger flip and theauthorWarndrop ride the stage-2 reader, per the claim.packages/spec/liveness/translation.jsonis unchanged: no note names a missing producer, and nothing in it became false.Open question for the seat
Should the
400 FLOW_FAILEDdetails carryflowLabel? My recommendation is no. No runner surface names the flow on a failure: the failure toast shows the error text, and theFlow "NAME" failedfallback is built client-side before the request. Widening an ADR-0112 details set inpackages/runtimewith no reader is outside this claim.Generated by Claude Code