filter step partitions a list: every element is evaluated against a per-item gate, and the step yields both halves — items (passed, input order) and dropped (did not). It takes over (anything that renders to an array) and exactly one of where (a logical condition, the same grammar exit and decide use) or infer (a yes/no question judged per item). Where map answers “do this for each of these”, filter answers “which of these should the next step even see?”
{{E1.items}} downstream (a numbered prompt listing, a map’s results) stays aligned with it by construction.
The gate and its scope
The gate is evaluated once per element with two pseudo-roots in scope:{{item}} (the element) and {{index}} (0-based position). It may also reference plan input and any earlier top-level step. where is the shared condition grammar — value/op/to with eq, ne, gt, lt, gte, lte, empty, not_empty, contains. infer asks the judge role a yes/no question per item; model pins those verdicts to a named model or role, and concurrency (default 1) runs them in parallel. A where gate costs no inference at all.
Like every control step, only over renders up front; the gate renders per item, when that item’s scope exists — validation rejects {{item}} in over for exactly that reason.
The step result
{{Ex.dropped}} (say, listing deleted files a review should mention) while iteration proceeds over {{Ex.items}}.
An empty over array is not an error — {count: 0, items: [], dropped: [], dropped_count: 0} — and neither is a filter that keeps nothing. Guard with an exit gate on {{Ex.count}} when an empty selection should stop the plan. A non-array over is a plan defect (hard failure in your plans, replan for the planner), and so is a gate that references a field an element does not have.
Inside bodies
Unlikeexit, decide, map, and reduce, a filter may appear inside a decide branch or a map/reduce body — it is pure selection: no tool dispatch, no execution-gate consultation, no body of its own, so the reasons control steps stay out of bodies do not apply to it.
Inside a body, the filter’s own {{item}}/{{index}} shadow the enclosing body’s within the gate — innermost wins. Reference the outer element in over, where the filter’s pseudo-roots do not yet exist:
Failure and cost
A gate that fails to evaluate — a missing field, a non-number under an ordering op, a judge error — fails the whole step, attributed with the item index (item 3: …). Human-authored plans fail hard as always; planner-authored plans replan with that context. EmptyData raised while rendering degrades normally (errors). Under infer with concurrency, verdicts already in flight drain on failure; unstarted items are skipped — same policy as map.
In the workbench, a filter renders as a
▽ junction. Because it never dispatches, the debugger’s execution gate does not pause on it — set a breakpoint on the step itself to inspect the partition.