NDOVU · AUDIO EXPLAINER · LISTEN
Keys: space/K play-pause · J/L jump 10s · arrows on the waveform nudge 5s
The full briefing transcript, timed to the audio. Click any line to jump there; the current line lights up while you listen.
HostYou write a filter. It parses. It is textbook-correct against the RFC grammar. The provider answers 400 anyway.
HostNothing about your syntax is broken. The grammar allows the filter, and the provider that must process it documents a narrower contract: an operator subset, an allowlist of matchable attributes. A filter can be valid per the RFC and unsupported in the same breath, and that gap is where real integrations fail. This is the story of the SCIM filter - SCIM, S, C, I, M, pronounced sim: where the grammar came from, how a filter travels from query string to response, and the three failure modes that wait along the way.
HostSCIM filtering descends from the LDAP search filter line, carried forward through the SCIM 1.x drafts into Section 3.4.2.2 of RFC 7644. The grammar kept the LDAP shape: attribute expressions with comparison operators like equals, not-equals, contains, starts-with, ends-with, and the present test; logical combinators and, or, and not; grouping parentheses; and bracketed value paths that reach into complex attributes, like the email address of the work-type email.
HostThe operators and logical keywords are case-insensitive. The comparison literals true, false, and null stay case-sensitive. Precedence follows the reading the RFC's own examples require: attribute expressions are atomic, not binds tighter than and, and binds tighter than or, and explicit parentheses override all of it.
HostA filter travels through four stages, and each stage has its own way to fail. The journey starts as a query parameter on a GET to the Users endpoint. First, the provider gate: each provider documents which operators and attributes it actually processes, and a filter outside that subset is rejected with 400 unsupported filter. This is not an RFC violation. It is the documented, narrower contract an integration must model before the first request goes out.
HostSecond, the parser: the grammar itself, with a depth cap, because recursive-descent parsing on hostile input is a known denial-of-service class, and two real CVEs of exactly that shape were fixed in 2026. Third, the evaluator: it applies the section's semantics against actual resources. Fourth, the envelope: the ListResponse a compliant server returns, or the error it must return instead.
HostThree semantics rules carry most of the weight. First: absent is false. A filter over an unassigned attribute, null, missing, or an empty array, which are one state in this protocol, evaluates to false. And here is the trap: not-equals over an absent attribute is also false. The attribute did not match, so it did not match the negative filter either. Cross-type comparisons behave the same way, evaluating as absent rather than erroring.
HostSecond: string comparisons follow the attribute's caseExact characteristic, which defaults to false. Both sides fold unless the schema says otherwise, so whether a reference matches byte-for-byte is decided by the schema, not by you.
HostThird: ordered comparisons, greater-than and friends, against Boolean or Binary attributes fail with the keyword invalidFilter. That is a real error the server must raise, not a silent false.
HostAnd the envelope: a query answers 200 with a ListResponse. Total results is required. The resources array is required when the result set is non-zero. Start index and items per page are required when the response is partial. An empty match is not an error: it is a 200 with total results zero. One more gate exists at the server root: a match count past the server's threshold is rejected with 400 tooMany rather than dumping the set, the protocol's defense against unselective filters that force full-directory evaluation.
HostSo the failure modes, in order of how often they bite. One: the provider subset. Entra sends equals and and, on configured matching attributes. GitHub's enterprise build accepts one equals at a time. Zoom documents exactly two filters. Okta requires a user-name equals for user matching, the exact filter. Two: the parser rejects a filter the grammar accepts. There are recorded cases of valid filters being rejected by real implementations, and the grammar itself carries three open errata, documented disputes about the published text. Two correct parsers can disagree about the same input. Three: the hostile filter. Deeply nested expressions drive recursive-descent parsers to stack exhaustion, which is why implementations cap depth, and why you should too.
HostThree things to carry. First: valid is not supported. The grammar is the floor, the provider's documented subset is the contract, and the 400 that separates them is the provider doing its job. Second: absent is false, including for not-equals, so a filter can never find what was never assigned. Third: the envelope is part of the contract. An empty match is a 200 with total results zero, and tooMany is the protocol refusing to evaluate your unselective filter against the whole directory. Read one filter apart from the network and all four stages become visible; that is exactly what the filter playground on this site is for.