Feedback
Feedback lets you capture user ratings and comments tied to a trace. VoltAgent can create signed feedback tokens for each trace and attach them to assistant message metadata so you can submit feedback later from the UI.
How it works
- Feedback is always linked to a trace_id.
- When feedback is enabled, VoltAgent requests a short-lived token from the VoltOps API and returns metadata.
- The metadata is added to the last assistant message so you can show a UI control and submit later.
Feedback metadata shape:
{
"traceId": "...",
"key": "satisfaction",
"url": "https://api.voltagent.dev/api/public/feedback/ingest/...",
"tokenId": "...",
"expiresAt": "2026-01-06T18:25:26.005Z",
"provided": true,
"providedAt": "2026-01-06T18:30:00.000Z",
"feedbackId": "feedback-id",
"feedbackConfig": {
"type": "categorical",
"categories": [
{ "value": 1, "label": "Satisfied" },
{ "value": 0, "label": "Unsatisfied" }
]
}
}
Best-practice state model
For production apps, use a hybrid approach:
- Keep feedback records in VoltOps as the source of truth.
- Use message metadata (
provided,providedAt,feedbackId) as fast UI state for hiding feedback controls. - After a successful feedback submit, persist the metadata state on the stored assistant message with
agent.markFeedbackProvided(...).
This keeps UX responsive and prevents feedback controls from reappearing after conversation reloads.
Feedback keys (registry)
Feedback keys let you register a reusable schema for a signal (numeric, boolean, or categorical). The system stores keys per project and uses them to resolve feedbackConfig when you only pass key.
- If a key exists with
feedback_config, it is reused whenfeedbackConfigis omitted. - If a key does not exist and you pass
feedbackConfig, the key is created automatically. - If a key exists, the stored config wins. Update the key if you need to change the schema.
Manage feedback keys in the Console UI under Settings.
Enable feedback in the SDK
You can enable feedback at the agent level or per request. A VoltOps client must be configured (environment or explicit) so tokens can be created.
Agent-level default
import { Agent } from "@voltagent/core";
import { openai } from "@ai-sdk/openai";
const agent = new Agent({
name: "support-agent",
instructions: "Help users solve issues",
model: openai("gpt-4o-mini"),
feedback: true,
});
Per-call feedback options
const result = await agent.generateText("Help me reset my password", {
feedback: {
key: "satisfaction",
feedbackConfig: {
type: "categorical",
categories: [
{ value: 1, label: "Satisfied" },
{ value: 0, label: "Unsatisfied" },
],
},
expiresIn: { hours: 6 },
},
});
const feedback = result.feedback;
If you want to persist feedback-submitted state in memory after ingestion, use the helper on the returned feedback object:
const feedbackId = "feedback-id-from-ingestion-response"; // returned by your feedback ingestion API response
if (result.feedback && !result.feedback.isProvided()) {
await result.feedback.markFeedbackProvided({
feedbackId, // optional
});
}
Use this helper only for memory-backed conversations (requests that include userId and conversationId). If those IDs are missing, messages are not persisted, so there is nothing to mark as provided.
Use a registered key
If the key is already registered, you can omit feedbackConfig and the stored config is used.
const result = await agent.generateText("How was the answer?", {
feedback: { key: "satisfaction" },
});
Streaming feedback metadata
For streaming, VoltAgent attaches feedback metadata to the stream wrapper returned by agent.streamText. The onFinish callback receives the underlying AI SDK StreamTextResult, which does not include VoltAgent feedback metadata. Read feedback from the returned stream wrapper after the stream completes.
const stream = await agent.streamText("Explain this trace", {
feedback: true,
onFinish: async (result) => {
// result is the AI SDK StreamTextResult (no VoltAgent feedback here)
console.log(await result.text);
},
});
for await (const _chunk of stream.textStream) {
// consume stream output
}
console.log(stream.feedback);
Automated feedback with eval scorers
You can run LLM or heuristic scorers and persist the result as feedback without manual fetch calls. The onResult callback receives a feedback helper with feedback.save(...). The key is required and the trace id is taken from the scorer result.
import { Agent, buildScorer } from "@voltagent/core";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const judgeAgent = new Agent({
name: "satisfaction-judge",
model: openai("gpt-4o-mini"),
instructions: "Return JSON with score (0-1), label, and optional reason.",
});
const judgeSchema = z.object({
score: z.number().min(0).max(1),
label: z.string(),
reason: z.string().optional(),
});
const satisfactionScorer = buildScorer({
id: "satisfaction-judge",
label: "Satisfaction Judge",
})
.score(async ({ payload }) => {
const prompt = `Score user satisfaction (0-1) and label it.
User: ${payload.input}
Assistant: ${payload.output}`;
const response = await judgeAgent.generateObject(prompt, judgeSchema);
return {
score: response.object.score,
metadata: {
label: response.object.label,
reason: response.object.reason ?? null,
},
};
})
.build();
const agent = new Agent({
name: "support-agent",
model: openai("gpt-4o-mini"),
eval: {
scorers: {
satisfaction: {
scorer: satisfactionScorer,
onResult: async ({ result, feedback }) => {
await feedback.save({
key: "satisfaction",
value: result.metadata?.label ?? null,
score: result.score ?? null,
comment: result.metadata?.reason ?? null,
feedbackSourceType: "model",
});
},
},
},
},
});
Notes:
- Requires a configured VoltOps client (keys) so feedback can be persisted.
feedback.saveuses the scorer trace id by default; you can override viatraceId.
useChat integration
When you use the /agents/:id/chat endpoint (AI SDK useChat compatible), the assistant message includes feedback metadata under message.metadata.feedback. You can render a thumbs up/down UI and submit feedback to feedback.url.
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
const apiUrl = "http://localhost:3141"; // your VoltAgent server base URL
const agentId = "support-agent-id"; // from route param or app config
const userId = "user-1"; // from your auth/session layer
const conversationId = "conv-1"; // current conversation id from your app state
const transport = new DefaultChatTransport({
api: `${apiUrl}/agents/${agentId}/chat`,
prepareSendMessagesRequest({ messages }) {
const lastMessage = messages[messages.length - 1];
return {
body: {
input: [lastMessage],
options: {
feedback: {
key: "satisfaction",
feedbackConfig: {
type: "categorical",
categories: [
{ value: 1, label: "Satisfied" },
{ value: 0, label: "Unsatisfied" },
],
},
},
},
},
};
},
});
const { messages } = useChat({ transport });
const isFeedbackProvided = (feedback: any): boolean =>
Boolean(feedback?.provided || feedback?.providedAt || feedback?.feedbackId);
const isFeedbackExpired = (feedback: any): boolean =>
typeof feedback?.expiresAt === "string" && new Date(feedback.expiresAt).getTime() <= Date.now();
const shouldShowFeedback = (message: any): boolean => {
const feedback = message?.metadata?.feedback;
if (!feedback?.url) return false;
if (isFeedbackExpired(feedback)) return false;
if (isFeedbackProvided(feedback)) return false;
return true;
};
// Example usage while rendering messages:
// {messages.filter(shouldShowFeedback).map(renderFeedbackButtons)}
async function submitFeedback(message: any, score: number) {
const feedback = message?.metadata?.feedback;
if (!feedback?.url || isFeedbackProvided(feedback)) return;
const response = await fetch(feedback.url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
score,
comment: "Helpful response",
feedback_source_type: "app",
}),
});
if (!response.ok) return;
const { id: feedbackId } = (await response.json()) as { id?: string };
// Persist "already submitted" state for reloads
await markFeedbackProvided({
agentId,
userId,
conversationId,
messageId: message.id,
feedbackId, // optional: feedback id returned by ingestion API
});
}
async function markFeedbackProvided(input: {
agentId: string;
userId: string;
conversationId: string;
messageId: string;
feedbackId?: string;
}) {
// This is a custom app endpoint you implement.
await fetch(`/api/agents/${input.agentId}/feedback/provided`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
}
Persist provided-state on the server
Call agent.markFeedbackProvided(...) after feedback submit succeeds. This is not a built-in HTTP route; expose it from your own backend endpoint.
const updated = await agent.markFeedbackProvided({
userId,
conversationId,
messageId,
feedbackId, // optional
});
This updates the stored assistant message metadata so reloaded conversations still show feedback as completed.
API usage
Use the API directly when you are not calling the SDK or when you want a custom feedback flow.
Create a feedback token
curl -X POST "https://api.voltagent.dev/api/public/feedback/tokens" \
-H "X-Public-Key: $VOLTAGENT_PUBLIC_KEY" \
-H "X-Secret-Key: $VOLTAGENT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"trace_id": "trace-id",
"feedback_key": "satisfaction",
"expires_in": { "hours": 6 },
"feedback_config": {
"type": "categorical",
"categories": [
{ "value": 1, "label": "Satisfied" },
{ "value": 0, "label": "Unsatisfied" }
]
}
}'
Response:
{
"id": "token-id",
"url": "https://api.voltagent.dev/api/public/feedback/ingest/token-id",
"expires_at": "2026-01-06T18:25:26.005Z"
}
If the key is already registered, you can omit feedback_config and the stored config is used.
Submit feedback with the token
curl -X POST "https://api.voltagent.dev/api/public/feedback/ingest/token-id" \
-H "Content-Type: application/json" \
-d '{
"score": 1,
"comment": "Resolved my issue",
"feedback_source_type": "app"
}'
Direct feedback create
If you want to submit feedback directly (without a token), call the feedback endpoint with project keys:
curl -X POST "https://api.voltagent.dev/api/public/feedback" \
-H "X-Public-Key: $VOLTAGENT_PUBLIC_KEY" \
-H "X-Secret-Key: $VOLTAGENT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"trace_id": "trace-id",
"key": "satisfaction",
"score": 0,
"comment": "Did not help"
}'
Custom feedback
Use different keys and configs to collect multiple signals.
const result = await agent.generateText("Review this answer", {
feedback: {
key: "accuracy",
feedbackConfig: {
type: "continuous",
min: 0,
max: 5,
},
},
});
const accuracyFeedback = result.feedback;
You can also use type: "freeform" when you want only text feedback (no score).
Notes
- If VoltOps keys are not configured,
feedbackwill be null. - Tokens expire. Use
expiresAtorexpiresInto control TTL. - Store feedback metadata with your message history so users can rate later.