picatz / picatz/flowstate

Unfenced expressions: let a field that is an expression just hold one

Open
#545 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

dsl
Dominant language
Go
Stars
9
Forks
0
Avg merge
3h 3m
Merged PRs (30d)
509

Description

Design exploration raised by the owner, 2026-08-13. Not a decision. The question is whether ${...} should be required in positions the schema already knows are expressions, or whether those fields should accept CEL directly.

The idea

Today every expression is fenced, everywhere:

- id: deploy
  if: ${steps.approval.outcome == "deployed"}

But if: cannot hold anything except an expression. The fence carries no information there: it is punctuation the author writes because the parser wants it, not because the meaning is ambiguous. The proposal is that fields whose type is "expression" accept CEL directly, and ${...} survives only where it earns its place, interpolating a value into a string:

- id: deploy
  if: steps.approval.outcome == "deployed"
  log:
    message: deploying ${inputs.version} to ${inputs.environment}

The pull of this is that the file stops looking like a template and starts looking like a language with identifiers and scopes. must: this in ["staging", "production"] reads as a constraint. must: ${this in ["staging", "production"]} reads as a constraint wrapped in ceremony.

There is proven prior art: GitHub Actions takes bare expressions in if: and reserves ${{ }} for interpolation, which is exactly this split, at very large scale.

What YAML actually allows, measured

This is where the idea meets a constraint, and the constraint is real. Parsing each form as YAML:

Form Result
if: steps.approval.outcome == "deployed" parses
must: this in ["staging", "production"] parses
must: this.matches(r'^v?[0-9]+$') parses
subject: issuer + "#" + name parses
subject: "https://issuer.example.com#" + inputs.approver fails to parse
message: "hi %s".format([x]) fails to parse
outcome: a ? b : c fails to parse
must: 'this in ["staging", "production"]' parses

Two rules explain all of it. A plain scalar cannot begin with a quote, so any expression starting with a string literal is a YAML error. And : inside a plain scalar ends it, so a spaced ternary is a YAML error. Both are fixed by quoting the whole expression, which is what the last row shows.

So the honest form of the proposal is not "fences disappear". It is "fences are replaced by YAML's own quoting rules, which apply to some expressions and not others".

That trade deserves a clear head. A fence is uniform: every expression looks the same and the rule is syntactic. Unfenced CEL is lighter in the common case and requires the author to know when YAML forces quotes, which depends on the shape of the expression rather than on anything about Flowstate. Whether that is a better experience is a genuine question, not a foregone conclusion, and it is worth testing on real files rather than on the examples that flatter it.

Where the split would fall

The rule that makes this coherent is that the field's type decides, not the syntax:

  • Expression-typed fields hold CEL directly: if:, until:, items:, must:, a loop's init: and update:, a switch's discriminant, a signal rule's subject:, and the shaped output entries #533 just unified.
  • String-typed fields stay literal by default, with ${...} interpolating into them: message:, url:, a description. #413 made multi-fence interpolation work, so this half is already good.

This also answers what a bare word means. In an expression-typed field, foo is an identifier and resolves or fails to resolve, with a diagnostic. In a string field it is text. That distinction is currently blurred by the fence doing double duty.

What it would cost

Editions and a rewriter. The fenced form must keep working, so this is edition-gated, and flow fix migrates. That machinery exists and has been exercised repeatedly this week, but flow fix has corrupted valid files three times, so a rewrite touching every expression in every file is the highest-risk rewriter change yet attempted. It would need byte-comparison tests over the whole corpus and a very deliberate rollout.

Editor colour outside our own tooling. Our LSP is schema-aware and can highlight an expression field correctly with no fence. A generic YAML highlighter, including the one rendering these files on GitHub, cannot: the fence is what tells it something special is here. Losing that is a real cost paid by readers who are not in an editor we control, and the mitigation, shipping our own grammar for the common highlighters, is a genuine ongoing commitment rather than a one-off.

Ambiguity for tools that are not us. A .yaml file that is valid YAML but whose scalars are a second language is harder for other people's tooling to reason about. That is already true with fences, but fences at least mark where it happens.

What it would not cost

Nothing in the runtime. The DSL is a facade over the protobuf specification, and the IR already holds parsed expressions rather than source text. A frontend change of this kind does not touch semantics, both drivers, or the plugin boundary, which is also the argument that someone could bring an entirely different frontend, a visual builder or another surface syntax, and produce the same specification. That property is worth protecting for its own sake, and this proposal exercises it.

What to do next

Not build it. Establish whether it is better, with evidence:

  1. Take a representative set of real workflows, including the enterprise examples, and write each one both ways. Compare them as a reader, not as an author. The claim to test is legibility, and legibility is only visible at the scale of a whole file.
  2. Count how many expressions in the corpus would need quotes under the unfenced rule. If it is rare, the rule is easy to teach. If it is common, the unfenced form is a coin flip and the fence is better.
  3. Decide the highlighting question honestly before committing, since it is the cost most likely to be discovered late and resented.
  4. Only then consider an edition, and treat the rewriter as the most dangerous one this project has attempted.

Related

  • #543, the style charter, and its distinction between what is legal, what is canonical, and what we show
  • #413, multi-fence interpolation, which makes the string half of this split work
  • #533, the mapping form, which unified how shaped outputs are written
  • #539, the signal policy rule, whose readability is one of the motivating cases

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with a representative set of real workflows, including the enterprise examples, and write each in fenced and unfenced forms. Count expressions requiring YAML quotes and evaluate whole-file legibility and highlighting; done means producing evidence for a decision before considering an edition or rewriter.

Written by the indexing model from the issue text.

Assessment

Tech stack
yaml
Domain
developer-experience, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.