Skip to main content

Observability API

The Observability API provides methods to retrieve traces, monitor application performance, score traces for evaluation, and record feedback linked to traces and spans.

Getting a specific trace
Direct link to Getting a specific trace

Retrieve a specific trace by its ID, including all its spans and details:

const trace = await mastraClient.getTrace('trace-id-123')

Getting traces with filtering
Direct link to Getting traces with filtering

Retrieve a paginated list of trace root spans with optional filtering:

const traces = await mastraClient.getTraces({
pagination: {
page: 1,
perPage: 20,
dateRange: {
start: new Date('2024-01-01'),
end: new Date('2024-01-31'),
},
},
filters: {
name: 'weather-agent', // Filter by trace name
spanType: 'agent', // Filter by span type
entityId: 'weather-agent-id', // Filter by entity ID
entityType: 'agent', // Filter by entity type
},
})

console.log(`Found ${traces.spans.length} root spans`)
console.log(`Total pages: ${traces.pagination.totalPages}`)

// To get the complete trace with all spans, use getTrace
const completeTrace = await mastraClient.getTrace(traces.spans[0].traceId)

Listing traces for a list view
Direct link to Listing traces for a list view

listTracesLight() returns the same rows as a trace list, without the input, output and attributes payloads. Each row carries a short inputPreview string instead, so a list can render a preview column without transferring the whole prompt.

Prefer it whenever you are rendering many traces at once, and fetch the full record only when a row is opened:

const list = await mastraClient.listTracesLight({
pagination: { page: 0, perPage: 25 },
filters: { entityType: 'agent' },
})

for (const span of list.spans) {
console.log(span.name, span.inputPreview)
}

// Fetch the full payload only for the trace the user selects
const selected = await mastraClient.getTrace(list.spans[0].traceId)

It accepts the same filtering, ordering and delta-polling arguments as listTraces(). Use listTraces() when you actually need the full span payloads.

Querying traces and threads
Direct link to Querying traces and threads

queryTraces() finds completed logical traces using trace fields and conditions over related spans, scores, or feedback. Every query requires an ISO timestamp range of at most 31 days.

const result = await mastraClient.queryTraces({
timeRange: {
from: '2026-08-01T00:00:00.000Z',
to: '2026-08-08T00:00:00.000Z',
},
where: {
scores: {
some: {
op: 'and',
args: [
{ op: 'eq', left: { path: 'scorerId' }, right: { literal: 'factuality' } },
{ op: 'lt', left: { path: 'score' }, right: { literal: 0.6 } },
],
},
},
},
})

Load a page and poll for traces
Direct link to Load a page and poll for traces

queryTraces() supports keyset traversal with page, numbered pages with pagination, and delta polling with mode: 'delta'. Use one mode per request. To migrate from listTracesLight(), read traces instead of spans and retain the numbered page's deltaCursor:

const timeRange = {
from: '2026-08-01T00:00:00.000Z',
to: '2026-08-08T00:00:00.000Z',
}
const initial = await mastraClient.queryTraces({
timeRange,
pagination: { page: 0, perPage: 100 },
})
if (!initial.deltaCursor) throw new Error('Delta polling is unavailable')

const changes = await mastraClient.queryTraces({
timeRange,
mode: 'delta',
after: initial.deltaCursor,
limit: 100,
})

Merge changes.traces by traceId, retain changes.deltaCursor, and continue while changes.delta.hasMore is true. Keep the same time range and predicate across polls. Only completed traces are returned; polling doesn't guarantee notifications for related-record updates, deletions, or traces that stop matching.

See Delta polling for bootstrap behavior, cursor lifetime, retention, and a complete polling loop. queryTraceThreads() remains keyset-only.

Discover trace-query fields and values
Direct link to Discover trace-query fields and values

getTraceQueryFields() returns canonical query fields and observed top-level string metadata fields for one predicate scope. The response includes each field's value kind, supported operators, and whether value suggestions are available.

const timeRange = {
from: '2026-08-01T00:00:00.000Z',
to: '2026-08-08T00:00:00.000Z',
}

const fields = await mastraClient.getTraceQueryFields({
timeRange,
predicateScope: 'spans',
search: 'model',
limit: 25,
})

Use the exact local path from the response with the same scope. For example, model belongs inside a spans.some or spans.none predicate. A global picker can fetch trace, spans, scores, and feedback in parallel.

Call getTraceQueryValues() only after selecting a field with valueSuggestions: true:

const controller = new AbortController()

const values = await mastraClient.getTraceQueryValues(
{
timeRange,
predicateScope: 'spans',
path: 'model',
search: 'claude',
limit: 25,
},
{ signal: controller.signal },
)

// Cancel this autocomplete request when the search text changes.
controller.abort()

Both methods use case-insensitive literal substring search and accept an empty search. Limits default to 25 and can't exceed 100. Results are ordered by occurrence count, then deterministically by path or value. observedFieldsTruncated and valuesTruncated indicate that the caller should refine search. Discovery has no cursor.

Suggestions are bounded and advisory. Manual values remain valid when suggestions are unavailable, empty, or truncated. Discovery doesn't accept a draft query predicate. The client doesn't retry either request, and a per-call AbortSignal takes precedence over the signal configured on MastraClient.

A discovery timeout rejects with 504 TRACE_QUERY_EXECUTION_TIMEOUT. Backend memory or resource exhaustion rejects with 503 TRACE_QUERY_RESOURCE_LIMIT. Neither error contains partial suggestions; truncation flags are only present on successful, completely ranked responses.

queryTraceThreads() returns thread identities derived from observability traces after applying eligibility and cross-trace conditions. It doesn't read or return memory thread records or messages. In this example, one production trace can have the low factuality score while another production trace in the same thread has the clinician correction:

const result = await mastraClient.queryTraceThreads({
traces: {
timeRange: {
from: '2026-08-01T00:00:00.000Z',
to: '2026-08-08T00:00:00.000Z',
},
where: {
op: 'eq',
left: { path: 'environment' },
right: { literal: 'production' },
},
},
where: {
op: 'and',
args: [
{
traces: {
some: {
scores: {
some: {
op: 'lt',
left: { path: 'score' },
right: { literal: 0.6 },
},
},
},
},
},
{
traces: {
some: {
feedback: {
some: {
op: 'eq',
left: { path: 'feedbackType' },
right: { literal: 'clinician-correction' },
},
},
},
},
},
],
},
})

// { threads: [{ threadId: 'thread-123' }], page: { next: null } }

The API limits predicate depth, nodes, related clauses, set members, literal bytes, and the total literal budget before storage execution. Cursor pages are deterministic but aren't a database snapshot, so signals written between requests can change later pages. PostgreSQL and ClickHouse trace and thread queries have a configurable 15-second execution timeout.

See Advanced trace queries for the complete limits, request fields, predicates, thread qualification semantics, cursor pagination, response shapes, and errors.

Deleting traces
Direct link to Deleting traces

Delete traces and their associated spans, metrics, logs, scores, and feedback:

const result = await mastraClient.deleteTraces({
traceIds: ['trace-1', 'trace-2'],
})

console.log(result.success)

Each request accepts up to 1,000 trace IDs and no tenant scope fields. Signals that aren't linked to a trace are preserved. Traces created by experiments are deleted like any other trace.

Scoring traces
Direct link to Scoring traces

Score specific traces using registered scorers for evaluation:

const result = await mastraClient.score({
scorerName: 'answer-relevancy',
targets: [
{ traceId: 'trace-1', spanId: 'span-1' }, // Score specific span
{ traceId: 'trace-2' }, // Score specific span which defaults to the parent span
],
})

Getting scores by span
Direct link to Getting scores by span

Retrieve scores for a specific span within a trace:

const scores = await mastraClient.listScoresBySpan({
traceId: 'trace-123',
spanId: 'span-456',
page: 1,
perPage: 20,
})

Feedback
Direct link to Feedback

Feedback methods create, list, and query human-in-the-loop signals such as ratings, thumbs, comments, and corrections through the target Mastra runtime and its configured observability storage. They don't call the hosted Mastra Platform Feedback API. See the feedback guide for examples and the feedback reference for full schemas.

Creating feedback
Direct link to Creating feedback

Create a feedback record linked to a trace or span:

const response = await mastraClient.createFeedback({
feedback: {
traceId: 'trace-123',
spanId: 'span-456',
feedbackSource: 'user',
feedbackType: 'rating',
value: 1,
comment: 'Helpful answer.',
},
})

Deleting feedback and scores
Direct link to Deleting feedback and scores

Delete feedback or score records by id. Deletion is idempotent, so missing ids are ignored. Each request accepts at most 1,000 feedbackIds or 1,000 scoreIds. Optional organizationId and resourceId fields restrict each deletion to records with matching scope fields:

await mastraClient.deleteFeedback({
feedbackIds: ['feedback-1', 'feedback-2'],
organizationId: 'org-1',
resourceId: 'resource-1',
})

await mastraClient.deleteScores({
scoreIds: ['score-1', 'score-2'],
organizationId: 'org-1',
resourceId: 'resource-1',
})

Listing feedback
Direct link to Listing feedback

Retrieve paginated feedback records with optional filters:

const feedback = await mastraClient.listFeedback({
filters: {
feedbackType: 'rating',
feedbackSource: 'studio',
},
pagination: { page: 0, perPage: 20 },
orderBy: { field: 'timestamp', direction: 'DESC' },
})

filters accepts every FeedbackFilter field. The client sends them as query parameters on GET /api/observability/feedback. See list query parameters.

Aggregating feedback
Direct link to Aggregating feedback

Aggregate numeric feedback values, such as ratings or thumbs encoded as 1 and -1:

const averageRating = await mastraClient.getFeedbackAggregate({
feedbackType: 'rating',
aggregation: 'avg',
comparePeriod: 'previous_day',
})

Grouping feedback
Direct link to Grouping feedback

Group numeric feedback by observability dimensions:

const ratingsByAgent = await mastraClient.getFeedbackBreakdown({
feedbackType: 'rating',
groupBy: ['entityName'],
aggregation: 'avg',
})

Querying feedback over time
Direct link to Querying feedback over time

Bucket numeric feedback by interval:

const ratingsOverTime = await mastraClient.getFeedbackTimeSeries({
feedbackType: 'rating',
interval: '1h',
aggregation: 'avg',
groupBy: ['feedbackSource'],
})

Querying feedback percentiles
Direct link to Querying feedback percentiles

Return percentile series for numeric feedback values:

const ratingPercentiles = await mastraClient.getFeedbackPercentiles({
feedbackType: 'rating',
percentiles: [0.5, 0.95],
interval: '1d',
})