A null is a value that isn't there: an empty cell, a missing field. Most things you do with a null give null back, and a filter treats null as "no". Set a record's fields to a value or to null, pick an expression, and see every step of how it's worked out.
A filter condition has three possible results: true, false or null. Only true keeps the record. Null is treated like false, so a record whose condition comes out null is dropped too.
That catches people out with "not". If amount is null, then amount > 100 is null, and so is not (amount > 100). Both filters drop the record, so a record can fail a test and its opposite at the same time.
== and != are different. They never give null: null == null is true, and null != 100 is true. So filter amount != 100 keeps a record with no amount. Every other comparison (<, >, <=, >=) and every calculation gives null when either side is null.To decide yourself what happens to empty values, ask for them directly with is_null(), or give a default first with ??: filter (amount ?? 0) > 100.
if … then … else takes the else branch when the condition is null. With no else, the result is null.match without a subject skips an arm whose condition is null. With a subject (match status { … }) it compares with ==, so a null => arm matches a missing value.?? gives the right side only when the left side is null.+ - * /) give null if either side is null.is_null(), is_empty(), type_of() and catch(x), which are made for nulls.Read null as "unknown". false and unknown is false whatever the unknown is, so it's false. true and unknown could go either way, so it stays null. The same reasoning makes true or null true. The engine also stops early: if the left side of and is false, or the left side of or is true, the right side is never worked out. The tree in section 01 greys it out.
For a record with no amount, filter amount > 100 and filter not (amount > 100) both drop it. Splitting records into two outputs with a condition and its opposite loses the empty ones. Send them somewhere on purpose with is_null().
filter status != "closed" keeps records with no status, because null != "closed" is true. Add and not status.is_null() if they should go.
Two missing values count as equal. Comparing two empty fields with == gives true, not unknown. Some other tools treat this differently, so don't carry that habit over.
price * qty is null if either is missing, and so is anything built on it. Give defaults where the value enters: (qty ?? 1).
.debug("label") logs the value it's called on, except null: on a null it returns null without logging, like most methods. Check with is_null() instead.