Feedback and scores

Collect message thumbs and write, read, and inspect named scores for threads, messages, and runs.

Feedback is a user judgement on one stored assistant message. A score is a named numeric, boolean, or categorical value on a message, thread, or run. Assistant Cloud records who authored each value, so feedback from an end user, a dashboard user, an API integration, and an evaluator can be distinguished.

Message feedback

The thread component installed with npx assistant-ui add thread shows thumbs up and thumbs down on assistant messages whenever its runtime has a feedback adapter. Assistant Cloud runtimes provide that adapter. It does not collect a comment. To collect one, add FeedbackDialog with npx assistant-ui add elements-feedback-dialog and pass its note to submitFeedback as comment.

An app that builds its own thread adds ActionBarPrimitive.FeedbackPositive and ActionBarPrimitive.FeedbackNegative to its action bar. The manual submission example below applies to a custom thread. The feedback adapter resolves the cloud thread and message ids before it submits the request.

Submit message feedback
await cloud.threads.messages.feedback(threadId, messageId, {
  type: "positive",
});

Feedback is a boolean score named feedback. A positive rating stores 1 and a negative rating stores 0. There is one feedback score for each user and message, so submitting feedback again updates that score and can flip its value.

Collecting a comment in a custom thread

Use this recipe when a custom thread needs to collect more detail after a thumbs down. It opens a small form and submits the current message's negative rating with the comment.

FeedbackComment.tsx
import { useAui } from "@assistant-ui/react";
import { useState } from "react";

export function FeedbackComment() {
  const aui = useAui();
  const [isOpen, setIsOpen] = useState(false);
  const [comment, setComment] = useState("");

  if (!isOpen) {
    return (
      <button
        type="button"
        aria-label="Leave negative feedback"
        onClick={() => setIsOpen(true)}
      >
        👎
      </button>
    );
  }

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        aui.message.submitFeedback({ type: "negative", comment });
        setIsOpen(false);
      }}
    >
      <label>
        What could be better?
        <textarea
          value={comment}
          onChange={(event) => setComment(event.target.value)}
          maxLength={2000}
        />
      </label>
      <button type="submit">Submit feedback</button>
    </form>
  );
}

The runtime trims the comment and sends none when it is blank. Assistant Cloud accepts 1 to 2,000 characters, and a longer comment receives a 400 response. It stores an accepted comment with the rating and shows it on the thread in the dashboard. An Assistant Cloud server older than the release that added comment rejects the field with a 400, so a self-hosted installation upgrades the server before the client sends comments. Rating again without a comment keeps the stored one; switching between thumbs up and thumbs down clears it. The message's local submittedFeedback metadata mirrors the last submitFeedback call, so after such a repeat it carries no comment while the cloud still holds one.

The client sends the following request. Both path ids accept 1 to 255 characters, and the strict body requires type to be "positive" or "negative" and accepts an optional comment.

Submit feedback
POST /v1/threads/{thread_id}/messages/{message_id}/feedback
Content-Type: application/json

{ "type": "positive" }
StatusBodyWhen
200{ "feedback_id": "score_…", "type": "positive", "comment"?: string | null }The feedback score was stored.
400{ "error": "Schema validation error", "details": […] }The required body failed validation.
404{ "error": "Thread not found" }The thread is not available to the caller.
404{ "error": "Message not found" }The message is not available in that thread.

When a feedback click is skipped

The runtime submits feedback without throwing into the interface. It warns and returns before making a request in these two cases.

ConditionWarning
The thread has no remote id.[assistant-ui] Skipping feedback for message ${message.id}: the thread has no remote id.
The message has no mapped cloud message id.[assistant-ui] Skipping feedback for message ${message.id}: no cloud message id is mapped.

If the request itself is rejected, the adapter logs [assistant-ui] Cloud feedback submission failed:.

Write a score

Use cloud.scores.create to attach a score from your application. The client sends POST /v1/scores.

Create a run score
const score = await cloud.scores.create({
  name: "answer_relevance",
  data_type: "numeric",
  value: 0.82,
  comment: "Evaluated after the response completed",
  run_id: runId,
});

The request body is strict. Unknown fields are refused.

FieldRequiredRules
nameYes1 to 64 characters. It matches ^[A-Za-z0-9_.-]+$.
data_typeYesnumeric, categorical, or boolean.
valueDepends on data_typeRequired and numeric for numeric. For boolean, it accepts true, false, 1, or 0 and is stored as 1 or 0. It is forbidden for categorical.
string_valueDepends on data_typeRequired for categorical, 1 to 255 characters, and forbidden for numeric and boolean.
commentNo1 to 2,000 characters when present.
thread_id, message_id, run_idOne targetEach id is 1 to 48 characters. The target combinations below are the only valid choices.

Every score targets exactly one thing.

TargetRequired fieldsFields that must be absent
Messagemessage_id and thread_idrun_id
Runrun_idthread_id, message_id
Threadthread_idmessage_id, run_id

With no target, the request is refused with "a score must target a message, run, or thread". With an invalid combination, it is refused with "a score must target exactly one message, run, or thread".

StatusBodyWhen
201{ score_id, name, data_type, value, string_value }The score was stored. value is a number or null; string_value is a string or null.
400{ "error": "Schema validation error", "details": […] }A field or data type rule failed validation.
400{ "error": "message_id must belong to thread_id" }The message is not in the supplied thread.
404{ "error": "Thread not found" }The thread target is not available to the caller.
404{ "error": "Run not found" }The run target is not available to the caller.

Source, author, and replacement

The API does not accept a caller supplied source. It records the source and author from the writing path.

SourceWho writes itAuthor
end_userAn end user through message feedback or a JWT authenticated POST /v1/scores request.The authenticated user.
apiA server using an API key with POST /v1/scores.The authenticated API caller.
humanThe dashboard's manual score procedure, including conversation reviews.The viewer who creates the score.
evaluatorAn evaluator rule's verdict.The evaluator rule.

An API key can score any thread in its project. A JWT score write first checks that the user owns the thread. For every path, a write with the same target, name, and author replaces the earlier value and updates its timestamp. The dashboard's manual score procedure records human, uses the viewer as author, and upserts on the most specific target.

Conversation reviews

On the dashboard thread page, a team member can record a verdict for the whole conversation as Good, Needs work, or Unclear, with an optional note. It is stored as a score with name: "review", source: "human", data_type: "categorical", and a string_value of good, needs_work, or unclear. The note is stored in comment. A review targets the thread, with no message or run, and there is one review for each reviewer and thread.

Reviews are returned through the score list endpoints like every other score. Use the name=review filter to list them. An end user's thumb is a feedback score and is never a review.

Read scores

The project read API is API key only. GET /v1/projects/scores returns scores newest first by created_at and id.

List project scores
GET /v1/projects/scores?thread_id={thread_id}&name=answer_relevance&limit=50
curl
curl "https://backend.assistant-api.com/v1/projects/scores?thread_id=thread_0qzof3jPoDwr7K3agyJN3D4U&name=answer_relevance&limit=50" \
  -H "Authorization: Bearer $ASSISTANT_API_KEY" \
  -H "Aui-User-Id: user_123" \
  -H "Aui-Workspace-Id: workspace_acme"
read_scores.py
import os

import requests

headers = {
    "Authorization": f"Bearer {os.environ['ASSISTANT_API_KEY']}",
    "Aui-User-Id": "user_123",
    "Aui-Workspace-Id": "workspace_acme",
}
limit = 50
after = None
while True:
    params = {"name": "answer_relevance", "limit": limit}
    if after:
        params["after"] = after
    response = requests.get(
        "https://backend.assistant-api.com/v1/projects/scores",
        headers=headers,
        params=params,
        timeout=30,
    )
    response.raise_for_status()
    scores = response.json()["scores"]
    for score in scores:
        print(score)
    if len(scores) < limit:
        break
    after = scores[-1]["id"]
QueryRules
name1 to 64 characters.
thread_id, run_id1 to 48 characters.
sinceAn ISO 8601 datetime.
limit1 to 200. Default 50.
afterA score id cursor of 1 to 48 characters.

Each returned score includes its id, target ids, name, data type, numeric and string values, source, author id, comment, and timestamps. This route does not accept a source filter and does not return next_cursor.

The API key only MCP server exposes the same project through list_scores. Its result is { scores, next_cursor }.

MCP inputRules
thread_id, run_idOptional, 1 to 48 characters.
nameOptional, 1 to 64 characters.
sourceOptional: end_user, human, evaluator, or api.
limit1 to 100. Default 50.
cursorOptional cursor of 1 to 512 characters.

Scores in the dashboard

Run on the demo project

The run page has a Scores section with Recorded and Sources, alongside run details, usage, attributes, and the trace waterfall. The thread page reads scores for its thread together with the conversation, turns, satisfaction, and interactions.

Thread on the demo project

Feedback appears under the rated message on the thread page, in the Feedback filter and column of Threads, in the Feedback block at the top of Engagement, and in the Rated threads and Thumbs down tiles of the Overview. For satisfaction, a thumbs rating outranks behaviour. Without a thumbs rating, a copy or a second turn counts as positive, and a stop, regenerate, or edit counts as negative.

The Intelligence page includes Evaluator verdicts. It shows Not judged yet until the next analysis run for sections that do not have a judgement.

Troubleshooting

What you seeWhyWhat to do
A feedback click logs a skip warningThe thread or message has not acquired its cloud id.Wait for the message to be stored, then submit feedback on that stored message.
Feedback answers Thread not found or Message not foundThe path does not identify a message available to the caller.Use the stored thread and message ids for the current caller.
A score request answers a target errorIt supplied no target or more than one target combination.Send a message with its thread, a run alone, or a thread alone.
A categorical score is rejectedIt used value, omitted string_value, or used a label outside the allowed length.Send a 1 to 255 character string_value and omit value.
You need to filter project scores by sourceGET /v1/projects/scores has no source filter.Use the MCP list_scores tool with source.
Evaluator verdicts are not visibleThe section has not been judged yet.Wait for the next analysis run.