Documentation — Choose models for each job
Guides · Models

Choose models for each job

An agent does more than answer: it triages incoming messages, summarises its transcript, checks its goal, approves on your behalf and runs workflow steps. This guide is for builders and operators who want each of those jobs on the model that suits it: a frontier model for the answer, a small one for triage, a reasoning model for hard problems, and a decision model for typed choices. It assumes you have connected your providers (Connect model providers).

Declare your models

Give each model you use a short name under intelligence.models. The name is what the rest of the configuration refers to; the provider only ever sees the wire model. This page uses the same cast throughout:

NameModelKindFor
smartgpt-5.1 (claude-sonnet-5 on Anthropic)chatthe agent's answers
fastgpt-5-mini (claude-haiku-4-5 on Anthropic)chattriage, summaries, cheap steps
deepclaude-opus-5-5reasoningproblems that need thought
jevtypesafe/jev-1.13decisiontyped choices and yes/no questions
intelligence:
  # The top-level connection: OpenAI.
  endpoints: https://api.openai.com/v1/chat/completions
  token: "{{secret:OPENAI_API_KEY}}"

  providers:
    anthropic:
      endpoints: https://api.anthropic.com
      dialect: anthropic
      token: "{{secret:ANTHROPIC_API_KEY}}"
    openrouter:
      endpoints: https://openrouter.ai/api/alpha/decisions
      dialect: decisions
      token: "{{secret:OPENROUTER_API_KEY}}"

  models:
    smart: {model: gpt-5.1, fallback: fast}
    fast:  {model: gpt-5-mini}
    deep:  {model: claude-opus-5-5, provider: anthropic, kind: reasoning, reasoning: {effort: medium}}
    jev:   {model: typesafe/jev-1.13, provider: openrouter, kind: decision, escalate: smart}

  roles:
    turn: smart
    think: deep
    decide: {model: jev, confidence: 0.7}
    preflight: fast
    compaction: fast
    goal: fast
    approve: smart

Each model entry takes model (required), provider, kind, window, fallback, and for its kind reasoning or escalate. A model's window tells compaction the real context size, so it does not guess from the name. Every name is checked at load. Once any model is declared, a role, node or route that names an undeclared one is refused, with the list of models that exist. With no models declared, a role or node names a wire model on the top-level connection directly.

Bind each job to a role

A role is a job agentd does on its own. Bind it to a model by name, or as {model, confidence} for a role that decides:

RoleRunsAcceptsThreshold
turnthe agent's turns, agent/extract/summarize nodes, subagentschat, reasoning—
thinkthe think tool and think nodeschat, reasoning—
decidedecide, classify, route and judge nodes, the decide tool, decision guardsany kindconfidence
preflighttriage of an incoming message before its turnany kindconfidence
compactionsummarising the transcript when it grows too largechat, reasoning—
goalthe goal checkany kindconfidence
approveasks answered on your behalfany kindconfidence

Every role defaults to turn, and no role inherits from another, so binding decide moves only what decides. turn and intelligence.model are the same setting: set one, or the load is refused with say it once. The flag --model sets it too.

A node with no model: runs on its kind's role. A node's own model: wins:

intelligence:
  endpoints: https://api.openai.com/v1/chat/completions
  token: "{{secret:OPENAI_API_KEY}}"
  providers:
    anthropic:
      endpoints: https://api.anthropic.com
      dialect: anthropic
      token: "{{secret:ANTHROPIC_API_KEY}}"
  models:
    smart: {model: gpt-5.1}
    fast:  {model: gpt-5-mini}
    deep:  {model: claude-opus-5-5, provider: anthropic, kind: reasoning}
  roles:
    turn: smart
    think: deep
    decide: fast

workflows:
  - name: support
    steps:
      start:
        kind: once
        inputs: {ticket: "I was charged twice for my invoice."}
      sort:                       # classify runs on roles.decide: fast
        kind: classify
        depends_on: [start]
        input: "{{ inputs.ticket }}"
        classes: [billing, bug, question]
      plan:                       # think runs on roles.think: deep
        kind: think
        depends_on: [sort]
        prompt: "Plan a reply to this ticket: {{ inputs.ticket }}"
        effort: high
      reply:                      # a node's model: wins over its role
        kind: agent
        depends_on: [plan]
        model: fast
        instruction: "Write the reply, following this plan: {{ steps.plan.output }}"
      done:
        kind: finish
        depends_on: [reply]

A subagent runs on the model its spawn names, else its template's, else subagents.defaults.model, else turn. A budget's degrade.model is a model reference too. All of these must be models that write, never a decision model.

Use a reasoning model

A reasoning model thinks before it answers. Declare it kind: reasoning, with default reasoning: {effort, budget}; effort is low, medium (the default) or high. It can serve anywhere a chat model can, and think is the natural role for it: the agent calls the think tool when a problem needs it, and pays for reasoning only then.

An effort: on a think or agent node, or in a think tool call, overrides the model's effort for that call. Naming an effort where the model does not reason is refused, at load for a node and as a tool error for a call.

With Anthropic as your only provider, smart is claude-sonnet-5 and deep thinks adaptively at the effort you set:

intelligence:
  endpoints: https://api.anthropic.com
  dialect: anthropic
  token: "{{secret:ANTHROPIC_API_KEY}}"
  models:
    smart: {model: claude-sonnet-5}
    deep:  {model: claude-opus-5-5, kind: reasoning, reasoning: {effort: high}}
  roles:
    turn: smart
    think: deep

Three things to know:

  • budget is for models that take a thinking budget. It sets the thinking tokens directly (at least 1024). Current Claude models think adaptively and refuse a budget, so leave it out for them.
  • On Bedrock the model id must name its family. agentd sends Claude and OpenAI open-weight models different reasoning fields, so a reasoning model whose id is an application inference profile ARN, which names no family, is refused at load.
  • Thinking costs tokens. agentd raises the call's output cap by the thinking room (budget, else 2048, 8192 or 24576 tokens by effort), and budget admission counts it. See Control spend with budgets.

The model's reasoning stays within the turn that produced it. It is never logged, never stored in the transcript, and never sent to another model.

Use a decision model

A decision model answers typed questions about a state, with calibrated probabilities, and writes no text. It is fast and cheap for the choices an agent makes all day. Put it on a dialect: decisions provider, declare it kind: decision, and bind it to decide:

intelligence:
  endpoints: https://api.openai.com/v1/chat/completions
  token: "{{secret:OPENAI_API_KEY}}"
  providers:
    openrouter:
      endpoints: https://openrouter.ai/api/alpha/decisions
      dialect: decisions
      token: "{{secret:OPENROUTER_API_KEY}}"
  models:
    smart: {model: gpt-5.1}
    jev:   {model: typesafe/jev-1.13, provider: openrouter, kind: decision, escalate: smart}
  roles:
    turn: smart
    decide: {model: jev, confidence: 0.7}

workflows:
  - name: triage
    steps:
      start:
        kind: once
        inputs: {ticket: "The app crashes on login and I cannot work."}
      triage:
        kind: decide
        depends_on: [start]
        state: "{{ inputs.ticket }}"
        questions:
          queue:  {choice: {billing: "payment problems", tech: "bugs and outages"}}
          urgent: {noul: "The customer cannot work at all."}
          mood:   {score: [calm, annoyed, angry]}
        confidence: 0.8
        on_unsure: escalate
      page:
        kind: agent
        depends_on: [triage]
        when:
          noul: "The customer cannot work at all."
          state: "{{ inputs.ticket }}"
        instruction: "Open an incident for: {{ inputs.ticket }}"
      done:
        kind: finish
        depends_on: [page]

A decide node asks up to 16 questions about one state in one call. A choice picks one option, a noul gives the probability that a statement is true, and a score places the state on a scale of levels. On a decision model, classify, route and judge are the one-question forms of the same call. Each answer is logged on an intel.decision line, with its confidence; the state is not logged:

{"event":"intel.decision","model":"typesafe/jev-1.13","escalated":false,
 "answers":{"queue":{"choice":"tech","confidence":0.91,"calibrated":true},
 "urgent":{"noul":0.96,"confidence":0.92,"calibrated":true},
 "mood":{"score":1.4,"level":"annoyed","confidence":0.9,"calibrated":true}},
 "unsure":[], …}

Confidence and escalation

confidence is the least sureness an answer is acted on with. For a noul it is the distance from a coin flip, doubled: a probability of 0.9 has confidence 0.8. A node's confidence wins; without one, the decide role's applies. on_unsure says what an answer under it does, and needs a confidence on the same node:

on_unsureAn unsure answer
escalateis asked once more on the model's escalate model, whose answer is final
failfails the node
continueis handed on, marked unsure: true

The default is escalate when the model names an escalate model, else fail, so a threshold you set is never quietly ignored. on_unsure: escalate on a node whose model names no escalate model is refused at load. A decision model's escalate and fallback may be chat models.

Without a decision model

Every deciding job also runs on a chat model. agentd asks all the questions in one structured call and checks the answer the same way. The confidence is then the model's own report (calibrated: false), and there is nothing to escalate to. So a workflow written against decide works before you add a decision model, and gets cheaper and better calibrated when you do.

The agent reaches the same contract through the decide tool, on the decide role. An unsure answer is escalated when it can be, and otherwise handed back marked unsure for the agent to weigh.

Guard a step with a decision

A step's when: can be a decision instead of a CEL expression, as page shows above. The guard runs as a decide step of its own, page.when, with one question. The guarded step runs only when the statement holds (probability at least 0.5) and the answer is sure; otherwise it is pruned, with whatever depends on it alone.

FieldDefault
noulrequired: the statement the step runs on
statethe outputs of the step's dependencies, by step id; with none, the run's inputs
confidencethe decide role's
modelthe decide role's model

An unsure guard is escalated when the model names an escalate model, and otherwise prunes the step. A guard never fails the run for doubt. A guard whose model is a chat model, such as the turn model when nothing binds decide, is answered through a structured call. The Workflow node reference lists the rules for guards on routed steps and the cases refused at load.

Put the deciding roles on a decision model

preflight, goal and approve decide too, and run natively on a decision model:

agent:
  approval: auto
goal:
  statement: "Every open ticket has a reply."
intelligence:
  endpoints: https://api.openai.com/v1/chat/completions
  token: "{{secret:OPENAI_API_KEY}}"
  providers:
    openrouter:
      endpoints: https://openrouter.ai/api/alpha/decisions
      dialect: decisions
      token: "{{secret:OPENROUTER_API_KEY}}"
  models:
    smart: {model: gpt-5.1}
    jev:   {model: typesafe/jev-1.13, provider: openrouter, kind: decision, escalate: smart}
  roles:
    turn: smart
    preflight: {model: jev, confidence: 0.6}
    goal: {model: jev, confidence: 0.8}
    approve: {model: jev, confidence: 0.9}
RoleOn a decision modelAn unsure answer
preflightthe message's intent, whether it needs a plan, its risk, the route (with routes set), and one yes/no per catalogued skill, as one decisionno verdict: the message is treated as a task
goalachieved and stuck as two statements about the agent's statusneither achieved nor stuck
approvea closed ask (an enum or yes/no answer) as a choice; an open question goes to the escalate model when that model writesthe ask stays undecided

On a decision model, preflight asks about at most 13 skills (12 when routes are set); a preflight.skills_capped line says when more were left out. A plan or clarifying question the message calls for is then the turn's to write, and the turn is told.

approve on a decision model must set a confidence, and an escalated answer is held to it too. Whatever the model, the approve judge never answers an ask whose run, subagent or conversation carries outside input: the ask fails with human.judge.withheld. See Human in the loop.

Route each turn to the right model

intelligence.routes lets preflight pick the model for each message. Name each route's model and when to use it:

agent:
  preflight: always
intelligence:
  endpoints: https://api.openai.com/v1/chat/completions
  token: "{{secret:OPENAI_API_KEY}}"
  providers:
    anthropic:
      endpoints: https://api.anthropic.com
      dialect: anthropic
      token: "{{secret:ANTHROPIC_API_KEY}}"
  models:
    smart: {model: gpt-5.1}
    fast:  {model: gpt-5-mini}
    deep:  {model: claude-opus-5-5, provider: anthropic, kind: reasoning, reasoning: {effort: high}}
  roles:
    turn: smart
    preflight: fast
  routes:
    fast: "a quick answer, a status question or one simple operation"
    deep: "multi-step work, analysis, or anything that needs careful reasoning"

Preflight answers one more question for each message it looks at: which route. The turn that answers that message runs on the chosen model instead of roles.turn, and agentd logs it:

{"event":"turn.routed","ctx":"root","model":"fast", …}
{"event":"turn.model","role":"turn","model":"gpt-5-mini", …}

The turn stays on roles.turn when preflight gives no verdict, when the route's confidence is under the preflight role's (preflight.route_unsure), or when a reload removed the route while the message waited (turn.route_removed). Only that turn moves; the next message is routed on its own.

  • Each route is a declared chat or reasoning model, with a criterion; 32 routes at most.
  • Routes need preflight. With agent.preflight: never they are refused at load. Under the default auto, preflight looks only at long messages, messages that ask for work (build, fix, deploy, review, …) and messages in a conversation with an open plan; the others run on roles.turn. Set always to route every message.
  • The transcript fits the smallest window. Any route may answer the next message, so compaction keeps the transcript within the smallest window among the turn model and the routes. Declare each model's window.

What an instruction document may set

An instruction document can choose which model does what, but never where it lives. A :::!config block may declare models and bind every role except approve:

# Support agent

Answer billing questions from customers.

:::!config
intelligence:
  models:
    fast: {model: gpt-5-mini}
  roles:
    preflight: fast
    compaction: fast
:::

The operator keeps the rest. A document cannot write intelligence.providers, intelligence.routes, roles.approve, endpoints, credentials or headers, and a model it declares cannot name a provider. A document that tries is refused at load, and the message names each path. The approve judge resolves in the operator's configuration, so a document cannot pick the judge of its own gates; only an operator who names no model at all leaves that choice to the document. When the operator names the turn model, in either spelling, the document's is dropped with a warning. See Instruction documents.

Check what you configured

Print the assembled configuration and where each setting came from:

agentd --effective-config --config models.yaml

At run time each turn, workflow step, preflight, compaction, and think or decide call logs its role, wire model and provider on a turn.model line:

{"event":"turn.model","agent_path":"preflight/root","role":"preflight","model":"typesafe/jev-1.13","provider":"openrouter", …}

Roles, models and routes reload live, so you can move a job to another model with SIGHUP and no restart.

Next steps