Gating actions on exceptions stops a reviewer approving data that is broken. The Form Completeness Gate stops a reviewer approving data they have not looked at — a half-read form where two tabs were never opened and the instruction panel was never expanded.
The gate is a per-form checklist of “you must do X before you can finish” requirements. It is fully opt-in: existing forms and actions are unchanged until you add the props described below.
The problem
A review form has three tabs — line items, charges, and a panel of reviewer instructions tucked at the bottom. A reviewer who only ever looks at the first tab can still click Approve: the document had no open exceptions, the button was enabled, and the task transitions to approved. Nobody actually read the charges, and the instruction panel that explained how to handle a multi-stop shipment route was never even expanded.
The gate closes that hole. The reviewer cannot finish until they have visited the data you flagged as mandatory.
The pattern
A single action property turns the gate on:
| Property | Type | Effect |
|---|
gatedByCompleteness | boolean | When true, the action stays disabled while the form’s completeness gate has any outstanding item. An amber info popover next to the button lists what is still outstanding. Default is off — omit it and the action behaves exactly as before. |
It lives under metadata.actions[].properties in the task template YAML, alongside the existing exception gates. The gate is evaluated entirely in the UI whenever a requirement is satisfied — there is no round trip to the server.
When the gate blocks an action, an amber info icon appears next to the button. Clicking (or keyboard-focusing) it opens a popover headed Before continuing: that lists the outstanding items as bullets. It shows up to six items; if more remain it appends a …and N more line.
gatedByCompleteness combines with the exception gates from Gating actions on exceptions. All the disable rules are OR-ed together — any one of them is enough to keep the button disabled.
What counts as outstanding
The gate collects requirements from two places: the layout components on the form, and the open data exceptions on the paths the form binds.
| Source | Raised by | Outstanding until… | Item label |
|---|
tab | a v2:tabs mustView index | the reviewer clicks into that tab | Review <tab> tab |
panel-expand | a v2:panel mustExpand | the reviewer expands the panel at least once | Expand <panel> |
panel-scroll | a v2:panel mustScroll | the reviewer scrolls the panel to its end | Scroll to the end of <panel> |
exception | an open data exception on a bound path | the exception is resolved or overridden | the exception’s message (falls back to Resolve <type>) |
Opting fields in
The mandatory requirements come from props on the layout components themselves, not from the task template. Add them where the field is rendered in the data form schema. See Layout Components for the full prop tables — the gate-specific props are:
| Component | Prop | Type | Notes |
|---|
v2:tabs | mustView | number[] | Zero-based indices of tabs the reviewer must open. The first-rendered tab counts as already viewed, so index 0 is normally pre-satisfied — list the tabs after the first. Out-of-range indices are ignored with a dev warning. |
v2:panel | mustExpand | boolean | Requires collapsible: true. The panel then starts collapsed and must be expanded at least once. If collapsible is false the prop is ignored (with a dev warning), because a non-collapsible panel is always open. |
v2:panel | mustScroll | boolean | A bottom sentinel must scroll into view at least once — useful for long instruction blocks. Can be combined with mustExpand on the same panel. |
mustExpand only has an effect on a collapsible panel. Set collapsible: true on the same v2:panel or the requirement is dropped and a warning is logged in dev.
Exception scoping
The non-exception requirements (mustView, mustExpand, mustScroll) always apply to a gatedByCompleteness action. Exception-source requirements are scoped:
- If the action also declares
onlyEnabledIfNoOpenExceptionsForPaths, only exceptions on those listed paths fold into the gate. (See Gating actions on exceptions.)
- Otherwise, every open exception on a path the form binds applies.
A form’s bound paths are the tagPath / groupTaxon values of every component it renders. Exceptions on paths the form does not render never fold into its gate, and matching is hierarchical — an exception on a child attribute is in scope for a panel bound at its parent group. An exception blocks the gate while it is open and has no closing comment; resolving it (or adding a closing comment) clears it from the gate.
Example
An invoice review form with two tabs after the first, a collapsible instruction panel the reviewer must open, and an Approve action gated on completeness:
# Data form schema — layout components opt the fields in
- component: v2:tabs
properties:
mustView: [1, 2] # "Line Items" and "Charges" — first tab is pre-viewed
children:
- component: v2:panel # index 0 — first tab, counts as viewed
properties: { title: "Header" }
- component: v2:panel # index 1
properties: { title: "Line Items" }
- component: v2:panel # index 2
properties: { title: "Charges" }
- component: v2:panel
properties:
title: "Reviewer Instructions"
collapsible: true # required for mustExpand
mustExpand: true
# Task template — the action opts into the gate
metadata:
actions:
- uuid: approve-action
type: approve
label: "Approve"
properties:
targetStatus: approved
statusSlug: approved
icon: check
color: green
keybind: "a"
gatedByCompleteness: true
Until the reviewer opens the Line Items and Charges tabs, expands Reviewer Instructions, and clears any open exception on a bound path, Approve stays disabled and the popover lists what is left: Review Line Items tab, Review Charges tab, Expand Reviewer Instructions, and one bullet per outstanding exception.
The gate is in-memory and lives for one form view. Reloading the form starts the reviewer over — every tab, panel, and scroll requirement is outstanding again. Keep the list short enough that a re-read is not punishing.
Recipe summary
- On each layout component, opt the mandatory fields in:
mustView on a v2:tabs, mustExpand (with collapsible: true) and/or mustScroll on a v2:panel.
- On the action in the task template, set
gatedByCompleteness: true.
- To restrict which exceptions count, add
onlyEnabledIfNoOpenExceptionsForPaths to the same action.
- Keep a reject or escape action ungated so a reviewer is never trapped.
- Test by leaving one flagged tab unopened — the action must stay disabled and the popover must name it.
The completeness gate makes sure a reviewer has seen the data. The next recipe — Requiring comments on actions — makes sure they explain a decision before it is recorded.