Skip to content

[feat] Let a workflow choose the model of its prompt steps #1177

Closed

Description

@SaulMoro

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.model

Rules

  • Which model a prompt step runs on, first match wins:

    1. the step's model;
    2. the model its definition sets for the tool;
    3. defaults.model;
    4. the tool's default.

    defaults.model fills a gap, as Warp's agentDefaults does, and never replaces a model a definition chose for its role. To override a definition, set model on the step. This keeps Discussion: Workflow Runtime Integration #818's rule that a step's key replaces defaults (decided point 3), and places the definition between the two. It is the one place where defaults does not behave as if the key were written on every step.

  • defaults.model follows the same value rules as a step's model and 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's model or the tool's default. An inline sub-workflow carries no defaults (it carries only steps, outputs, ends and loop), so its steps use the enclosing workflow's. A workflow file run as a sub-workflow uses its own defaults.model if it sets one, and otherwise the one in effect for the step that runs it, as setup does (Discussion: Workflow Runtime Integration #818).

  • model is allowed only on prompt and prompt_file steps, as agent is (parse.ts:360-365). On another step, pull reports model is only allowed on a prompt or prompt_file step for 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_unavailable before the tool starts. That covers an alias with no mapping for the tool, and one the member's local aliases turn off (~ or default). The message names the step, the alias, the tool and the reason, for example Step "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 to defaults.model on a step where it is the first match. Then the message names defaults instead, for example Step "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 model replaces only the definition's model and effort, including those two settings in tool_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.model is the first match, it applies as a step model does: 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_unavailable and say why. The same applies to a driver that cannot run a step on the model its model or defaults.model chose, such as one that runs every step in its own session. It does not release the lease, since next hands 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_KEYS in parse.ts), with the diagnostics above, and defaults as a header key (HEADER_KEYS) holding model. If [feat] Enforce workflow requirements and execution limits #1082 has not added defaults yet, add it here with model only; [feat] Enforce workflow requirements and execution limits #1082 adds requires to it.
  • Add to the lease (leaseData in engine.ts) the step's model as model, and the defaults.model in effect for its workflow as defaultModel, each only when set. A scorer step's lease never carries defaultModel. 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 lookup resolveAgentModel (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's model the same way, so both use one function.
  • Pass the result to the tool (invocation in local-tool-adapter.ts):
    • Claude: --model <model>, also when --agent is 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.
  • Add model_unavailable to the recorded failure codes (records.ts).
  • docs/guide/workflow.md and docs/guide/zh-CN/workflow.md: add model to the step keys table and to defaults, with the rules above.

Acceptance criteria

Checked on the arguments the tool receives, through a fake executable that writes its argv to a file:

  1. A model on a command or workflow step is reported by teamai pull with the file and step, and that workflow is not cached.
  2. model: ${{ inputs.m }} on a step, and the same value in defaults.model, are each reported as a validation error.
  3. On Claude, a prompt step with model: <alias> launches with --model <the alias's Claude model>.
  4. On Codex, the same step launches with --model <the alias's Codex model> and -c model_reasoning_effort=<effort> when the alias sets one.
  5. With agent: reviewer, whose definition sets a model, an effort and unrelated settings such as allowed tools, and a step model, the tool runs with the step's model and resolved effort, not the definition's, and keeps the unrelated settings.
  6. A value that is not an alias is passed unchanged.
  7. An alias without a model for the step's tool fails the attempt with model_unavailable and the message above, and the tool is not started.
  8. In a workflow without defaults.model, a step without model launches with the same arguments as before this change.
  9. Real CLI, Claude: a step with agent: reviewer, whose file names one model, and model: naming another, runs on the step's model, as the tool's JSON output reports it.
  10. With defaults.model: fast, a prompt step without agent runs on fast, a step whose definition sets no model for the tool runs on fast, a step whose definition sets a model keeps the definition's, and a step with its own model keeps 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 --model wins 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 with model_unavailable on that tool when agent and model are both set, and say so in the docs.
  • resolveAgentModel returns 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's model, without changing what the function returns for other callers.
  • [feat] Record the agent each workflow step ran as #1176 records model.requested, effort.requested and model.alias from 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

Activity

  1. SaulMoro commented on Oct 16, 2026

    @SaulMoro
    CollaboratorAuthor

    Two things from #1184 (for #1176) that this one has to line up with:

    • Where the requested model comes from. observeLaunch (agent-observation.ts) reads models only from the step's definition. For Claude and OpenCode, model.requested, model.alias and effort.requested come from the pull record's agentModels for the deployed agent file, and only while that file still has its delivered hash. For Codex they come from the definition's model that definitionHandoff puts on argv. So adding --model to the launch is not enough. A step model on Claude would run while the record still names the agent file's model. A step without agent: records no requested model at all. Hand observeLaunch the model and effort the driver resolved for the step, from the same value the launch passes.
    • Criterion 9. Claude's --output-format json names no model. It has only modelUsage, which keys every model the session billed, and a real -p session 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 --verbose and reads the model from the init event, which names the session's model. The run record carries it as agent.model.reported and the flat model, so check criterion 9 there, not in modelUsage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions