Relationships
#3190 stagecraft: let a method or workflow stage's target use {{name}} placeholders from run data
Opened by skunk-ape · 10/8/2026· Shipped 10/12/2026
Problem
A method stage's target is fixed. work.method.modelIdOrName (and a workflow stage's name under work.workflow) is copied into the dispatch packet verbatim (engine/dispatch.ts), so a definition cannot choose which model or workflow to call from run data.
software-factory allowed this by evaluating ${{ }} in every string, modelIdOrName included, e.g. modelIdOrName: "agent-${{ self.workItem }}". stagecraft rejects ${{ anywhere (see DESIGN.md, "Why software-factory's ${{ }} could not be kept") and fills {{name}} placeholders only in systemPrompt and command. A definition that targets a model per work item has no stagecraft form. #3141 hit this porting a software-factory definition, and a migration to stagecraft hits it too.
Why not placeholders filled from bindings, as first asked
In a workflow or method stage every binding was also sent to the call as an input. A binding used only to name the target would have been sent as --input <name>=..., and swamp refuses a model-method input the method does not declare (Unknown method input(s): ..., for built-in and extension types alike; a workflow with a closed inputs schema refuses it too). So named values and what a call sends are split.
Request (as shipped)
Definition schemaVersion 2:
work.bindingsbecomeswork.let: named bare CEL values, the only place CEL goes.- A
workflowormethodcall sends the let values itspassAsInputslists, under their own names and with their types kept, beside its literalinputs. A let value not listed is never sent. {{name}}fills text fields from let values:systemPrompt,command, and nowworkflow.nameandmethod.modelIdOrName.validatechecks every placeholder and everypassAsInputsname againstlet.- A filled target: every value must be a non-empty string, and the result must be a name swamp would create (swamp's model-name or workflow-name rule, mirrored in
_lib/engine/swamp_names.ts). Otherwise the dispatch fails and records nothing. A literal target is left as written (it may name an older model);validatewarns when it breaks swamp's rule. - The filled target is in the dispatch packet and recorded on the dispatch. The studio's design view shows the template.
- A v1 definition is upgraded on read (#3194): its
bindingsbecomelet, and its call'spassAsInputsnames every binding, so it sends exactly what it sent before. Work items in progress keep running; swamp rewrites factory files onvalidate.
Done
A definition can write modelIdOrName: "agent-{{item}}" with item under work.let, validate checks it, the dispatch packet and the dispatch record carry the filled name, a null value or one swamp would not accept as a name fails the dispatch, and a v1 definition with bindings keeps working unchanged.
Shipped
Click a lifecycle step above to view its details.
skunk-ape commented 10/8/2026, 9:29:25 PM
Carried over from #3194, which adds upgrade-on-read for stored factory definitions (definition_upgrade.ts) and leaves the production chain empty. This issue adds its first real step, v1 to v2. Its plan must include:
- A method-level test: a work item started and pinned at schemaVersion 1, before the change, still dispatches, records and advances after it. #3194 proves this path only with a made-up version 0, at the checkPinned and dispatch level.
- A step in DEFINITION_UPGRADES whose upgradeScenarios also renames binding references in saved scenarios, if any scenario names one.
- Fixtures at testdata/definition-versions/v2/, and every existing .expected.json updated to the v2 form.
- A new entry in the factory type's upgrades (toVersion = the new factory version) calling upgradeFactoryArguments.
See stagecraft/DESIGN.md, 'Format changes upgrade, they never break'.
skunk-ape commented 10/8/2026, 10:18:38 PM
More to fold into this issue, from the #3194 verification reviews. These are low findings, and all of them matter once the first real step ships:
- swamp model validate upgrades the definition (FactoryArgumentsSchema preprocess, upgradeOrPass) but not the saved scenarios. Once a step renames something scenarios refer to, validate would check old scenarios against the new definition. Run upgradeFactoryArguments at the object level, or upgrade scenarios in their own preprocess.
- In upgradeDefinition (definition_upgrade.ts), a step that returns a non-object makes the schemaVersion check throw a plain TypeError that does not name the step. Check the result is an object inside the step guard and raise an UpgradeError naming the step.
- The studio (studio/src/model.ts) matches upgraded scenarios to their file entries by position. A step that drops or reorders scenarios would show them under the wrong names and text. Either require steps to keep scenario order and count (and assert it), or match by scenario name.
- Check the code-review claim that a failed step shows two errors (the step message plus the generic schemaVersion one). A zod check during #3194 showed one, because the preprocess issue stops the literal from being checked. Pin that down with a test.
Sign in to post a ripple.