Repository navigation
[feat] Let a workflow choose the model of its prompt steps #1177
Description
Parent: #818
Goal
A prompt step without agent: runs on the tool's default model. A step with one runs on the model of the agent file. Asking for a stronger model on one step means writing an agent file just for that.
Add model to prompt steps, and to the workflow's defaults (#818, decided point 3) for steps that do not choose one. It takes what an agent's model takes: an alias from models/aliases.yaml (#830) or a model name.
defaults:
model: fast # prompt steps where neither the step nor its definition picks a model
steps:
- id: plan
prompt: Plan the change for ${{ inputs.issue }}
model: strong # alias, resolved per tool
- id: review
needs: [plan]
agent: reviewer
model: strong # replaces the model and effort of agents/reviewer
prompt: Review the plan
- id: summary
needs: [review]
prompt: Summarize the review # runs on defaults.modelRules
-
Which model a prompt step runs on, first match wins:
- the step's
model; - the model its definition sets for the tool;
defaults.model;- the tool's default.
defaults.modelfills a gap, as Warp'sagentDefaultsdoes, and never replaces a model a definition chose for its role. To override a definition, setmodelon the step. This keeps Discussion: Workflow Runtime Integration #818's rule that a step's key replacesdefaults(decided point 3), and places the definition between the two. It is the one place wheredefaultsdoes not behave as if the key were written on every step. - the step's
-
defaults.modelfollows the same value rules as a step'smodeland applies only to prompt steps. Command and workflow steps ignore it, and so do scorer steps ([feat] Add workflow scorers and recorded quality labels #1085), which run on the scorer'smodelor the tool's default. An inline sub-workflow carries nodefaults(it carries onlysteps,outputs,endsandloop), so its steps use the enclosing workflow's. A workflow file run as a sub-workflow uses its owndefaults.modelif it sets one, and otherwise the one in effect for the step that runs it, assetupdoes (Discussion: Workflow Runtime Integration #818). -
modelis allowed only onpromptandprompt_filesteps, asagentis (parse.ts:360-365). On another step, pull reportsmodel is only allowed on a prompt or prompt_file stepfor that file and step, and does not cache the new version. -
The value is a non-empty string without spaces. Templates are not rendered in it: a value containing
${{is a validation error. -
The lease carries the values as written. The driver resolves them, because aliases belong to the member and differ per tool.
-
An alias resolves to
{ model, effort }for the tool the step runs on. A value that is not an alias is passed as a model name. -
An alias that gives no model for that tool fails the attempt with code
model_unavailablebefore the tool starts. That covers an alias with no mapping for the tool, and one the member's local aliases turn off (~ordefault). The message names the step, the alias, the tool and the reason, for exampleStep "plan" asks for model alias "strong", which has no model for codex in models/aliases.yaml. Add one or remove model from the step.Running the tool's default instead would record a result for a model the step did not ask for. -
This strict rule applies to a step's
model, and todefaults.modelon a step where it is the first match. Then the message namesdefaultsinstead, for exampleStep "summary" gets model alias "fast" from defaults, which has no model for codex in models/aliases.yaml. Add one, or set model on the step.How aliases resolve for agent definitions and scorers does not change. -
A step
modelreplaces only the definition's model and effort, including those two settings intool_extras. Every other setting of the definition stays in effect. A model name with no effort runs at the tool's default effort, not the definition's, since an effort chosen for another model may not apply. -
When
defaults.modelis the first match, it applies as a stepmodeldoes: the alias's model and effort, or a model name at the tool's default effort. -
The launch must not let the agent file bring back the replaced model or effort. Never edit the member's deployed agent file for this. If a tool cannot run the definition with another model and effort, fail before launch with
model_unavailableand say why. The same applies to a driver that cannot run a step on the model itsmodelordefaults.modelchose, such as one that runs every step in its own session. It does not release the lease, sincenexthands a released step straight back to it. How a driver applies a model that only the definition sets does not change.
Scope
- Parse and validate the key (
STEP_KEYSinparse.ts), with the diagnostics above, anddefaultsas a header key (HEADER_KEYS) holdingmodel. If [feat] Enforce workflow requirements and execution limits #1082 has not addeddefaultsyet, add it here withmodelonly; [feat] Enforce workflow requirements and execution limits #1082 addsrequiresto it. - Add to the lease (
leaseDatainengine.ts) the step'smodelasmodel, and thedefaults.modelin effect for its workflow asdefaultModel, each only when set. A scorer step's lease never carriesdefaultModel. The driver applies the order above, because only the driver can read the definition deployed for its tool. Both are fields the CLI sends, so [feat] Tell a driver which payload fields next and report ignored #1164's lists of payload fields the CLI reads do not change. - Resolve it in the local driver with the member's aliases (
loadModelAliases). Use the alias lookupresolveAgentModel(models/aliases.ts:503) already uses for a definition, exported as a function that takes a model value and a tool. [feat] Add workflow scorers and recorded quality labels #1085 resolves a scorer'smodelthe same way, so both use one function. - Pass the result to the tool (
invocationinlocal-tool-adapter.ts):- Claude:
--model <model>, also when--agentis passed. Effort only if the CLI has a flag for it. - Codex:
--model <model>and-c model_reasoning_effort=<effort>when there is an effort. - OpenCode:
--model <model>. Effort only if the CLI has a flag for it.
- Claude:
- Add
model_unavailableto the recorded failure codes (records.ts). docs/guide/workflow.mdanddocs/guide/zh-CN/workflow.md: addmodelto the step keys table and todefaults, with the rules above.
Acceptance criteria
Checked on the arguments the tool receives, through a fake executable that writes its argv to a file:
- A
modelon a command or workflow step is reported byteamai pullwith the file and step, and that workflow is not cached. model: ${{ inputs.m }}on a step, and the same value indefaults.model, are each reported as a validation error.- On Claude, a prompt step with
model: <alias>launches with--model <the alias's Claude model>. - On Codex, the same step launches with
--model <the alias's Codex model>and-c model_reasoning_effort=<effort>when the alias sets one. - With
agent: reviewer, whose definition sets a model, an effort and unrelated settings such as allowed tools, and a stepmodel, the tool runs with the step's model and resolved effort, not the definition's, and keeps the unrelated settings. - A value that is not an alias is passed unchanged.
- An alias without a model for the step's tool fails the attempt with
model_unavailableand the message above, and the tool is not started. - In a workflow without
defaults.model, a step withoutmodellaunches with the same arguments as before this change. - Real CLI, Claude: a step with
agent: reviewer, whose file names one model, andmodel:naming another, runs on the step's model, as the tool's JSON output reports it. - With
defaults.model: fast, a prompt step withoutagentruns onfast, a step whose definition sets no model for the tool runs onfast, a step whose definition sets a model keeps the definition's, and a step with its ownmodelkeeps its own. The spawned arguments show each.
Test plan
- Parser tests for criteria 1 and 2.
- Driver and adapter tests for 3 to 8 and 10 with the argv-recording executable.
- One real-CLI run for the PR body with Claude for criterion 9, and with Codex if available. Say in the PR which tools were not run.
Watch out for
- Criterion 9 checks that
--modelwins over the agent file on Claude, and the same question applies to effort. If a flag does not win, do not fall back to the file's value: fail withmodel_unavailableon that tool whenagentandmodelare both set, and say so in the docs. resolveAgentModelreturns success with no model for~,default, a tool with no mapping and some model switches (models/aliases.ts). Treat those as no model for a step'smodel, without changing what the function returns for other callers.- [feat] Record the agent each workflow step ran as #1176 records
model.requested,effort.requestedandmodel.aliasfrom what the launch passed. Whichever of the two merges second makes sure the step's model is what it records.
Out of scope
A run-wide model override that beats definitions, a team-wide default model, models for command steps, a default agent, and a model chosen by expression.
Dependencies
- Blocked by: none
- Unblocks: [feat] Run benchmark suites as workflows #1180
Activity
SaulMoro commentedon Oct 16, 2026
Two things from #1184 (for #1176) that this one has to line up with:
observeLaunch(agent-observation.ts) reads models only from the step's definition. For Claude and OpenCode,model.requested,model.aliasandeffort.requestedcome from the pull record'sagentModelsfor the deployed agent file, and only while that file still has its delivered hash. For Codex they come from the definition'smodelthatdefinitionHandoffputs on argv. So adding--modelto the launch is not enough. A stepmodelon Claude would run while the record still names the agent file's model. A step withoutagent:records no requested model at all. HandobserveLaunchthe model and effort the driver resolved for the step, from the same value the launch passes.--output-format jsonnames no model. It has onlymodelUsage, which keys every model the session billed, and a real-psession that ran on sonnet also billed a haiku side call there. feat(workflow): record the agent each step pass ran as #1184 launches Claude with--output-format stream-json --verboseand reads the model from theinitevent, which names the session's model. The run record carries it asagent.model.reportedand the flatmodel, so check criterion 9 there, not inmodelUsage.feat(workflow): let a prompt step choose its model (#1177) (#1192)