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.

Caller or wireSaved configDefaultkey absentnothingnothingdefault usednullnull winsskippedskipped"" false 0value winsskippedskipped

How the spec reads it today

The core rules follow one reading. The bold cells are where a rule reads it differently.

WhereAbsent keynullEmpty ("", [], false)
Parameter tiers (CFG-5, CFG-7)not provided, falls througha value, winsa value, wins
Edge delivery (DATA-1, DATA-6)delivers nothingdelivers null, winsdelivers it
Workflow outputs (MAN-17)produced nothing, droppedproduced null, returnedreturned
Node-type default (CFG-3)not recordedsame as not recordeda default
Required check (CFG-9, CFG-21)missing: fail, or skip if markedfails like missing, never skipspasses
Extraction (LANG-24)—a miss, default deliveredfound
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() treats null as missing. Only array_key_exists() keeps them apart.
  • JavaScript: undefined is dropped when JSON is written, and ?? and ?. treat null and undefined alike.
  • Rust: a plain serde Option reads absent and null both as None. The Rust backend already uses Option<Option<T>> for some settings, where absent means leave it, null means 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.

  1. U-02 Should the answer bind the whole spec, or only workflow values? The whole spec
  2. D-01 What does null mean? Null is a chosen 'nothing'
  3. D-02 Is an empty value ever the same as not provided? Never
  4. D-03 What does a run result carry for an output with no value? Leave the key out
  5. D-04 What happens to the rules that read it differently? Align all to the convention
  6. D-05 What does 'required' demand? Keep both, name them
  7. 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. null means provided, with no value, and overrides lower tiers. Today's core rules.

    key absentconfig usednullnull winsclear a config valuesend null

    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 null both mean not provided. Both fall through to config and default.

    key absentconfig usednullconfig usedclear a config valueno way

    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; null marks the ones not provided. Absent becomes an error.

    key absenterrornullconfig usedclear a config valueno way

    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; null only when a node produced null. MAN-17 unchanged.

    unboundabsentbranch skippedabsentnode produced nullnull

    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; null when nothing produced it.

    unboundnullbranch skippednullnode produced nullnull

    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.

    unboundnullbranch skippedabsentnode produced nullnull

    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, null clears 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

    required is satisfied by a key being present. null is one of the seven types. default is an annotation, and null is 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 null is passed on as null and 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 null removes 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, "" and false read the same as unset. It brought back optional in 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

    omitempty drops false, 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 "" into NULL for integer and float keys, because forms submit empty strings. A null value 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() treats null as unset. JS drops undefined and ?? merges null with it. serde's Option reads absent and null alike 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, null clears 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

    Effort M · Risk Low

    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

    Effort M · Risk Medium

    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

    Effort M · Risk Low

    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

OPEN-26conventionOPEN-266 aligned rulesOPEN-26unbound entries

Two rulings

OPEN-26conventionOPEN-266 aligned rulesOPEN-27, cites 26unbound entries

Two rulings plus fixtures

OPEN-26 + OPEN-27as option twopresence fixturesspec reporun byeach implementation

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.

Decided by Shibin Das, .

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.

Shibin Das, 24 Oct 2026

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.

Shibin Das, 24 Oct 2026

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.

Shibin Das, 24 Oct 2026

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.

Shibin Das, 24 Oct 2026

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.

Shibin Das, 24 Oct 2026

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