agenthub · automation.filter
The node that decides who the rest of the workflow runs for. It takes a list in, tests every row against a condition you build on the node itself, and passes through only the rows that match.
Why the node earns its place
An automation is a fan-out, and Filter decides how wide. A batch pipeline reads a dataset then does something per row — send the message, start the run, raise the invoice. The dataset is almost never the audience: out of four hundred members, thirty-seven are due this week.
It is the only step in that chain that is both deterministic and free. No model is called, so nothing is spent deciding, and the verdict is a fixed comparison you can read off the node when someone asks why a member was or was not contacted. Every row it drops before a Fan out is an agent run that is never billed.
How it works at run time
rows input wins, else rows_path walked into data, else data itself if it is already an array. Nothing found is an empty list, not an error.field is a dotted path into that row; blank means the whole row.value_source: a literal, a run variable, or another field of the same row.keep, so non-matching turns the whole predicate into a drop-list.output.rows the rows that passed, unchanged output.count how many passed output.dropped how many did not — watch this in a Test Run
What you wire
Configuration
| Key | Type | Default & options | What it does |
|---|---|---|---|
| rows_path | string | — | Path to the array inside the wired input `data` (e.g. 'data.results'). Ignored when the `rows` input is wired directly. |
| conditions | object | default {"join":"and","children":[]} | Condition tree: {join:'and'|'or', children:[...]}. A child is either a leaf {field, operator, value, value_source} or a nested group with its own join. One joiner per group — no mixed AND/OR at a level. Empty groups are skipped. |
| field | string | — | LEGACY single-condition field. Superseded by `conditions`; still honoured when no tree is set. |
| operator | string | truthy · falsy · exists · absent · eq · ne · gt · gte · lt · lte · contains · not_contains · in · not_in | LEGACY single-condition operator. Superseded by `conditions`. |
| value | string | — | LEGACY single-condition comparand. Superseded by `conditions`. |
| value_context_key | string | — | LEGACY — read the comparand from run context. Superseded by a leaf with value_source='context'. |
| normalize | string | default "none" none · trim · lower · phone10 | Normalize both sides before comparing (per row). |
| keep | string | default "matching" matching · non_matching | Keep rows that MATCH the condition (default), or invert to keep the ones that DON'T (e.g. drop opted-out). |
What usually goes wrong
Watch for this
dropped tells you the truth fastest. A filter that drops everything is nearly always a path problem, not a logic problem — check field against the shape the run actually observed before touching the operator.
One joiner per group: a box is all-AND or all-OR, never mixed. That restriction is what lets the box border stand in for a bracket, so there is no second bracket notation to learn. An empty group constrains nothing rather than dropping every row.
Behaviour & provenance