stackbone.models

stackbone.models.use(key) returns the model id one named key runs on. Your code names the key; an operator picks the model in Studio. A pick applies to the next call, so changing model is a click and not a rebuild.

Mental model

A model id is a deployment decision, not a product decision. anthropic/claude-haiku-4.5 exists on one provider and not on another, and the operator who configured the model provider is the person who knows which ids that provider serves. So your code declares what the model is for (a key such as summariser or classifier) and the operator decides which model answers.

Surface Who decides the keys Who picks the value
stackbone.models You, in your code. An operator, in Studio.
stackbone.prompts You, in your code. An operator, in Studio.
stackbone.settings The platform. An operator, in the dashboard.

It is the same split as the prompt catalogue, one level down: a prompt is the text a model reads, a model selection is which model reads it.

Each pick belongs to the workflow or the deep agent whose code is running, so the same use('chat') line in two workflows reads two different picks. The surface is agent-local: it reads the agent's own Postgres over the same handle as stackbone.database, and the runtime injects everything the read needs.

Ask for the model a key runs on

import { stackbone } from '@stackbone/sdk';

async function summarise(article: string) {
  'use step';
  const model = await stackbone.models.use('summariser', 'openai/gpt-4o-mini');

  const completion = await stackbone.ai.chat.completions.create({
    model,
    messages: [{ role: 'user', content: article }],
  });
  if (completion.error) throw new Error(completion.error.code);
  return completion.data.choices[0]?.message?.content ?? '';
}

The second argument is your code default: the model this key runs on until somebody picks another one. It is optional. Leave it out when the agent is meant to run in several deployments whose providers serve different ids, and the box decides on its own.

Two rules about the key itself:

  • Write it as a plain string. It starts with a lowercase letter and holds letters, digits, _ and - (up to 128 characters), the same format as a prompt key. A key your code builds at run time still resolves, but the build cannot read it, so Studio has no row for it and nobody can pick a model for it.
  • One key is one address. Calling use('chat') from three places in the same workflow names one model three times, not three models.

The value is read on every call and never cached, so a new pick applies to the next run with no rebuild and no deploy.

The resolution order

use(key, defaultModelId?) tries four things in order and stops at the first one that answers:

# Source Set by
1 The model an operator picked for this key Studio, on the entry's Models block.
2 defaultModelId, the default you passed to use You, in your code.
3 The workspace's defaultModel An operator, on the Workspace settings screen.
4 Nothing resolves, so the call throws Nobody. This is the no_model_configured case.

A deep agent resolves its own model the same way, under the key model, with the model written in defineDeepAgent(...) as step 2. A subagent resolves under subagent_<its name>, and falls back to the model the agent resolved when it names none of its own.

When nothing resolves

use(...) throws, unlike stackbone.prompts.use(...), which returns an empty string. A prompt that cannot be read degrades to no instruction. A model id has no safe empty value, and quietly running the code default after a failed read would run a model the operator deliberately moved away from.

import { stackbone } from '@stackbone/sdk';

async function classify(text: string) {
  'use step';
  let model: string;
  try {
    model = await stackbone.models.use('classifier');
  } catch (error) {
    // `no_model_configured`: no pick, no default in the call, no workspace default.
    if ((error as { code?: string }).code === 'no_model_configured') {
      throw new Error("Pick a model for `classifier` in Studio, on the entry's Models block.");
    }
    throw error;
  }

  const completion = await stackbone.ai.chat.completions.create({
    model,
    messages: [{ role: 'user', content: text }],
  });
  if (completion.error) throw new Error(completion.error.code);
  return completion.data.choices[0]?.message?.content ?? '';
}

To fix no_model_configured, do any one of these:

  1. Pick a model for that key in Studio, on the entry's Models block.
  2. Pass a default in the code: stackbone.models.use('classifier', 'openai/gpt-4o-mini').
  3. Set the workspace Default model under Settings › Workspace, which covers every key that resolves to nothing.

The thrown value is a ModelsError, which the SDK exports, and it carries the same code / message / meta shape as every other error on these surfaces. Its meta names the owner and the key that had no model.

import { ModelsError } from '@stackbone/sdk';

Pick a model in Studio

Open the agent or the workflow in Studio's catalogue. Next to its Prompts block sits a Models block, with one row per model key your code declares:

Row Where the key comes from
model The deep agent's own model.
subagent_<name> One subagent's model, one row each.
Your own key Every stackbone.models.use(key, …) the build found.

Each row shows the default written in the code, the model currently picked, and where a reset lands. Reset deletes the pick, so the key falls back to the code default, or to the workspace default model when the code names none. A row says no model configured when neither side names a model, which is the state that throws at run time.

Three things make a row read-only, and the block says which one applies:

  • The code passes a built model instance rather than an id. That instance carries its own provider and credential, so a picked id has nothing to override.
  • The agent was built with an SDK older than model selections, so it resolves its model from its code alone. Upgrade @stackbone/sdk and rebuild.
  • Your seat has no permission to write configuration. The block still shows what runs.

Picking or resetting an agent's model rebuilds that agent, so the next turn runs on it. A turn already in flight finishes on the model it started with. A workflow needs no rebuild at all, because use(...) reads the pick on every call.

A picked id is not checked against the provider's catalogue when it is saved: the provider may be unreachable while an operator edits. An id the provider does not serve fails the run with the provider's own message, and the box never silently falls back to another model.

Errors

use(...) throws instead of returning the { data, error } envelope the other surfaces use.

Code Means
no_model_configured No pick, no default passed to use, and no workspace defaultModel. Do one of the three things above.
models_unavailable The pick could not be read from the agent's database.
database_not_configured This agent has no Postgres, so there is nowhere to keep a pick.
settings_unavailable The workspace settings read failed while looking for defaultModel.

A database that predates model selections is not an error: it holds no pick, so the call falls through to your code default.

What's next

  • Agents: model is optional on defineDeepAgent, and an agent and its subagents resolve through this same order.
  • stackbone.ai: the client you hand the resolved id to.
  • stackbone.prompts: the same code-declares, operator-decides split, for the text instead of the model.
  • stackbone.settings: where defaultModel, the last fallback, is read from.
  • Gateway: the one endpoint every resolved id is called through.
BUILT WITH ❤️ FROM CANADA AND SPAIN