filters object of an observer’s source. It decides whether one event reaches one
observer’s target. The language is declarative JSON (iii-filter v1): small, total, without side
effects or regular expressions, so the same filter gives the same answer wherever it runs:
- in the engine, for routed invocations of auto-delivered functions;
- in the SDK that hosts the function, for its local invocations;
- in the owner’s SDK, for owner-delivered functions, inside
obs.emit,obs.call, andobs.matches.
iii-filter crate), and the Node, browser, Python, and Go evaluators all
run the same conformance vectors
(sdk/fixtures/filters). For how
observers use filters, see Observers.
Filters are not arguments. An owner-delivered function’s
args (a schedule, a route, a table) go
to its owner, which decides what to set up and which observers each event is for. Filters decide
which of those events reach the target, and the owner never changes them.The document
A filter reads one document per event, with five roots:- A filter may reference any root, whatever the observer captures.
captureonly decides what is shipped to the target. on(success,error,complete) is checked before the filter.- For an owner-delivered function, the document is built from what the owner passes to
obs.emitorobs.call:input,caller, andmetadata.outputanderrordo not exist at that point.
Syntax
{ "input.op": "insert" } is INVALID_FILTER; write { "input.op": { "$eq": "insert" } }.
- A filter is a JSON object. Anything else,
nullincluded, isINVALID_FILTER. An absentfiltersmatches every event. - A filter object holds paths,
$and,$or, and$not. Any other$key isINVALID_FILTER, value operators included ({ "$eq": 1 }needs a path). $andand$ortake a non-empty array of filters ([]isINVALID_FILTER).$nottakes one filter.{}is allowed there ({ "$not": {} }matches nothing) and as a member of$andor$or(it matches everything).- An operator object is non-empty and holds only value operators. An empty
{}, a key without$, an unknown operator, or$and/$or/$notinside it isINVALID_FILTER.
Paths
- A path splits on unescaped dots.
\.is a literal dot and\\a literal backslash; any other backslash sequence and a trailing backslash areINVALID_FILTER. In a JSON string the backslash itself is escaped:"input.headers.x\\.tenant". - An empty path or an empty segment (
input.,.input,input..a) isINVALID_FILTER. - The first segment is the root:
input,output,error,caller, ormetadata(case-sensitive). A path may be the root alone:"error": { "$exists": true }. - An index segment (
0,1,12) selects an array element.01and-1are keys, never indexes. On an object, a numeric segment is a key. @is a root only inside$any, where every path starts at it. After the root,@is an ordinary key.
Operators
The operators of one operator object must all match:
{ "input.total": { "$gte": 100, "$lt": 500 } } is a range.
$any
There is no implicit fan-out over arrays: input.items.sku on an array of items is absent. Use
$any:
- Scalar elements: pass an operator object.
{ "input.tags": { "$any": { "$eq": "vip" } } } - Object elements: pass a filter whose paths start at
@, the current element.{ "input.items": { "$any": { "@.sku": { "$eq": "A-1" }, "@.qty": { "$gte": 10 } } } }
{ "$any": {} } matches any non-empty array. A nested $any binds @ to its own element. An empty
array, a value that is not an array, and an absent path do not match. Mixing operators and paths in
one $any object is INVALID_FILTER.
Matching rules
- Absent paths. A path yields a value or is absent: stepping into a scalar, a missing key, or an
out-of-range index yields absent. Only
$ne,$nin, and$exists: falsematch an absent path; every other operator is false.$notnegates its whole filter, so it matches when the inner path is absent. nullis a value.{ "$exists": true }matchesnull, and{ "$eq": null }matches onlynull, never an absent path.- Own keys only.
constructor,toString, and__proto__are ordinary keys that resolve only when the document holds them. - Equality is JSON deep equality. Object key order is ignored; array order is not. There is no
coercion between types:
0,false,null,"", and"0"all differ. Strings are equal when their code points are (no Unicode normalization). - Numbers compare as IEEE-754 doubles:
1equals1.0, and-0equals0. A payload integer above 2^53 is rounded to the nearest double first. Numeric literals in a filter must be within ±2^53 (9007199254740992), floats included (1e16isINVALID_FILTER). - Ordering applies within one type only. Numbers compare numerically; strings compare by Unicode
code point (UTF-8 byte order, not JavaScript’s UTF-16 order). Any other pairing (mixed types,
null, booleans, arrays, objects, absent) is false for all four operators. $in: []matches nothing;$nin: []matches everything, absent included. An array value is compared with each item as a whole.$prefix: ""matches every string.- Host values. SDKs evaluate as if on the JSON encoding of the document:
undefinedis absent, aDateis its ISO string, and aBigIntorDecimalcompares through its JSON number. - Totality. A filter that was valid at creation answers true or false for every document. It never fails.
Validation
The engine validates every filter when an observer is created and answersINVALID_FILTER or
INVALID_FILTER_PATH in the observer’s error state.
Limits
Every limit fails with
INVALID_FILTER. Documents have no limits.
Schema checks
When the observed function registered arequest_format (for input.* paths) or a
response_format (for output.* paths), the engine checks each path against the schema’s
properties and items:
- A property that a closed schema (
additionalProperties: false) does not declare isINVALID_FILTER_PATH, so a typo such asinput.tabelfails at creation instead of matching nothing. - Under an open schema, the same path is accepted with a warning in the observer’s result. So is a
construct the check cannot follow: a remote
$ref,anyOf/oneOf/allOfwith several branches,patternProperties, orif. Local$refs and nullable wrappers are followed. - A path that reaches a
falseschema isINVALID_FILTER_PATH. @paths inside$anyare checked against the array’sitems.- When the function is not registered yet, the checks run when it registers.
args are validated separately, against the function’s
args_format (INVALID_ARGS), and filters cannot reference them.
Normal form
The engine normalizes every filter before it stores it or pushes it to the function’s owner (engine::observers::list shows the normal form too):
- Every filter, nested ones included, becomes
{}or{ "$and": [ ... ] }with one clause per object. - Explicit
$ands are flattened into the filter that holds them. - Clauses are sorted by key in byte order (
$notand$orbefore paths), then by the compact JSON of their operand, and exact duplicates are dropped. - Operator object keys are sorted. Operands,
$in/$ninitems, paths, and the order of$ormembers are kept as written.$neand$ninare not rewritten.
Examples
From iii lang predicates
A filter maps one to one to a predicate of the iii language, which is why the language accepts explicit operators only:$exists, $prefix, and $any map to functions of the predicate language.
Evaluate a filter in code
Owners evaluate an observer’s filters withobs.matches(doc) (Matches in Go), for example to
answer a request differently before calling obs.call. The Python, Rust, and Go SDKs also expose
the evaluator itself, which is handy in tests:
- Python
- Rust
- Go