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:
- Pick a model for that key in Studio, on the entry's Models block.
- Pass a default in the code:
stackbone.models.use('classifier', 'openai/gpt-4o-mini'). - 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/sdkand 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:
modelis optional ondefineDeepAgent, 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: wheredefaultModel, the last fallback, is read from.- Gateway: the one endpoint every resolved id is called through.