Feedback
Added in: @mastra/core@1.18.0
Feedback APIs store and query human-in-the-loop observability signals such as ratings, thumbs, comments, and corrections. Use the feedback guide for usage patterns.
Usage exampleDirect link to Usage example
The following example records a rating for a persisted trace through the observability entrypoint. addFeedback() is optional on the entrypoint, so check that the active observability implementation supports it before calling it.
if (!mastra.observability.addFeedback) {
throw new Error('Feedback is not supported by the active observability implementation')
}
await mastra.observability.addFeedback({
traceId: 'trace-123',
spanId: 'span-456',
feedback: {
feedbackSource: 'user',
feedbackType: 'rating',
value: 1,
comment: 'Helpful answer.',
},
})
Create feedbackDirect link to Create feedback
addFeedback(args)Direct link to addfeedbackargs
Adds feedback to a persisted trace or span through the observability entrypoint.
await mastra.observability.addFeedback?.({
traceId: 'trace-123',
spanId: 'span-456',
feedback: {
feedbackSource: 'user',
feedbackType: 'rating',
value: 1,
comment: 'Helpful answer.',
},
})
traceId?:
spanId?:
correlationContext?:
feedback:
Without correlationContext, the method looks up traceId in configured observability storage. If the trace or requested span isn't found, Mastra logs a warning and drops the feedback event. Configure MastraStorageExporter when adding feedback by ID after execution, including when MastraPlatformExporter handles remote export.
createFeedback(args)Direct link to createfeedbackargs
Creates one feedback record through the observability storage domain. Storage-level calls write directly to the store, so include timestamp.
await observability.createFeedback({
feedback: {
feedbackId: 'feedback-1',
timestamp: new Date(),
traceId: 'trace-123',
spanId: 'span-456',
feedbackSource: 'user',
feedbackType: 'rating',
value: 1,
comment: 'Helpful answer.',
},
})
The HTTP and client SDK create route accepts CreateFeedbackBody and sets timestamp server-side. It generates feedbackId when omitted:
await mastraClient.createFeedback({
feedback: {
traceId: 'trace-123',
spanId: 'span-456',
feedbackSource: 'user',
feedbackType: 'rating',
value: 1,
},
})
batchCreateFeedback(args)Direct link to batchcreatefeedbackargs
Creates multiple feedback records through the observability storage domain. This method isn't exposed by the HTTP routes or @mastra/client-js.
await observability.batchCreateFeedback({
feedbacks: [
{
feedbackId: 'feedback-1',
timestamp: new Date(),
traceId: 'trace-123',
feedbackSource: 'user',
feedbackType: 'rating',
value: 1,
},
{
feedbackId: 'feedback-2',
timestamp: new Date(),
traceId: 'trace-123',
feedbackSource: 'qa',
feedbackType: 'comment',
value: 'Needs a citation before shipping.',
},
],
})
Delete feedbackDirect link to Delete feedback
deleteFeedback(args)Direct link to deletefeedbackargs
Deletes up to 1,000 feedback records by id. The operation is idempotent: ids that don't exist are ignored, and an empty feedbackIds array is a no-op. When organizationId or resourceId is provided, only records matching that scope are deleted.
await mastraClient.deleteFeedback({
feedbackIds: ['feedback-1', 'feedback-2'],
})
feedbackIds:
organizationId?:
resourceId?:
Returns Promise<{ success: boolean }> from mastraClient.deleteFeedback() and the HTTP route. The storage domain method returns Promise<void>. Requests with more than 1,000 ids return 400, and a server running @mastra/core older than 1.66.0 returns 501. ClickHouse vNext, PostgreSQL vNext, DuckDB, and the in-memory store implement deletion. Every other adapter, including LibSQL and MongoDB, throws OBSERVABILITY_STORAGE_DELETE_FEEDBACK_NOT_IMPLEMENTED.
On ClickHouse, deletion uses lightweight deletes on the main feedback events table to remove rows from reads, including OLAP queries, without guaranteeing immediate physical removal, so open-source deployments must configure an observability retention period to physically purge them. Each request is marked applied once its delete succeeds; if the delete fails, the request stays unapplied, doesn't block updateFeedbackReviewStatus() on the still-visible feedback, and you retry by calling deleteFeedback() again. updateFeedbackReviewStatus() also retries the delete and throws a not-found error if the retry succeeds, or saves the new status and throws the delete's error if it fails again. Open-source deployments have no background reconciler. ClickHouse applies a TTL to deletion requests only when all five signals have finite retention. See ClickHouse native TTL. Delete APIs leave the separate delta cursor table untouched. Its rows contain identifiers rather than feedback payloads and expire within two days.
deleteScores(args)Direct link to deletescoresargs
Deletes up to 1,000 score records by id. It behaves like deleteFeedback() with scoreIds in place of feedbackIds, and Oracle Database also implements it. Unsupported adapters throw OBSERVABILITY_STORAGE_DELETE_SCORES_NOT_IMPLEMENTED.
await mastraClient.deleteScores({
scoreIds: ['score-1', 'score-2'],
})
List feedbackDirect link to List feedback
listFeedback(args?)Direct link to listfeedbackargs
Returns feedback records in page mode or delta mode.
const response = await mastraClient.listFeedback({
filters: {
feedbackType: 'rating',
feedbackSource: 'studio',
},
pagination: { page: 0, perPage: 20 },
orderBy: { field: 'timestamp', direction: 'DESC' },
})
mode?:
filters?:
pagination?:
page is zero-indexed.orderBy?:
after?:
limit?:
OLAP queriesDirect link to OLAP queries
OLAP feedback queries operate on numeric value fields.
getFeedbackAggregate(args)Direct link to getfeedbackaggregateargs
Returns one aggregate feedback value.
const response = await mastraClient.getFeedbackAggregate({
feedbackType: 'rating',
feedbackSource: 'user',
aggregation: 'avg',
comparePeriod: 'previous_day',
})
feedbackType:
feedbackSource?:
aggregation:
filters?:
comparePeriod?:
getFeedbackBreakdown(args)Direct link to getfeedbackbreakdownargs
Returns feedback values grouped by dimensions.
const response = await mastraClient.getFeedbackBreakdown({
feedbackType: 'rating',
groupBy: ['entityName'],
aggregation: 'avg',
})
getFeedbackTimeSeries(args)Direct link to getfeedbacktimeseriesargs
Returns feedback values bucketed by interval.
const response = await mastraClient.getFeedbackTimeSeries({
feedbackType: 'rating',
interval: '1h',
aggregation: 'avg',
groupBy: ['feedbackSource'],
})
getFeedbackPercentiles(args)Direct link to getfeedbackpercentilesargs
Returns percentile values bucketed by interval.
const response = await mastraClient.getFeedbackPercentiles({
feedbackType: 'rating',
percentiles: [0.5, 0.95],
interval: '1d',
})
TypesDirect link to Types
FeedbackRecordDirect link to feedbackrecord
feedbackId?:
timestamp:
traceId?:
spanId?:
feedbackSource?:
source?:
feedbackSource.feedbackType:
value:
comment?:
feedbackUserId?:
sourceId?:
metadata?:
Shared context fieldsDirect link to Shared context fields
Feedback records can include shared observability context fields for filtering, grouping, and correlation with traces, logs, metrics, and scores.
entityType?:
entityId?:
entityName?:
parentEntityType?:
parentEntityId?:
parentEntityName?:
rootEntityType?:
rootEntityId?:
rootEntityName?:
userId?:
organizationId?:
resourceId?:
runId?:
sessionId?:
threadId?:
requestId?:
environment?:
serviceName?:
scope?:
entityVersionId?:
parentEntityVersionId?:
rootEntityVersionId?:
experimentId?:
executionSource?:
FeedbackInputDirect link to feedbackinput
Use FeedbackInput with mastra.observability.addFeedback(), recordedTrace.addFeedback(), and recordedSpan.addFeedback().
feedbackSource?:
source?:
feedbackSource.feedbackType:
value:
comment?:
feedbackUserId?:
userId?:
feedbackUserId.metadata?:
experimentId?:
sourceId?:
FeedbackFilterDirect link to feedbackfilter
Use FeedbackFilter in listFeedback() and OLAP query filters.
timestamp?:
traceId?:
spanId?:
feedbackType?:
feedbackSource?:
source?:
feedbackSource.feedbackUserId?:
reviewStatus?:
entityType?:
entityName?:
entityVersionId?:
parentEntityType?:
parentEntityName?:
parentEntityVersionId?:
rootEntityType?:
rootEntityName?:
rootEntityVersionId?:
userId?:
organizationId?:
resourceId?:
runId?:
sessionId?:
threadId?:
requestId?:
serviceName?:
environment?:
executionSource?:
experimentId?:
HTTP routesDirect link to HTTP routes
These routes belong to a Mastra runtime and use its configured observability storage. They're separate from the unversioned Mastra Platform Feedback API, which doesn't provide a feedback creation route.
| Method | Path | Purpose | Permission |
|---|---|---|---|
GET | /api/observability/feedback | List feedback records | observability:read |
POST | /api/observability/feedback | Create a feedback record | observability:write |
DELETE | /api/observability/feedback | Delete feedback records by id | observability:delete |
DELETE | /api/observability/scores | Delete score records by id | observability:delete |
PATCH | /api/observability/feedback/:feedbackId/review-status | Update review status | observability:write |
POST | /api/observability/feedback/aggregate | Return one aggregate value | observability:read |
POST | /api/observability/feedback/breakdown | Group feedback by dimensions | observability:read |
POST | /api/observability/feedback/timeseries | Bucket feedback by interval | observability:read |
POST | /api/observability/feedback/percentiles | Return percentile series | observability:read |
List query parametersDirect link to List query parameters
GET /api/observability/feedback takes its arguments as URL query parameters. The Mastra Platform Feedback API accepts the same parameters on its GET /feedback endpoint.
curl -sS "http://localhost:4111/api/observability/feedback?traceId=trace-123&environment=production&feedbackType=rating&page=0&perPage=20"
Every field of FeedbackFilter is accepted as a top-level query parameter with the same name, for example traceId, spanId, feedbackType, feedbackSource, feedbackUserId, reviewStatus, entityName, environment, experimentId, or tags. Repeat a parameter to pass multiple values where the filter accepts an array, such as feedbackType=rating&feedbackType=thumbs. Pass object-valued filters such as timestamp as JSON, for example timestamp={"start":"2026-01-01T00:00:00Z"} URL-encoded.
The remaining parameters control paging and delta polling:
mode?:
page?:
perPage?:
field?:
direction?:
after?:
limit?:
Requests that mix modes return 400, for example page or perPage with mode=delta, or after or limit without it. The analytics routes take the same JSON bodies as getFeedbackAggregate(), getFeedbackBreakdown(), getFeedbackTimeSeries(), and getFeedbackPercentiles().