Skip to main content

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 usage
Direct 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 install @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 models
Direct 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',
)

Constructor
Direct link to Constructor

new Classifier(options)

id:

string
A non-empty identifier used in tracing and error context.

model:

DecisionModelV4 | EvaluationModelV4 | MastraDecisionModel
An AI SDK decision model or a Mastra decision model wrapper. The model declares which question types it supports.

questions?:

ClassifierQuestions
A non-empty record of named questions. Omit this field to supply the questions to each decide() call instead.

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 types
Direct 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 questions
Direct 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 questions
Direct 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 questions
Direct 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 questions
Direct 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' and partial: '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 other forces every input into a team.
  • Put fields in state, not in instructions. 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:

ClassifierState
The JSON-compatible state evaluated by every question.

questions?:

ClassifierQuestions
The questions to evaluate. Required when the constructor omitted questions.

abortSignal?:

AbortSignal
Cancels the active provider request and any wait between retry attempts.

providerOptions?:

SharedV4ProviderOptions
Provider-specific options forwarded unchanged to the decision model.

maxRetries?:

number
= 2
The number of retries allowed after retryable provider API errors. Must be a non-negative integer.

Per-call questions
Direct 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 value
Direct link to Return value

decide() returns Promise<ClassifierResult<QUESTIONS>>.

answers:

ClassifierAnswers<QUESTIONS>
One typed answer for each configured question.

usage:

ClassifierUsage
Token usage reported by the provider. totalTokens is the sum of inputTokens and outputTokens, with missing values counted as zero.

warnings:

SharedV4Warning[]
Warnings returned by the decision model.

rounding?:

{ probabilityDecimals?: number; scoreDecimals?: number }
The precision the provider used for probabilities and scores.

providerMetadata?:

SharedV4ProviderMetadata
Provider-specific metadata returned by the decision model.

response:

ClassifierResult["response"]
Provider response metadata. Mastra supplies the current time and configured model ID when the provider omits them.
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 errors
Direct link to Validation and errors

Classifier validates inputs before provider input/output and validates provider output before returning it.

Input validation includes:

  • id must 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.
  • maxRetries must 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.

Observability
Direct 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 responses
Direct 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 classifiers
Direct 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.

src/mastra/index.ts
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 workflows
Direct 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.