Relationships
#1837 workflow validate: does not detect guard type mismatch when ${{ inputs.X }} fills the entire field and inputs.X is non-string
Opened by mellens · 8/26/2026· Shipped 8/27/2026
Summary
swamp workflow validate passes cleanly on a workflow where a guard: field
holds ${{ inputs.dryRun }} and dryRun is declared as boolean in the inputs
schema. At runtime the workflow fails because swamp substitutes the actual typed
boolean at parse time, producing guard: true, which fails the z.string()
validation on the guard field.
The information needed to catch this is available statically: the inputs schema
types dryRun as boolean, the guard field is z.string(), and the expression
is a whole-field substitution (not embedded in surrounding text). Validate should
flag this as an error.
Steps to reproduce
- Create a workflow with an input declared
type: boolean(e.g.dryRun). - Add a step with
guard: ${{ inputs.dryRun }}. - Run
swamp workflow validate <name>. It outputsResult: PASSED. - Run
swamp workflow run <name> --input dryRun=true. The workflow fails with a type error on the guard field.
Expected
swamp workflow validate reports an error on the step: "guard received a
boolean via ${{ inputs.dryRun }}; the guard field requires a string (CEL
expression). Use a CEL expression that returns a boolean, e.g.
${{ data.latest(...).?field.orValue("") == "" }}."
Actual
Result: PASSED — 10 checks, all green, including "✓ Step inputs" checks that
demonstrate validate does resolve the inputs schema for other purposes.
Why validate already has the information
validate already resolves the inputs schema — it checks step inputs against
method signatures (e.g. "✓ Step inputs for 'ensure-repo'"). So it isn't
uniformly skipping expression evaluation. For a whole-field ${{ inputs.X }}
substitution, the input type is known, the field type is known, and a mismatch
is statically detectable.
Workaround
Replace the boolean input reference with a data-based CEL expression that evaluates lazily (at step execution time rather than at workflow parse time). For example, instead of:
guard: ${{ inputs.dryRun }}Use a CEL expression over a data artifact that reflects the dry-run outcome:
guard: >-
${{ data.latest("model", "resource-" + inputs.name).?attributes.?field.orValue("") == "" }}This stays as a template string through parse and schema validation, then the CEL evaluator resolves it at step execution time with no type mismatch.
Context
Encountered in a Puddle repo workflow (start-development). The broken guard
shipped on main and caused a partially-executed workflow run against a live
GitHub repo. Fixed in commit 96b7c7f of mesgme/puddle. The false PASSED
from validate was the immediate cause of the confidence that the workflow was safe
to run.
swamp version
$(swamp version 2>&1 | head -1)
Shipped
Click a lifecycle step above to view its details.