Relationships
#3389 literal() output is refused in a direct step globalArgs, so foreign ${{ }} text has no escape there
Opened by zack · 10/13/2026
What happens
A workflow step that runs a model type directly (modelType + modelName with inline globalArgs) cannot use literal() to pass ${{ ... }} text through a global argument. The run fails when the step tries to persist its definition.
task:
type: model_method
modelType: command/shell
modelName: literal-shell
methodName: execute
globalArgs:
run: 'echo MSG=$MSG'
env:
MSG: ${{ literal('${{ github.sha }}') }}swamp workflow run fails with:
Refusing to persist definition 'literal-shell': the following global arguments hold expression text that was not written in the workflow or definition source, so it arrived as data content:
env.MSG: ${{ github.sha }}
Write the expression into the definition itself, or pass the value as a method argument instead of a global argument.Reproduced on release 20261013.163523.0-sha.84a186cd.
Why
literal() evaluates to a string, so its result is data by provenance. The direct-execution persist gate (src/libswamp/models/direct_execution.ts, the "Refusing to persist definition" check) refuses any global argument where expression text sits in data, and does not ask whether that text could ever be swamp's.
Why it matters
literal() is the documented way to keep ${{ ... }} text literal, and several messages point users to it:
- GitHub Actions text that uses a swamp namespace, such as
${{ inputs.version }}or${{ steps.build.outputs.sha }}, is evaluated as swamp's.literal()is the only way to pass it through. - swamp-club#2528 makes
swamp workflow validatefail a root that nearly spells a swamp namespace (input.region) and tells the author to useliteral()if the text is another system's.
In a step's inputs this works. In a direct step's globalArgs it does not, so there is no way to write such text there.
Expected
literal() output reaches a direct step's global argument unchanged.
Design question
The gate exists so data content can never be stored where a later run would evaluate it as an expression (swamp-club#3205). Two possible directions:
- Let the gate accept data text that
isForeignExpressionjudges foreign under the widest scope, since no swamp evaluation could ever own it. This covers${{ github.sha }}but not${{ inputs.version }}, which is the case that most needsliteral(). - Record that a value came from
literal()and store it in a form a later pass will not evaluate.
Found while fixing swamp-club#2528.
In Progress
Click a lifecycle step above to view its details.
Sign in to post a ripple.