Uh oh!
There was an error while loading. Please reload this page.
feat(approvals): decisionOutputs may be declared required (objectui#2955) - #3931
Merged
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 105 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…955)
`decisionOutputs` exists so an approver's decision can route the next step —
`approvers: [{ type: 'expression', value: 'vars.lead_review.next_reviewers' }]`
— but nothing made the approver actually answer. A skipped output resumed the
run with the key absent, and the next node either faulted with
EXPRESSION_FAILED or resolved an empty slate and stalled on
`onEmptyApprovers: 'admin_rescue'`, long after the one person who could have
filled it in had moved on. `onEmptyApprovers` is a recovery mechanism, not a
contract.
A typed entry may now carry `required: true`. Unlike `type`/`multiple`, which
only shape the input widget, this one is enforced by the runtime: an approve
with no value — or a blank one ('', whitespace, [], an array of blanks) — is
rejected with VALIDATION_FAILED before any write, so neither the audit row nor
the request moves and the run cannot resume past the node with the key missing.
Reject never requires them: the run leaves down the reject edge where nothing
reads the outputs, and demanding routing data to say "no" would trap the
rejection. No elevation bypass either — a one-click email action link and an
`auto_approve` SLA escalation both fail rather than advance into a node that
would resolve nobody (the escalation sweep already isolates a throwing
request, so it stays pending and visibly overdue). Enforcement is per
decision, so on a unanimous/quorum node every approver supplies them and the
finalizing decision's values are what the flow resumes with.
`required` rides `normalizeDecisionOutputs`, so it reaches clients on
`decision_output_defs` and a decision UI can block locally instead of
round-tripping to a 400 — the console side is objectui#2955. The showcase's
dynamic-approval flow now declares it, which is the honest shape for that
demo: its co-sign node declares `onEmptyApprovers: 'fail'`.
Verified against the showcase runtime: an approve with no outputs and one with
`[]` both return 400 with the request still pending and no audit row, and a
filled approve routes co-sign to the picked user.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 freeto 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.
Closes the platform half of objectstack-ai/objectui#2955 (no separate issue here).
Why
decisionOutputsexists so an approver's decision can route the next step —approvers: [{ type: 'expression', value: 'vars.lead_review.next_reviewers' }]— but nothing made the approver actually answer. A skipped output resumed the run with the key absent, and the next node either faulted withEXPRESSION_FAILEDor resolved an empty slate and stalled ononEmptyApprovers: 'admin_rescue'— long after the one person who could have filled it in had moved on.onEmptyApproversis a recovery mechanism, not a contract.What
A typed
decisionOutputsentry may now carryrequired: true. Unliketype/multiple, which only shape the input widget, this one is enforced by the runtime.@objectstack/spec—DecisionOutputDefSchemagainsrequired;normalizeDecisionOutputscarries it through, so it reaches clients ondecision_output_defsand a decision UI can block locally instead of round-tripping to a 400.@objectstack/plugin-approvals—decide()rejects an approve with no value, or a blank one ('', whitespace,[], an array of blanks), withVALIDATION_FAILEDbefore any write: neither the audit row nor the request moves.Semantics (deliberate, documented)
rejectedge where nothing reads the outputs — demanding routing data to say "no" would trap the rejection. Outputs still ride a reject when the approver filled them in.auto_approveSLA escalation both fail rather than advance into a node that would resolve nobody. The escalation sweep already isolates a throwing request, so that decision stays pending and visibly overdue instead of silently breaking the run downstream.unanimous/quorumnode every approver supplies the required outputs, and the finalizing decision's values are what the flow resumes with.Docs: the generated reference table,
content/docs/automation/approvals.mdxandskills/objectstack-automation/SKILL.md. The showcase's dynamic-approval flow now declares the flag — the honest shape for that demo, whose co-sign node isonEmptyApprovers: 'fail'.Tests
8 new service tests + 4 spec normalizer tests. Full suites for every touched package: spec 6827, plugin-approvals 326, lint 540, rest 419 — all green.
Verified on the showcase runtime
POST …/approvewith no outputs400 VALIDATION_FAILED, request still pending, audit table holds only thesubmitrowPOST …/approvewith{ next_reviewers: [] }expressionapprover resolves to the picked userThe console side is objectstack-ai/objectui#2955 (marks the field required so the approver is stopped at the empty field rather than by this 400).
🤖 Generated with Claude Code