Skip to main content
A filter is the 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, and obs.matches.
The engine, the Rust SDK (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. capture only 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.emit or obs.call: input, caller, and metadata. output and error do not exist at that point.

Syntax

A path clause always takes an operator object. There is no literal shorthand: { "input.op": "insert" } is INVALID_FILTER; write { "input.op": { "$eq": "insert" } }.
  • A filter is a JSON object. Anything else, null included, is INVALID_FILTER. An absent filters matches every event.
  • A filter object holds paths, $and, $or, and $not. Any other $ key is INVALID_FILTER, value operators included ({ "$eq": 1 } needs a path).
  • $and and $or take a non-empty array of filters ([] is INVALID_FILTER). $not takes one filter. {} is allowed there ({ "$not": {} } matches nothing) and as a member of $and or $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 / $not inside it is INVALID_FILTER.

Paths

  • A path splits on unescaped dots. \. is a literal dot and \\ a literal backslash; any other backslash sequence and a trailing backslash are INVALID_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) is INVALID_FILTER.
  • The first segment is the root: input, output, error, caller, or metadata (case-sensitive). A path may be the root alone: "error": { "$exists": true }.
  • An index segment (0, 1, 12) selects an array element. 01 and -1 are 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: false match an absent path; every other operator is false. $not negates its whole filter, so it matches when the inner path is absent.
  • null is a value. { "$exists": true } matches null, and { "$eq": null } matches only null, 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: 1 equals 1.0, and -0 equals 0. 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 (1e16 is INVALID_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: undefined is absent, a Date is its ISO string, and a BigInt or Decimal compares 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 answers INVALID_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 a request_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 is INVALID_FILTER_PATH, so a typo such as input.tabel fails 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 / allOf with several branches, patternProperties, or if. Local $refs and nullable wrappers are followed.
  • A path that reaches a false schema is INVALID_FILTER_PATH.
  • @ paths inside $any are checked against the array’s items.
  • When the function is not registered yet, the checks run when it registers.
Schema checks run in the engine only. 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 ($not and $or before paths), then by the compact JSON of their operand, and exact duplicates are dropped.
  • Operator object keys are sorted. Operands, $in / $nin items, paths, and the order of $or members are kept as written. $ne and $nin are not rewritten.
Normalizing is idempotent and matches exactly the documents its source matches. It never increases the depth, node, item, segment, or literal limits; the encoded size can grow, so workers compile the normal form they receive without the size limit.

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 with obs.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: