Skip to content

Explicit syntax for a legitimately optional filter condition (design only — no implementation until a real request) #3862

Closed as not planned

Description

@os-zhuang

Status: design placeholder. Do not implement until a real user asks. Filed so the next person who hits the guard finds the reasoning instead of re-deriving it — or worse, relaxing the guard.

The gap

Since #3810, a CRUD node refuses to run when interpolation erased any authored filter condition. That is correct as a default: an absent condition does not narrow a query, it widens it (see the decision note on #3810 for why get_record keeps the hard failure).

But it closes off a shape that is sometimes genuinely intended:

Filter by campaign if the run received one; otherwise return all rows.

Today that is inexpressible. { campaign: '{campaignId}' } on a run without campaignId is indistinguishable — to the guard and to a reader — from { campaign: '{campiagnId}' }, a typo. Both erase a condition; the guard refuses both, which is the right call precisely because it cannot tell them apart.

The constraint any design must respect

Fail-closed stays the default. The author must say "this condition is optional" explicitly, at authoring time. An implicit or inferred optionality re-opens the fail-open direction #3810 closed, and the typo case is far more common than the optional case.

Options sketched (not chosen)

A. An $optional marker on the condition

filter: { campaign: { $optional: '{campaignId}' } }

B. Branch to two get_record nodes

  • Zero new syntax; a decision node routes on whether the variable is set, each branch carrying its own fully-literal filter.
  • Both queries are explicit and greppable — arguably the clearest thing to read.
  • Verbose, and it duplicates the rest of the filter across both branches. Gets ugly with two or more optional conditions (2^N branches).

C. Author-side default at the variable, not the filter

  • Give the flow variable a declared default (campaignId ?? '*') so the token always resolves.
  • Requires a filter semantic for "match anything," which is a bigger and riskier change than either of the above.

Recommendation if this ever gets picked up

Start from A, and require it to be inert unless explicitly written. B already works today with no platform change — point people at it in the meantime; it is the honest answer for the one-or-two-condition case.

Do not

Relax the #3810 guard or downgrade it to a warning to serve this use case. That trades a deterministic build/run failure for a silent data-widening bug, and the optional case is the rare one. See the decision note on #3810.

Related

Activity

  1. os-zhuang commented on Jul 28, 2026

    @os-zhuang
    ContributorAuthor

    Closing: not planned

    Decided proactively rather than left pending a user request. Three reasons, in order of weight.

    1. This is syntax, not capability — the capability already ships

    Option B in the body (a decision node routing to two get_record nodes, each with a fully literal filter) expresses "filter by campaign if one was passed" today, with no platform change. It is also the more readable of the two: both queries are explicit and greppable.

    So $optional would not unlock anything. It would shorten something already expressible — and pay for that shortening with the risk below.

    2. Its dominant practical effect would be a universal guard-suppressor

    The #3810 guard works on AI authors precisely because it is a deterministic hard signal: this filter is broken, fix it. An AI hits the error, reads the named template, corrects the field name, converges in one round.

    Introduce $optional and the same error now has two fixes available:

    1. correct the typo — right
    2. wrap the token in $optional — the error disappears, and the result is wrong

    Fix 2 is cheaper and works on every filter error. A generation loop optimizing for "make the diagnostic go away" reaches for it by default. We would be shipping a one-token switch whose only function is to disable the safety net, and handing it to the actor most likely to pull it reflexively.

    Since ObjectStack's premise is that AI authors the metadata, this is not a marginal concern — it is the primary one.

    3. The asymmetry that makes this a general rule

    • Guards lower the AI error rate. Self-repair converges on hard signals.
    • Escape hatches raise it. They offer a path that is wrong but passes.

    Every new syntax slot is another fork an AI can take incorrectly, and the cost is higher for a platform whose pitch is "small enough for a model to hold in context and reason about whole." The bar for adding expressiveness should therefore sit above the usual one, and the bar for tightening a guard below it.

    If the underlying need resurfaces, it should not come back as this design

    The real need behind this issue is dynamic filters — composing a query from conditions known at run time. That belongs in the data/query layer as explicit filter composition (build a filter from parts, each part present or absent by construction), not as a flow-template token that is allowed to vanish. A vanishing token is indistinguishable from a typo by design; a composed filter is not. Anyone picking this up later should start from that framing, not from $optional.

    What stays true

    The #3810 guard is unchanged and stays fail-closed. #3861 catches the statically decidable half at build time, across all three commands since #3874. Authors needing a conditional filter branch to two nodes.

    Closing as not planned.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions