Classifier
Classifier asks a decision model one or more fixed-domain questions about shared state. Use it when your application needs a typed choice, numeric score, or boolean probability without parsing generated text.
A classifier only returns evaluation results. Your application decides how to use them, for example by selecting a route or comparing a probability with a safety threshold.
Classifier requires a decision model (DecisionModelV4, or the deprecated EvaluationModelV4, optionally wrapped in MastraDecisionModel). It doesn't accept a 'provider/model' string, a bare model ID, or a language model.
Basic usageDirect link to Basic usage
This example uses the TypeSafe AI provider. Make sure to set the TYPESAFE_AI_API_KEY environment variable. To use another provider, see Decision models.
Install the provider package:
- npm
- pnpm
- Yarn
- Bun
npm install @ai-sdk/typesafe-ai
pnpm add @ai-sdk/typesafe-ai
yarn add @ai-sdk/typesafe-ai
bun add @ai-sdk/typesafe-ai
Define questions in the constructor when every evaluation uses the same question set. Keep the question object literal so TypeScript can infer the answer keys and choice values.
import { Classifier } from '@mastra/core/classifier'
import { typeSafeAi } from '@ai-sdk/typesafe-ai'
const classifier = new Classifier({
id: 'request-classifier',
model: typeSafeAi.decisionModel('jev-latest'),
questions: {
route: {
type: 'choice',
instructions: 'Choose the team that should handle this request.',
criteria: {
billing: 'Questions about invoices, charges, or refunds',
support: 'Questions about using or troubleshooting the product',
},
},
urgent: {
type: 'boolean',
instructions: 'Does this request require an immediate response?',
},
},
})
const result = await classifier.decide({
state: {
subject: 'Duplicate charge',
message: 'I was charged twice for the same subscription.',
},
})
result.answers.route.choice // 'billing' | 'support'
result.answers.urgent.probability // P(true), from 0 to 1
The decision model receives every question with the same state. When a question omits instructions, its question name is used instead.
Decision modelsDirect link to Decision models
The examples on this page follow the TypeSafe AI provider setup in the AI SDK documentation. Other AI SDK providers, including OpenAI, Anthropic, and Google, also expose a decisionModel() factory (previously evaluationModel(), still available as a deprecated alias). Mastra calls doDecide() and falls back to doEvaluate() for older providers. See provider models for the full list, supported model IDs, and how each provider answers questions.
Each provider exports a ready-made instance, which is what the examples on this page use. See AI SDK providers for the others.
Pass a key explicitly by calling the factory instead:
import { createTypeSafeAi } from '@ai-sdk/typesafe-ai'
const model = createTypeSafeAi({ apiKey: process.env.TYPESAFE_AI_API_KEY }).decisionModel(
'jev-latest',
)
ConstructorDirect link to Constructor
new Classifier(options)
id:
model:
questions?:
Constructor questions and per-call questions are mutually exclusive. If the constructor includes questions, decide() rejects a questions option at compile time. If the constructor omits them, every decide() call must provide them. A processor, workflow step, or scorer classifies through a configured classifier, which requires constructor questions.
Question typesDirect link to Question types
Question names become keys in result.answers. Each question supports an optional instructions field containing a JSON-compatible value. Criteria values can also contain JSON-compatible data, not only strings.
Choice questionsDirect link to Choice questions
A choice question selects one key from a non-empty criteria record.
const questions = {
sentiment: {
type: 'choice',
instructions: 'Classify the customer sentiment.',
criteria: {
positive: 'The customer is satisfied or complimentary.',
neutral: 'The customer has no clear positive or negative sentiment.',
negative: 'The customer is dissatisfied or frustrated.',
},
},
} as const
The answer contains the selected choice. When the provider includes probabilities, the record must contain every choice key and sum to 1 within the provider's declared rounding precision.
const answer = {
type: 'choice',
choice: 'negative',
probabilities: {
positive: 0.05,
neutral: 0.15,
negative: 0.8,
},
}
Score questionsDirect link to Score questions
A score question uses an ordered array with at least two levels. Array indexes define the score range. For three criteria, valid scores range from 0 through 2, including fractional values.
const questions = {
quality: {
type: 'score',
instructions: 'Score the response quality.',
criteria: ['Incorrect or unhelpful', 'Partially correct', 'Correct and complete'],
},
} as const
A provider can return a fractional score and an optional probability for each level:
const answer = {
type: 'score',
score: 1.7,
probabilities: {
'0': 0.05,
'1': 0.2,
'2': 0.75,
},
}
Boolean questionsDirect link to Boolean questions
A boolean question returns the estimated probability that the answer is true. The value is always between 0 and 1. It isn't the confidence of whichever outcome is more likely.
const questions = {
unsafe: {
type: 'boolean',
instructions: 'Does the message contain unsafe content?',
criteria: {
true: 'The message contains unsafe content.',
false: 'The message is safe.',
},
},
} as const
The criteria field is optional for boolean questions. When present, its true and false descriptions are also optional.
const answer = {
type: 'boolean',
probability: 0.92,
}
Designing questionsDirect link to Designing questions
These rules keep a question answerable from the state:
- Ask for one judgment. A question that moves when either of two fields changes can't be routed on.
- Keep criteria exclusive.
strong: 'A good fit'andpartial: 'Maybe a fit'leak probability between them. - Give a score enough levels to represent intermediate values.
- Add an escape level when the list isn't closed. Three teams and no
otherforces every input into a team. - Put fields in
state, not ininstructions. One changed field then moves one answer.
To check a question, change one field of the state and confirm one answer moves.
decide(options)Direct link to decideoptions
Answers all questions against the supplied state.
evaluate(options) is a deprecated alias for decide(options) and will be removed in a future release.
state:
questions?:
abortSignal?:
providerOptions?:
maxRetries?:
Per-call questionsDirect link to Per-call questions
Omit questions from the constructor when the question set changes between evaluations. The answer type is inferred from the questions supplied to that call.
import { Classifier } from '@mastra/core/classifier'
import { typeSafeAi } from '@ai-sdk/typesafe-ai'
const classifier = new Classifier({
id: 'dynamic-classifier',
model: typeSafeAi.decisionModel('jev-latest'),
})
const result = await classifier.decide({
state: { response: 'Your refund has been processed.' },
questions: {
tone: {
type: 'choice',
criteria: {
empathetic: 'Acknowledges the customer and responds with care',
neutral: 'States the outcome without emotional language',
},
},
},
})
result.answers.tone.choice // 'empathetic' | 'neutral'
Return valueDirect link to Return value
decide() returns Promise<ClassifierResult<QUESTIONS>>.
answers:
usage:
warnings:
rounding?:
providerMetadata?:
response:
interface ClassifierUsage {
inputTokens?: number
outputTokens?: number
totalTokens: number
}
interface ClassifierResponse {
id?: string
timestamp: Date
modelId: string
headers?: Record<string, string | undefined>
body?: unknown
}
Validation and errorsDirect link to Validation and errors
Classifier validates inputs before provider input/output and validates provider output before returning it.
Input validation includes:
idmust be a non-empty string.state, instructions, and criteria must be JSON-compatible.- The question record and choice criteria must not be empty.
- Score criteria must contain at least two defined levels.
- Every question type must be supported by the decision model.
maxRetriesmust be a non-negative integer.
Provider responses must contain exactly one matching answer for each question. The classifier also rejects invalid choices, out-of-range scores or probabilities, incomplete probability distributions, non-finite token counts, malformed warnings, and invalid response timestamps.
Only retryable AI SDK APICallError failures are retried. Other errors reject immediately. When all retries fail, decide() rethrows the last provider error. Aborting the supplied signal rejects with the signal's abort reason, including when cancellation occurs during retry backoff.
ObservabilityDirect link to Observability
When an active Mastra span exists, decide() creates a CLASSIFIER_EVALUATION child span. It records the classifier and model identifiers, provider, question count and types, attempts, duration, and token usage.
The span doesn't include the evaluated state, question instructions, criteria, answers, probabilities, provider response body, or generated text.
Customize provider responsesDirect link to Customize provider responses
Classifier automatically wraps a raw AI SDK decision model in MastraDecisionModel. Pass your own wrapper when provider output needs normalization before validation.
import { Classifier, MastraDecisionModel, type DecisionModelResult } from '@mastra/core/classifier'
import { typeSafeAi } from '@ai-sdk/typesafe-ai'
class NormalizedDecisionModel extends MastraDecisionModel {
protected override transformResult(result: DecisionModelResult): DecisionModelResult {
return {
...result,
answers: normalizeAnswers(result.answers),
}
}
}
const classifier = new Classifier({
id: 'normalized-classifier',
model: new NormalizedDecisionModel(
typeSafeAi.decisionModel('jev-latest'),
),
})
transformResult() runs after the provider call and before Classifier validates the response.
MastraEvaluationModel and EvaluationModelResult are deprecated aliases for MastraDecisionModel and DecisionModelResult and will be removed in a future release.
Registering classifiersDirect link to Registering classifiers
Register classifiers on the Mastra instance to share them across your application and retrieve them by key or ID. When a registered classifier is evaluated without an active trace, it starts a root CLASSIFIER_EVALUATION span through the Mastra instance's configured observability provider.
import { Mastra } from '@mastra/core'
import { Classifier } from '@mastra/core/classifier'
import { typeSafeAi } from '@ai-sdk/typesafe-ai'
export const safety = new Classifier({
id: 'safety',
model: typeSafeAi.decisionModel('jev-latest'),
questions: {
unsafe: { type: 'boolean' },
},
})
export const mastra = new Mastra({
classifiers: { safety },
})
mastra.getClassifier('safety')
mastra.getClassifierById('safety')
mastra.listClassifiers()
Using classifiers in workflowsDirect link to Using classifiers in workflows
A classifier with constructor-configured questions can be added to a workflow with createStep(classifier) or .classifier(). Workflow output contains complete typed answers and normalized usage.
import { safety } from '../index'
const workflow = createWorkflow({
id: 'ticket-triage',
inputSchema: z.object({ message: z.string() }),
outputSchema: z.any(),
})
.map({ message: { initData: true, path: 'message' } })
.classifier<typeof safety.questions>('safety')
.commit()
A string reference resolves through the Mastra instance at execution time. The ID doesn't carry the question types, so supply them with a type argument to keep the answers typed. In Studio, the workflow graph shows the step as a classifier node.
The workflow adapter doesn't include warnings, raw provider responses, headers, or provider metadata in step output. Use direct decide() calls or classifier observability when you need that provider-level detail.