Skip to main content

Question Card

When the agent needs a decision rather than a guess, it sends a QUESTION template: a card carrying 1–4 multiple-choice questions with 2–4 options each. The user picks, submits, and the SDK folds the answers into text posted as an ordinary next user message.

With the built-in <Chatbot> the card renders itself; there is nothing to wire.

Answering Is Not a Protocol Handshake

The run that produced the card has already finished, so answering takes the plain send-a-message path:

  • The user can ignore the card entirely and type something else — that is the same path, not an error path.
  • There is no "awaiting an answer" pause, and the card does not lock the composer.

The template also carries a text field holding a plain-text rendering of the same questions, so a client that does not know this template falls back to it instead of showing an empty bubble.

Types

interface QuestionOption {
label: string;
description?: string; // what picking it means; may be absent
}

interface Question {
question: string; // full question text; also the key when folding answers
header: string; // short chip label (~12 chars)
multiSelect: boolean;
options: QuestionOption[];
}

interface QuestionMessageTemplate {
type: "QUESTION";
text?: string; // plain-text fallback
questions: Question[];
}
header is not an identity key

header is the short chip label and may repeat across questions. Always match on the full question text. The backend currently sends 1–4 questions with 2–4 options each, but a renderer must not rely on those bounds.

How Answers Are Folded into Text

composeQuestionAnswers(questions, answers) builds the message that gets sent:

import { composeQuestionAnswers } from "@asgard-js/react";

const text = composeQuestionAnswers(questions, { 0: ["PostgreSQL"], 1: ["Authentication", "Observability"] });

The resulting string looks like this:

1. Which storage should this use?

PostgreSQL

---

2. What should ship in v1?

Authentication, Observability
This is a contract with the agent, not display logic

The string has two readers at once: the model, which matches each answer back to the question it asked, and the human, who sees this exact text in the transcript. Every rule serves both:

  • The question text is copied verbatim — it is the model's own string, so matching is unambiguous. header never appears here.
  • Numbering runs consecutively over the submitted questions, not their original indices. A message that opens at "2." reads as though something went missing.
  • A skipped question is omitted whole — no "not answered" placeholder.
  • Blank and whitespace-only picks do not count, so opening the free-text row without typing leaves the question unanswered.
  • Every question skipped returns the empty string — callers treat that as "nothing to send" and keep the submit button disabled.

See Also