# 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.

Study 0002 · Decided · 24 Oct 2026 · By Shibin Das. Source: https://www.d34dman.com/studies/0002-absent-null-empty/

Here is a node's settings, as a workflow sends them:

```json
{ "retries": null }
```

Does that mean "I didn't set retries, use the default"? Or "retries: none, and ignore whatever is saved"? If you build a JSON API, you have an answer, and the person writing the client in another language may have a different one. Neither of you wrote it down.

This study is how I answered that for the FlowDrop spec, by giving every kind of "nothing" exactly one meaning. If you only want the result, skip to [the five rules](#five-rules-worth-stealing) at the end.

## What the FlowDrop spec is

[FlowDrop](https://www.d34dman.com/projects/flowdrop/) is a visual workflow editor: you draw nodes, connect their inputs and outputs, and something runs the result. The editor is a JavaScript library. The running happens elsewhere, for example in the FlowDrop modules for Drupal.

When more than one program reads the same workflow, "what the code does" stops being a definition. So the rules a workflow obeys (how it is written, stored, validated and run) are written down on their own, independent of any implementation: the [FlowDrop Workflow Specification](https://flowdrop.io/spec/). It is a set of short rules, each with a permanent id like [CFG-7](https://flowdrop.io/spec/rules/gr-cfg/cfg-7/) or [STORE-4](https://flowdrop.io/spec/rules/gr-store/store-4/), so that tests and issues can cite them. Where an implementation disagrees with a rule, the implementation has the bug, unless the rule is wrong. It is at version 1.0-draft. I'm reviewing it rule by rule before 1.0, and this study came out of that review.

Most of those rules move JSON around: a workflow's inputs, a node's saved settings, the values on the wires between nodes, the result of a run. Which brings us back to `null`.

## Four states, two ways to say nothing

A value can be in four states:

1. **Not provided.** Nobody said anything.
2. **Provided as nothing.** Somebody said "no value here", on purpose.
3. **Provided as empty.** `""`, `[]`, `false`, `0`.
4. **Provided with content.**

JSON gives you two ways to write "nothing": leave the key out, or send `null`. So the first two states have a spelling each, *if* you agree which is which. The spec already said that a missing key and a `null` are different. It never said what each one means.

It came up over something small. A workflow declares its outputs, and one of them isn't connected to anything yet. Should the run result carry it as `null`, or leave it out? I had answered that a week earlier with "`null`". When I tried to write the rule down, I couldn't, because the answer depended on what `null` means everywhere else.

## The spec mostly agreed with itself

I read every rule that touches a value and sorted them by how they treat the three "nothing" states. The core was consistent:

- A **missing key** means not provided. A parameter falls through to the saved config, then to the default.
- **`null`** is a real value. It stops there and wins over the saved config.
- An **empty value** is a value too. It also wins.

*Diagram*

**A missing key falls through; `null` stops and wins.** Read each row left to right: a missing key lets the saved config decide, and the default only if that is missing too, while `null` and empty values are answers in their own right.

That model is worth having, because `null` is the only way a workflow can say "use nothing here" against a saved setting. But about a dozen rules quietly read it another way. A `null` default counted as "no default". A required check failed on `null` exactly like a missing key. An extraction that found a `null` reported a miss. Edge keys, versions and message roles treated `""` as if the key weren't there. Each one made sense locally. Together they meant a reader had to check every rule to know what "nothing" meant in it.

## Every language blurs it differently

This is where it stops being a spec problem and becomes everybody's problem. Each language has a comfortable idiom that merges two of the states, and a less comfortable one that keeps them apart. The first three read `null` and a missing key as the same thing; Go's `omitempty` goes further and writes `false`, `0` and `""` as missing:

| Language | Blurs the states | Keeps them apart |
|---|---|---|
| PHP | `isset()` | `array_key_exists()` |
| JavaScript | `??`, `JSON.stringify` | `Object.hasOwn()` |
| Rust | `Option<T>` | `Option<Option<T>>` |
| Go | `omitempty` | leave it off |

None of these is a bug. They are defaults that are right most of the time, which is exactly why a spec that depends on the difference has to say so out loud. An implementation keeps the difference only if someone does it on purpose.

## What I decided

**`null` is a chosen nothing.** A missing key means not provided. `null` means provided, with no value, and it wins over lower tiers. The alternative, "`null` also means not provided", is friendlier to `isset` and `??`, but it leaves two spellings for one meaning and no way at all to clear a saved setting. JSON Schema, which the spec's contracts are written in, already sees it this way: `required` means the key is present, and `null` is a type like any other.

**Empty is always a value.** `false` is a choice. `""` is a string. An untouched optional boolean is *missing*, not `false`.

**An output with no value has no key.** That reversed my own earlier answer. If an unconnected output came back as `null`, it would flow into the next node and, by the first rule, win over that node's saved config. A sub-workflow would override settings by accident. Left out, the next node's config still applies, and `null` keeps its one meaning: some node really produced `null`.

**In an update, `null` clears.** A missing key leaves the stored value alone, `null` clears it, a value sets it. That is [JSON Merge Patch](https://www.rfc-editor.org/rfc/rfc7396), and the Rust implementation already worked this way.

**"Required" keeps two meanings, both named.** At a boundary (launching a workflow) it means the key is present. For a node parameter, it means a non-`null` value once everything is resolved. A node needs a value to run; a boundary needs the key. The glossary now defines both, so nobody has to guess which one a rule means.

## The two calls I made against the recommendation

The study recommended two softer options, and I turned both down.

It suggested that empty could count as "not provided" where a rule says so openly, which would keep some handy input clean-up, like treating a blank version as no version. I said never. An exception like that holds while a value sits in one process, and breaks the moment it is serialized in one language and read back in another. Go's `omitempty` is that exact bug, shipped as a feature.

It also suggested keeping the dozen odd rules as stated exceptions, since each had a local reason. I chose to align all of them instead. That costs behaviour changes in four places and some storage work, because a stored default has to tell "not recorded" (no key) from "recorded as `null`". One exception stays, on purpose: the two meanings of "required".

The recommendation was the cheaper path. I think the cheap path is how a spec ends up with a dozen exceptions in the first place.

## Others paid for this before

The prior art was the most convincing part of the study, because the same lesson keeps coming back:

- **Protocol Buffers** dropped field presence for scalars in proto3, so `0`, `""` and `false` read the same as unset. Version 3.15 brought `optional` back, because people needed the difference. Collapsing empty into absent was undone after years of workarounds.
- **GraphQL** gives a missing input field its default, and passes an explicit `null` on as `null`. The same three states, in a widely used API language.
- **Kubernetes** makes optional fields pointers, so unset (`nil`) stays distinct from the zero value. A large API with many clients chose to enforce the split in the type system rather than rely on care.
- **JSON Merge Patch** uses absent and `null` exactly as above, and has one known gap: a patch can't *set* a value to `null`. Worth knowing before you copy it.

## Tests, not review

Rules in prose get read differently in each language, which was the whole problem. So the rulings come with a small set of **presence fixtures**: for each tier and each output, one case with the key missing, one with `null`, one empty and one with a value, each stating its input, its expected result and the rules it tests. The spec repo validates them, and every implementation runs the same files. When a PHP `isset` or a serde `Option` slips back in, a test fails instead of a reviewer having to notice.

## Five rules worth stealing

If you design a JSON API or a config format, write these down before your second client exists:

1. **A missing key means "not provided".** Defaults and fallbacks apply.
2. **`null` means "provided, and it's nothing".** It overrides, and in an update it clears.
3. **Empty is a value.** `false`, `0`, `""` and `[]` are answers, never "not set".
4. **Output nothing as no key.** Don't invent a `null` for something nobody produced.
5. **Name the presence test for each language you support,** and back it with shared fixtures: `array_key_exists`, `Object.hasOwn`, `Option<Option<T>>`, no `omitempty`.

None of this is new. What was missing was writing it down in one place, before the implementations each decided for themselves.

## Decisions at a glance

| Question | Decided |
| --- | --- |
| Should the answer bind the whole spec, or only workflow values? | The whole spec |
| What does null mean? | Null is a chosen 'nothing' |
| Is an empty value ever the same as not provided? | Never |
| What does a run result carry for an output with no value? | Leave the key out |
| What happens to the rules that read it differently? | Align all to the convention |
| What does 'required' demand? | Keep both, name them |
| In a partial update, what does null do? | Null clears the value |
| Which direction do we plan for? | Two rulings plus fixtures |

## The full record

Everything the study asked and answered, also at https://www.d34dman.com/studies/0002-absent-null-empty/record/.

## 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.

*Diagram: How a parameter resolves today: an absent key falls through to config then default; an explicit null or an empty value stops at the first tier that has the key.*

### 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()` 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

- **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

- 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 (Shibin Das, 24 Oct 2026)

The owner answered yes and asked to continue.

### 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.

#### The whole spec (decided, recommended)

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

#### 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 (Shibin Das, 24 Oct 2026)

The whole spec: one definition in conventions.md binds every payload.

### 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.

#### Null is a chosen 'nothing' (decided, recommended)

Absent key means not provided. `null` means 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 ??

#### Null also means not provided

Absent and `null` both 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; `null` marks 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' (Shibin Das, 24 Oct 2026)

Absent means not provided; null means provided with no value and overrides lower tiers.

### 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.

#### Never (decided)

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

#### Only where a rule says so (recommended)

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 (Shibin Das, 24 Oct 2026)

Empty is always a value, with no rule-level exceptions: an exception would bite when values are serialized and read back.

### 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.

#### Leave the key out (decided, recommended)

Both cases are absent; `null` only when a node produced `null`. 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

#### Always send every key

Every declared output is present; `null` when 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 (Shibin Das, 24 Oct 2026)

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.

### 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.

#### Align all to the convention (decided)

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

#### Keep each, stated as an exception (recommended)

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 (Shibin Das, 24 Oct 2026)

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.

### D-05: What does 'required' demand?

#### Keep both, name them (decided, recommended)

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

#### 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 (Shibin Das, 24 Oct 2026)

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.

### D-06: In a partial update, what does null do?

#### Null clears the value (decided, recommended)

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

#### 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 (Shibin Das, 24 Oct 2026)

In a partial update, absent leaves a value as stored, null clears it, a value sets it.

## Prior art

- **[JSON Schema 2020-12](https://json-schema.org/draft/2020-12/json-schema-validation)** (Standard). `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.
- **[GraphQL input coercion](https://spec.graphql.org/October2021/#sec-Input-Objects.Input-Coercion)** (Standard). 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.
- **[JSON Merge Patch (RFC 7396)](https://www.rfc-editor.org/rfc/rfc7396)** (Standard). 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.
- **[Protocol Buffers field presence](https://protobuf.dev/programming-guides/field_presence/)** (Prior decision). 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.
- **[Go encoding/json omitempty](https://pkg.go.dev/encoding/json)** (Pitfall). `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.
- **[Kubernetes API conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#optional-vs-required)** (Practice). 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.
- **[Drupal config castValue()](https://git.drupalcode.org/project/drupal/-/blob/11.x/core/lib/Drupal/Core/Config/StorableConfigBase.php)** (Project). 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'.
- **[Language traps: PHP, JavaScript, serde](https://docs.rs/serde_with/latest/serde_with/rust/double_option/index.html)** (Pitfall). 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>>.
- **Three-state settings in the Rust backend** (Project). 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.

### 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.

*Diagram: One ruling covers the convention, the aligned rules and the unbound entries*

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).

*Diagram: OPEN-26 holds the convention and aligned rules; OPEN-27 holds unbound entries and cites OPEN-26*

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

### Two rulings plus fixtures (chosen, recommended)

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.

*Diagram: Two rulings plus a shared fixture set run by three implementations*

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

**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:** Two rulings plus fixtures (Shibin Das, 24 Oct 2026)

Two rulings (OPEN-26 the four states and aligned rules, OPEN-27 unbound interface entries) plus a presence fixture set written with them in the spec repo; wiring into each implementation follows in its own repo, the Drupal module first.

### 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

### 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. (Shibin Das, 24 Oct 2026)

Suggested answer accepted.

### 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. (Shibin Das, 24 Oct 2026)

Suggested answer accepted.

### 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. (Shibin Das, 24 Oct 2026)

Suggested answer accepted. A second stated difference from JSON Schema, beside CFG-9.

### 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. (Shibin Das, 24 Oct 2026)

Suggested answer accepted.

### 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. (Shibin Das, 24 Oct 2026)

Suggested answer accepted.

## Next

- Write OPEN-26 (conventions, glossary, six aligned rules, CFG-9 and CFG-10 stated)
- Write OPEN-27 (MAN-2, 8, 14, 20, 21, R4.c, R4.d)
- Add fixtures/presence with its schema and validator check; list fixtures on rule pages
- File issues for the behaviour changes and the fixture harness in each implementation, the Drupal module first
- Then continue the review of the spec

## About this record

A read-only record of a problem study. Shibin Das is the author; others are named only if they agreed to it, everyone else appears by role.

Ids like CFG-7 or STORE-4 are rules of the FlowDrop Workflow Specification (version 1.0-draft), each with its own page at https://flowdrop.io/spec/. OPEN-26 and OPEN-27 are the spec's rulings this study led to.

The live, interactive study ran on claude.ai; this page is the read-only record. One implementation is internal, so it appears as "the Rust backend", and internal plan step ids were left out.
