Decision API
On this page
The decision primitive makes structured decisions without starting an agent conversation.
Provide shared context and named questions; receive typed answers with the
provider’s probabilities, scores, and available usage. Workflow code decides
what to do with those answers.
This is the API reference. Start with the complete classification workflow for a runnable definition and manifest. The sections below cover configuration, request and response schemas, provider differences, and error behavior.
Execution and question helpers
decision is the model-independent name for a structured decision primitive. Supported providers are Jev and local Ollama decision models, tested with Nimble and Clef. Both use the native /v1/systemone API; there is no chat-prompt emulation. Compatible answer shapes do not establish identical capabilities, accuracy or confidence semantics.
import { decision, choice, score, noul } from "@stratoscli/stratos";
Only decision(...) makes an API call. choice(...), score(...), and noul(...) are reusable, pure SDK helpers that construct typed question objects. They do not require generated wrapper files for individual questions. Plain question objects remain valid inputs.
The call follows the existing primitive contract: typed input/output, runtime validation, an execution-context-associated span, and catchable errors. Question builders are not separate execution primitives.
| Builder | Arguments | Answer |
|---|---|---|
choice(instructions, options) |
Instructions and an option-name/description map | Selected option, option probabilities, and confidence |
score(instructions, levels) |
Instructions and an ordered array of at least two level descriptions | Fractional score, level probabilities, legend, and confidence |
noul(instructions?, criteria?) |
Optional instructions and true/false descriptions; provider restrictions apply | Probability of true, from 0 to 1 |
The helpers validate their question shapes. noul does not return a boolean,
and confidence does not measure whether a particular answer is correct.
SDK call
Call decision(options) inside a workflow’s run function. Its exported
DecisionOptions<Q> combines the following fields, and its promise resolves
to DecisionResult<Q>, inferred from the questions.
| Field | Requirement | Meaning |
|---|---|---|
state |
Required | Shared text, object, or array to evaluate |
questions |
Required nonempty map | Names mapped to Choice, Score, or Noul questions |
configId |
Optional | Existing named entry under decision in workflow.yaml |
model |
Required after configuration resolution | Provider’s model ID |
provider |
Defaults to jev |
jev or ollama |
apiKey |
Required for Jev | Environment/file reference, not a literal secret |
baseUrl |
Optional | HTTP(S) API base, without the /v1/systemone suffix |
Supply settings in code or select named configuration as described next. Question helpers are pure and may be used outside execution; the API call requires an active workflow context.
Credential sources
Decision credentials use the same environment/file references as telemetry headers: {env:VARIABLE} resolves an environment variable and {file:path} reads a secret file. Secret files must be readable only by their owner (for example 0400 for owner-read-only, or 0600 when owner writes are also needed); exclude group and other-user access. Keep credentials out of checked-in workflow files, logs, events, traces, and errors.
Named Decision settings live under decision in workflow.yaml. Jev
requires model and apiKey, with apiKey holding a reference rather than
the secret itself:
decision:
classifier:
model: jev-1.13.0
apiKey: "{env:JEV_API_KEY}"
Alternatively, apiKey: "{file:~/.stratos/secrets/jev-key}" reads an owner-protected file. Decision configuration also accepts provider and baseUrl. When omitted, provider defaults to jev. Unsupported providers fail explicitly.
| Provider | Default baseUrl | Credentials |
|---|---|---|
jev |
https://api.typesafe.ai |
Required env/file API-key reference; sent as Bearer authentication. |
ollama |
OLLAMA_HOST, otherwise http://127.0.0.1:11434 |
No key or Authorization header. Inherited Jev references are not resolved or forwarded. |
An explicit baseUrl overrides the provider default. Stratos appends
/v1/systemone; configure a base URL, not that full endpoint. URLs must use
HTTP(S), without embedded credentials, query parameters or fragments.
For Ollama, build checks and runtime both use explicit baseUrl >
OLLAMA_HOST > http://127.0.0.1:11434. Resolve explicit fields with the
normal call-site > invocation > YAML precedence first. An explicit baseUrl
can include a path prefix; OLLAMA_HOST must be an HTTP(S) origin without
credentials, path, query or fragment. Host-only values such as localhost:11435
use HTTP, with port 11434 when omitted. Invalid selected endpoints fail instead
of falling back. Explicit URLs bypass OLLAMA_HOST; Jev never reads it.
Code selects this configuration with
decision({ configId: "classifier", state, questions }). Invocation overrides
use --overrides '{"decision":{"classifier":{...}}}'. Settings resolve field
by field with call site > workflow invocation override > YAML configuration >
defaults. Unspecified fields retain their lower-priority values. The question
examples below use this named classifier configuration.
Missing or rejected Jev credentials raise a catchable error. Handled errors permit workflow recovery; unhandled errors fail the workflow under the normal cleanup rules. References do not provide credential refresh. Telemetry’s soft-error policy does not turn failed model calls into successful calls.
Local Ollama and configuration-only swaps
Run Ollama locally and pull a compatible decision model before invoking Stratos:
ollama pull nimble or ollama pull clef. The endpoint requires Ollama 0.35+;
Clef requires 0.35.1+. Ordinary chat models are not substitutes, and the endpoint
does not support Ollama cloud or MLX/Safetensors models. Stratos neither starts
the service nor downloads models.
decision:
classifier:
provider: ollama
model: nimble
Change only model to clef to use Clef. To switch an existing named Jev
configuration without editing TypeScript or supplying a Jev key, run the
classification example from the composition guide:
stratos run classify-request --input '{"text":"I need help with a refund."}' \
--overrides '{"decision":{"classifier":{"provider":"ollama","model":"nimble"}}}' \
--json
Because merging is field by field, an explicitly configured baseUrl persists
when changing providers. Override it too if it points at the old service.
Call-site settings still win over invocation/YAML. An inherited apiKey
reference remains syntactically validated but is unused by Ollama; switching
back to Jev restores credential resolution.
An explicit stratos build checks declared decision Ollama model IDs against
the configured baseUrl, then OLLAMA_HOST, then the localhost default,
preserving an explicit URL’s path prefix. The installed entry must be local and
advertise decision; it need not advertise the ordinary-agent
completion/tools capabilities. This reads
metadata only; it does not call /v1/systemone. Dynamic call-site/override
settings retain runtime validation. Ordinary agents and
stratos provider status ollama use the same OLLAMA_HOST/localhost fallback.
Request retries
Transient failures allow three retries after the initial attempt, for four attempts maximum, with a fixed one-second delay. HTTP 408, 429, 5xx and transport failures may retry; malformed JSON and response-contract errors do not. Each attempt has a 60-second timeout, not a shared 60-second budget. Exhausted failures raise a catchable error. Workflow cancellation aborts in-flight requests and pending retries and is never retried. These settings are not configurable.
Input schema
The following types describe the request data after SDK settings have been
resolved. This is the provider payload, not a second argument to decision;
the workflow-facing options also include the configuration fields above.
type JsonValue =
| string | number | boolean | null
| JsonValue[]
| { [key: string]: JsonValue };
type Content = string | JsonValue[] | { [key: string]: JsonValue };
type Description = Content | null;
type Question =
| {
type: "choice";
instructions?: Description;
criteria: Record<string, Description>;
}
| {
type: "score";
instructions?: Description;
criteria: readonly [Content, Content, ...Content[]];
}
| {
type: "noul";
instructions?: Description;
criteria?: {
true?: Description;
false?: Description;
} | null;
};
type DecisionInput<Q extends Record<string, Question>> = {
model: string;
state: Content;
questions: Q;
};
state is required and cannot be null or a bare number/boolean. Nested JSON values may contain numbers, booleans, and nulls. An array is one shared state, not an implicit batch of requests. Jev evaluates text/JSON; images, audio, and video are not supported by this integration.
questions must be nonempty. Each question’s map key identifies its answer; it is not an instruction to the model. Independent questions of different types can be included in one call. If a later question needs an earlier answer, workflow code constructs a subsequent call.
| Question | Criteria | Validation |
|---|---|---|
| Choice | Map of option names to descriptions | At least one option; Jev allows at most 255. Descriptions may be text, objects, arrays, or null. |
| Score | Ordered array of level descriptions | Stratos requires at least two levels; Jev allows at most 10. A level is text, an object, or an array, never null. |
| Noul | Optional descriptions of true and false | Either description may be omitted or null; the criteria object itself may be omitted or null. |
For Jev, instructions may be omitted or null on all three question types. Choice’s 255-option and Score’s 10-level maxima are Jev-specific limits, not guarantees about other models. Stratos requires at least two Score levels.
Ollama portability limits
The SDK retains the existing input schema above. Ollama imposes additional native restrictions; valid Jev inputs are not necessarily portable:
| Field | Ollama requirement |
|---|---|
| State/instructions | Nonblank text, object or array. Instructions are required for every question, not omitted/null. |
| Questions | 1-64 questions with nonblank names. |
| Choice | 2-26 options with nonblank names; descriptions are text or null, not objects/arrays. |
| Score | Text levels only. Stratos still permits 2-10 levels, even though Ollama permits up to 26. |
| Noul | Omit criteria or use text descriptions for true/false; avoid Jev’s nullable/structured variants. |
| Request | Text/JSON only in Stratos, at most 64 KiB for Ollama and within the model’s loaded context. |
Use this common subset for configuration-only provider swaps. Stratos sends
validated input unchanged and exposes catchable native HTTP failures; it does
not invent instructions, stringify structured criteria or truncate input.
Ollama’s optional image and keep_alive request fields are not exposed.
Missing models return HTTP 404; unsupported models/input/context return 400;
oversized requests return 413. These errors are not retried. Raw provider error
bodies are omitted, as for Jev.
Output schema and inference
type ChoiceAnswer<K extends string> = {
type: "choice";
choice: K;
probabilities: Record<K, number>;
confidence: number;
};
type ScoreAnswer = {
type: "score";
score: number;
probabilities: Record<string, number>;
legend: Record<string, Content>;
confidence: number;
};
type NoulAnswer = {
type: "noul";
noul: number;
};
The response envelope contains model, answers, and usage: { input_tokens: number; output_tokens: number }, with integer token counts. model identifies the model that answered, which may differ from a requested alias.
Ollama reports input tokens evaluated for scoring, potentially counting shared
context again across questions. Output tokens count internal scoring work, not
the length of the response JSON; zero is valid, as observed with Clef.
Ollama confidence measures distribution concentration (1 - H(p) / ln(N)),
not calibrated correctness. Do not assume a threshold tuned on Jev has the same
behavior on Nimble or Clef. Model loading is included in client invocation
latency and the existing 60-second per-attempt timeout; it is not independently
timed by this integration.
answers preserves each input question’s name and infers its answer kind. Choice preserves the literal option-name union in choice and the corresponding probability keys. Fixed Score rubrics provide their level indices for typed access. Dynamically generated question names remain a string-keyed map with the applicable inferred answer type.
Malformed responses—including missing answers, wrong types, or invalid ranges—raise a catchable validation error immediately, without retrying. This is distinct from the retry policy for transient request failures.
No separate outputSchema argument is needed. Response validation is tied to the submitted questions: answer names and types, Choice option names, Score level keys, and numeric ranges. Validate structure, answer keys/types, option names, and documented numeric ranges. Return provider values unchanged: do not check probability sums, normalize probabilities, or recalculate scores. Workflow authors own those calculations.
- Choice selects one option and retains probabilities for all options. Probability-key ordering need not match the input’s object ordering.
- Score uses zero-based level indices. With N levels,
scoreis a number from 0 to N−1, computed as the probability-weighted average of those indices; it may be fractional.legendpreserves the supplied descriptions, including objects and arrays. JSON level keys are strings. - Noul returns P(true), a number from 0 to 1, without a separate confidence field. It is not automatically converted to a boolean.
- Choice and Score retain both probability distributions and confidence in the 0–1 range. Confidence is distinct from the selected option’s probability and is not a guarantee of correctness. Distributions sum approximately to 1.
Workflow code owns thresholds, score normalization, aggregation, branching, and subsequent agent or MCP calls. No additional execution primitives are required for routing, filtering, ranking, or verification.
Workflow examples
These snippets run inside a workflow’s run function. Inputs, tool results, and handler functions are illustrative local variables/functions, not new Stratos APIs. Thresholds are example workflow policy, not SDK defaults. Each example uses the imports shown above.
Choice: route a request
const { answers } = await decision({
configId: "classifier",
state: input.message,
questions: {
category: choice("What does the user need?", {
billing: "Payments, charges, invoices, or refunds",
technical: "Bugs, crashes, or broken functionality",
other: "Anything else",
}),
},
});
answers.category.choice; // "billing" | "technical" | "other"
answers.category.probabilities.billing; // number
answers.category.confidence; // number
switch (answers.category.choice) {
case "billing": return handleBilling(input);
case "technical": return handleTechnicalIssue(input);
case "other": return handleOtherRequest(input);
}
Handlers are ordinary workflow functions and can invoke MCP tools or agents.
Noul: check evidence before continuing
const { answers } = await decision({
configId: "classifier",
state: { task: input.task, evidence: toolResults },
questions: {
sufficient: noul(
"Does the evidence contain reproduction steps for the reported bug?"
),
},
});
if (answers.sufficient.noul >= 0.9) {
return investigateBug(toolResults);
}
return gatherMoreEvidence(input);
Score: assess severity
const { answers } = await decision({
configId: "classifier",
state: input.bugReport,
questions: {
severity: score("How severe is the reported issue?", [
"Cosmetic issue; functionality works",
"Broken functionality with a workaround",
"Blocked functionality without a workaround",
]),
},
});
answers.severity.probabilities["2"]; // probability of the blocking level
answers.severity.legend["2"]; // that level's description
return {
severity: answers.severity.score, // fractional number from 0 to 2
needsReview: answers.severity.confidence < 0.7,
};
Mixed questions: one call, multiple decisions
const { answers, usage } = await decision({
configId: "classifier",
state: { report: input.bugReport, investigation: toolResults },
questions: {
area: choice("Which area is affected?", {
frontend: "Browser interface or rendering",
backend: "Server processing or APIs",
unknown: "The evidence does not identify the area",
}),
severity: score("How severe is the issue?", [
"Cosmetic issue",
"Broken functionality with a workaround",
"Blocked functionality without a workaround",
]),
reproducible: noul(
"Does the investigation provide concrete reproduction steps?"
),
},
});
if (answers.reproducible.noul < 0.8) {
return gatherMoreEvidence(input);
}
return investigateArea({
area: answers.area.choice,
severity: answers.severity.score,
});
Dynamic questions: filter tool results
if (searchResults.length === 0) {
return { results: [] };
}
// Each item.id is unique within these results.
const questions = Object.fromEntries(
searchResults.map((item) => [
item.id,
noul({
question: "Does this candidate help answer the user's request?",
request: input.request,
candidate: item,
}),
])
);
const { answers } = await decision({
configId: "classifier",
state: { request: input.request },
questions,
});
return {
results: searchResults.filter((item) => answers[item.id].noul >= 0.8),
};
Each candidate receives an independent yes/no probability, allowing multiple candidates or none to qualify. Dynamic IDs cannot be individually autocompleted, but their answer kind remains typed. The empty-input branch avoids an invalid empty question map.
OpenTelemetry instrumentation
Each call records structured inputs and answers, available usage, and outcome. See Telemetry for exporter setup, Langfuse, logs, and trace hierarchy. Decision measurements explains the latency boundaries, token mapping, and confidence limitations.
Provider compatibility and limitations
Stratos uses native Node fetch against Jev or Ollama. Provider input
restrictions differ; use the portability limits above rather than assuming
that another SDK’s types describe Stratos’s accepted inputs. Errors are
catchable and omit raw provider error bodies. model remains required after
configuration merging; apiKey is required only for Jev.
Nimble and Clef support configuration-only workflow swaps on Ollama 0.35.1. They may disagree on classification, confidence and routing even with valid response shapes. Stratos does not hide those differences through input conversion, threshold changes or fallback providers.
References: API, OpenAPI, Score semantics, SDK types.
Ollama references: native API, decision guide, Nimble, Clef.