Study 0002 Decided Full record
Absent, null and empty
What a missing key, a null and an empty value each mean in the FlowDrop spec, so every implementation reads them the same way.
This is the full record behind the study: every question, every option and every answer. For the story of how it was decided, read the essay.
Context and problem
A value in a workflow can be in four states: not provided, provided as nothing, provided as empty ("", [], false, 0) and provided with content. JSON has two ways to say "nothing": leave the key out, or send null.
The conventions say an absent key and a null are different, but never say what each one means. The core of the spec agrees with itself: a missing key means not provided, and null is a real value that overrides config. About a dozen rules quietly read it another way.
It came up with unbound workflow outputs (MAN-20): should a run result carry such an output as null, or leave it out? We can't answer that without first deciding what null means everywhere.
How the spec reads it today
The core rules follow one reading. The bold cells are where a rule reads it differently.
| Where | Absent key | null | Empty ("", [], false) |
|---|---|---|---|
| Parameter tiers (CFG-5, CFG-7) | not provided, falls through | a value, wins | a value, wins |
| Edge delivery (DATA-1, DATA-6) | delivers nothing | delivers null, wins | delivers it |
| Workflow outputs (MAN-17) | produced nothing, dropped | produced null, returned | returned |
| Node-type default (CFG-3) | not recorded | same as not recorded | a default |
| Required check (CFG-9, CFG-21) | missing: fail, or skip if marked | fails like missing, never skips | passes |
| Extraction (LANG-24) | — | a miss, default delivered | found |
| Workflow update (STORE-4) | leave as stored | — | empty inputs clears the side |
| Edge keys, versions, roles (EDGE-2, INT-8, MEM-16) | same as empty | — | same as absent |
Why it matters across systems
Each language we ship in blurs the states in its own way:
- PHP:
isset()treatsnullas missing. Onlyarray_key_exists()keeps them apart. - JavaScript:
undefinedis dropped when JSON is written, and??and?.treatnullandundefinedalike. - Rust: a plain serde
Optionreads absent andnullboth asNone. The Rust backend already usesOption<Option<T>>for some settings, where absent means leave it,nullmeans clear it, and a value means set it.
Whatever the spec decides, each implementation has to keep that difference on purpose.
The question behind it
Take an optional boolean that nobody has touched. Is it left out, or sent as null? If null means "not provided", the spec has two spellings for one meaning, and nothing left that says "clear this". If a missing key means "not provided", a consumer that expects a fixed set of keys has to read the contract to learn which keys can exist.
In scope
- What absent, null and empty mean in payloads, config, wires, launch inputs and run results
- The conventions text and the rules that read these states differently
- What an unbound or empty workflow output carries (feeds the MAN-20 ruling)
Out of scope
- How an editor draws an untouched field: that is an implementation's own affair
- The rest of the MAN-20 unbound-entry ruling
- Changing implementations: that follows the ruling
Assumptions
- The Drupal module checks key presence with array_key_exists on every tier path (CFG-7's tests pin this)
- The JavaScript library leaves an untouched optional field out of the payload rather than sending null
- No caller depends on null meaning 'not provided', except through CFG-3 and LANG-24
Glossary (5 terms)
- Absent
- The key is not in the JSON object at all.
- Null
- The key is present and its value is JSON null.
- Empty
- A present value with no content: "", [], {}, false or 0.
- Tier
- One of the places a parameter value can come from: caller or wire, saved config, default (CFG-4 to CFG-6).
- Unbound output
- A declared workflow output that no node port feeds yet (MAN-20).
Decision drivers
What the direction had to satisfy. Each one was decided first; the details are below.
- U-02 Should the answer bind the whole spec, or only workflow values? The whole spec
- D-01 What does null mean? Null is a chosen 'nothing'
- D-02 Is an empty value ever the same as not provided? Never
- D-03 What does a run result carry for an output with no value? Leave the key out
- D-04 What happens to the rules that read it differently? Align all to the convention
- D-05 What does 'required' demand? Keep both, name them
- D-06 In a partial update, what does null do? Null clears the value
Decisions
U-01
Does this capture the problem?
Decided Yes, this matches my understanding
The owner answered yes and asked to continue.
Decided by Shibin Das, .
U-02
Should the answer bind the whole spec, or only workflow values?
This sets how many rules the ruling touches, and whether conventions.md or a family of rules carries the definition.
-
Decided Recommended
The whole spec
One definition in conventions.md binds every payload.
Pros
- One reading everywhere
- conventions.md already speaks spec-wide
Cons
- More rules to check, API ones included
The other option
-
Workflow values only
Inputs, config tiers, wires and outputs. API payloads keep their own rules.
Pros
- Smaller ruling
Cons
- Two readings in one spec
- STORE-4 and EDGE-2 stay ambiguous
Recommendation The whole spec, because conventions.md already states the distinction spec-wide; only the meanings are missing.
Decided The whole spec
The whole spec: one definition in conventions.md binds every payload.
Decided by Shibin Das, .
D-01
What does null mean?
It decides whether a workflow can say 'use nothing here' against a saved config (CFG-7), and how many spellings of 'not provided' every consumer must handle.
-
Decided Recommended
Null is a chosen 'nothing'
Absent key means not provided.
nullmeans provided, with no value, and overrides lower tiers. Today's core rules.Pros
- One spelling per meaning
- Keeps CFG-7, DATA-1, MAN-17
- JSON Schema aligned: required is presence, null a type
Cons
- Implementations must test key presence, not isset or ??
The other 2 options
-
Null also means not provided
Absent and
nullboth mean not provided. Both fall through to config and default.Pros
- Forgiving to sloppy callers
- PHP isset and JS ?? just work
Cons
- Two spellings for one meaning
- No way to override config with nothing
- Differs from JSON Schema; reverses CFG-7, DATA-1, MAN-17
-
Every key sent, null = not provided
Payloads always carry every declared key;
nullmarks the ones not provided. Absent becomes an error.Pros
- Fixed payload shape
- No undefined across languages
Cons
- No way to override config with nothing
- Every caller must send every key
- Breaks partial updates (STORE-4)
Recommendation Null is a chosen 'nothing', because only it keeps one spelling per meaning and still lets a workflow clear a value. The PHP and JS traps are named in the narrative.
Decided Null is a chosen 'nothing'
Absent means not provided; null means provided with no value and overrides lower tiers.
Decided by Shibin Das, .
D-02
Is an empty value ever the same as not provided?
Settles the boolean case: an untouched optional boolean is absent, and false is a choice. It also sets how the exceptions in the table are written.
-
Decided
Never
Empty is always a value. INT-8 and MEM-16 must be rewritten to test for absence.
Pros
- No exceptions at all
- JSON Schema aligned: empty values are values
Cons
- Changes behaviour on blank versions and ids
The other 2 options
-
Recommended
Only where a rule says so
Empty is a value by default. A rule may treat it as absent, and must say so in its text.
Pros
- Default is clear
- Keeps sensible input clean-up
- JSON Schema aligned by default
Cons
- A few named exceptions remain
-
Each rule decides
No default. Status quo.
Pros
- No change
Cons
- Readers must check every rule
Recommendation Only where a rule says so: empty is a value by default, and the few rules that clean up blank input say so openly.
Decided Never
Empty is always a value, with no rule-level exceptions: an exception would bite when values are serialized and read back.
Decided by Shibin Das, .
D-03
What does a run result carry for an output with no value?
Unblocks ruling OPEN-26. Through CFG-7, a null output passed into the next node beats that node's saved config, so this also changes sub-workflow chaining.
-
Decided Recommended
Leave the key out
Both cases are absent;
nullonly when a node producednull. MAN-17 unchanged.Pros
- Null keeps one meaning
- Next node's config still applies
- JSON Schema aligned: result validates against its contract
Cons
- Reverses the 2026-10-15 answer
- Caller reads the contract for the key list
The other 2 options
-
Always send every key
Every declared output is present;
nullwhen nothing produced it.Pros
- Fixed result shape
Cons
- Produced null and nothing look alike
- Skipped branch overrides next node's config
- JSON Schema: null fails a non-nullable output type
-
Null only when unbound
The 2026-10-15 answer: unbound is
null, a skipped branch stays absent.Pros
- Matches the earlier answer
Cons
- Null means two things
- Unbound output overrides next node's config
- JSON Schema: null fails a non-nullable output type
Recommendation Leave the key out, because null then keeps one meaning and a sub-workflow never overrides the next node's config by accident. The contract still lists every key.
Decided Leave the key out
An unbound output and one whose node produced nothing are both absent from the run result; null only when a node produced null. MAN-17 stands.
Decided by Shibin Das, .
D-04
What happens to the rules that read it differently?
Decides how many rules OPEN-26 rewrites. A rule that stays different has to say so in its own text, per conventions.md.
-
Decided
Align all to the convention
Rewrite each rule so it follows D-01 and D-02 with no exceptions.
Pros
- No exceptions left
- CFG-3 matches JSON Schema defaults
Cons
- Behaviour changes in four places
- CFG-3 needs storage that tells unset from null
The other 2 options
-
Recommended
Keep each, stated as an exception
No behaviour change. Each rule names the reading it uses and why.
Pros
- No behaviour change
- Readers see every exception
Cons
- Six exceptions to a fresh convention
- CFG-3 differs from JSON Schema, where null is a default
-
Align some, keep the rest
Keep CFG-3, CFG-9, LANG-24 and EDGE-2 as stated exceptions; align INT-8 and MEM-16 if D-02 is 'never'.
Pros
- Changes only what is cheap
Cons
- Needs a call per rule
Recommendation Keep each, stated as an exception: every one has a reason (CFG-3: storage cannot tell unset from null; LANG-24: an extraction has no 'absent'), and none of them leaks across a boundary.
Decided Align all to the convention
Rewrite the rules that read null or empty differently so they follow D-01 and D-02. D-05 keeps CFG-9's meaning of required as the one named exception.
Decided by Shibin Das, .
D-05
What does 'required' demand?
-
Decided Recommended
Keep both, name them
Present at the boundary, non-null once resolved. The glossary defines both.
Pros
- No behaviour change
Cons
- Two meanings of one word
- Differs from JSON Schema at CFG-9
The other option
-
Present everywhere
JSON Schema's meaning; the type decides whether null passes.
Pros
- One meaning
- JSON Schema aligned
Cons
- Changes CFG-9 for nullable types
Recommendation Keep both and name them: a node needs a value to run, a boundary needs the key. The outcomes only differ for a type that admits null.
Decided Keep both, name them
Required means present at a boundary (MAN-15) and a non-null value once resolved for a node parameter (CFG-9); the glossary names both.
Decided by Shibin Das, .
D-06
In a partial update, what does null do?
-
Decided Recommended
Null clears the value
Absent leaves it,
nullclears it, a value sets it. The Rust backend already works this way.Pros
- Follows from D-01
- Same as JSON Merge Patch; JSON Schema is silent
Cons
- Must be stated per updatable key
The other option
-
Null is refused
An update may not send
null; clearing needs an empty value.Pros
- No ambiguity
Cons
- Empty and cleared become one state
Recommendation Null clears the value, because it follows from D-01 and matches the three-state settings the Rust backend already ships.
Decided Null clears the value
In a partial update, absent leaves a value as stored, null clears it, a value sets it.
Decided by Shibin Das, .
Prior art
What others did, and what we took from it.
-
Standard
JSON Schema 2020-12
requiredis satisfied by a key being present.nullis one of the seven types.defaultis an annotation, andnullis a valid default.Takeaway Our contracts are JSON Schema, so D-01, D-02 and D-03 follow its model; CFG-3 aligned (D-04) now matches its default too.
-
Standard
GraphQL input coercion
An input field that is not provided gets its default. An explicit
nullis passed on asnulland does not get the default.Takeaway The same three states as D-01, in a widely used API language: 'not provided' and 'null' are both legal, and they differ.
-
Standard
JSON Merge Patch (RFC 7396)
In a patch, an absent member leaves the target alone and
nullremoves it.Takeaway D-06 matches it. Its known gap: a patch cannot set a value to null, which our update rules must not inherit where null is meaningful.
-
Prior decision
Protocol Buffers field presence
proto3 dropped presence for scalars, so
0,""andfalseread the same as unset. It brought backoptionalin 3.15 because users needed the difference.Takeaway Collapsing empty into absent was reversed after years of workarounds; D-02 'never' avoids repeating it.
-
Pitfall
Go encoding/json omitempty
omitemptydropsfalse,0,""and empty arrays and maps when it writes JSON, so a chosen empty value goes out as absent.Takeaway The serialization bite the D-02 note warns about. An implementation must never drop empty values when it writes.
-
Practice
Kubernetes API conventions
Optional fields are pointers, so that unset (
nil) stays distinct from the zero value.Takeaway A large, many-client API chose the same split, and enforces it in the type system rather than by care.
-
Project
Drupal config castValue()
Saving config turns
""intoNULLfor integer and float keys, because forms submit empty strings. Anullvalue keeps its key.Takeaway The Drupal module stores defaults and saved config this way, so CFG-3 and CFG-5 need absent keys, not empty strings, for 'not recorded'.
-
Pitfall
Language traps: PHP, JavaScript, serde
PHP
isset()treatsnullas unset. JS dropsundefinedand??mergesnullwith it. serde'sOptionreads absent andnullalike unless you use a double option.Takeaway Each stack needs the presence test by name: array_key_exists, Object.hasOwn, Option<Option<T>>.
-
Project
Three-state settings in the Rust backend
Some stored settings are
Option<Option<String>>: absent leaves it,nullclears it, a value sets it.Takeaway D-01 and D-06 already ship in our Rust backend; the spec catches up with it.
Considered options
Sets what gets written next in the spec repo, and whether the cross-language bite from D-02 is caught by tests or only by review.
-
Chosen Recommended
Two rulings plus fixtures
As two rulings, written together with a small presence fixture set in the spec repo (absent, null, empty, value per tier and per output), each fixture citing its rule ids and checked by validate-rules.mjs. Wiring the fixtures into each implementation becomes follow-up work in its own repo, the Drupal module first. The JavaScript library resolves no values, so it only checks that a payload survives a write and a read.Pros
- Catches omitempty-style bugs in tests
- Fixtures double as narrative examples
- Tests results against their JSON Schema contract
Cons
- A fixture format to decide now
- Harness work later in three repos
The other 2 options
-
One ruling for both
OPEN-26 defines the four states in conventions.md, aligns the rules (D-04) and reverses MAN-20's draft clause, all at once.Pros
- One record to read
Cons
- Mixes a convention with a feature
- Harder to cite precisely
-
Two rulings
OPEN-26 defines the four states and aligns the rules. OPEN-27 makes unbound interface entries legal and cites OPEN-26 for their outputs (D-03).Pros
- Each ruling answers one question
- Implementation work splits the same way
Cons
- Cross-language bugs caught only in review
- JSON Schema alignment checked by reading, not tests
One ruling for both
Two rulings
Two rulings plus fixtures
Recommendation Two rulings plus fixtures, because the fixtures are small to write with the rulings and catch the serialization bite D-02 fears. Wiring them into each implementation is follow-up work there, so the spec review isn't blocked.
Decision outcome
Chosen · E-01 Two rulings plus fixtures
The spec gets one definition of the four states, binding every payload: absent means not provided, null is a chosen nothing, and empty is always a value. OPEN-26 writes it into conventions.md and aligns the rules that read it differently. OPEN-27 makes unbound interface entries legal, and their outputs are absent. Presence fixtures are written with both rulings, and each implementation wires them in afterwards.
Good, because
- Catches omitempty-style bugs in tests
- Fixtures double as narrative examples
- Tests results against their JSON Schema contract
Bad, because
- A fixture format to decide now
- Harness work later in three repos
Clarifications
The questions a plan would otherwise have to guess, with the answers given.
C-01
Where do the fixtures live, and what does one look like?
fixtures/presence/<case>.json holding rules, given, expect; validate-rules.mjs checks shape and rule ids; each rule page lists its fixtures.
Decided fixtures/presence/<case>.json holding rules, given, expect; validate-rules.mjs checks shape and rule ids; each rule page lists its fixtures.
Suggested answer accepted.
Decided by Shibin Das, .
C-02
Do the rulings land before the sweep continues?
Yes: commit OPEN-26, OPEN-27 and fixtures before the spec review continues, so it reviews the final text once.
Decided Commit OPEN-26, OPEN-27 and fixtures before the spec review continues, so it reviews the final text once.
Suggested answer accepted.
Decided by Shibin Das, .
C-03
Is an explicit null checked against an optional parameter's type?
Keep CFG-10 as stated with CFG-9 (D-05): a resolved null skips type validation; required alone decides whether null fails.
Decided Keep CFG-10 with CFG-9 (D-05): a resolved null skips type validation; required alone decides whether null fails.
Suggested answer accepted. A second stated difference from JSON Schema, beside CFG-9.
Decided by Shibin Das, .
C-04
What happens to a blank tool call id (MEM-16)?
Keep it as sent; tool-pairing repair already handles a tool row whose id pairs with no call.
Decided A blank tool call id is kept as sent; tool-pairing repair handles a tool row whose id pairs with no call.
Suggested answer accepted.
Decided by Shibin Das, .
C-05
How does the follow-up reach the implementations?
One issue for the Drupal module (behaviour changes and harness) once OPEN-26 commits; the other implementations follow it.
Decided One issue for the Drupal module (behaviour changes and harness) once OPEN-26 commits; the other implementations follow it.
Suggested answer accepted.
Decided by Shibin Das, .
Decision log
| Id | Question | Outcome | Who | When |
|---|---|---|---|---|
| U-01 | Does this capture the problem? | Decided Yes, this matches my understanding | Shibin Das | |
| U-02 | Should the answer bind the whole spec, or only workflow values? | Decided The whole spec | Shibin Das | |
| D-01 | What does null mean? | Decided Null is a chosen 'nothing' | Shibin Das | |
| D-02 | Is an empty value ever the same as not provided? | Decided Never | Shibin Das | |
| D-03 | What does a run result carry for an output with no value? | Decided Leave the key out | Shibin Das | |
| D-04 | What happens to the rules that read it differently? | Decided Align all to the convention | Shibin Das | |
| D-05 | What does 'required' demand? | Decided Keep both, name them | Shibin Das | |
| D-06 | In a partial update, what does null do? | Decided Null clears the value | Shibin Das | |
| E-01 | Which direction do we plan for? | Decided Two rulings plus fixtures | Shibin Das | |
| C-01 | Where do the fixtures live, and what does one look like? | Decided fixtures/presence/<case>.json holding rules, given, expect; validate-rules.mjs checks shape and rule ids; each rule page lists its fixtures. | Shibin Das | |
| C-02 | Do the rulings land before the sweep continues? | Decided Commit OPEN-26, OPEN-27 and fixtures before the spec review continues, so it reviews the final text once. | Shibin Das | |
| C-03 | Is an explicit null checked against an optional parameter's type? | Decided Keep CFG-10 with CFG-9 (D-05): a resolved null skips type validation; required alone decides whether null fails. | Shibin Das | |
| C-04 | What happens to a blank tool call id (MEM-16)? | Decided A blank tool call id is kept as sent; tool-pairing repair handles a tool row whose id pairs with no call. | Shibin Das | |
| C-05 | How does the follow-up reach the implementations? | Decided One issue for the Drupal module (behaviour changes and harness) once OPEN-26 commits; the other implementations follow it. | Shibin Das |