# Authoring rich questions

[Documentation index](README.md) · [Result contract](result-contract.md) · [Interaction guide](interaction-and-accessibility.md)

The model normally writes the tool input, but explicit authoring instructions improve forms generated by prompts, extensions, or tests.

## Choose the question type

- Use **`radio`** for exactly one choice.
- Use **`checkbox`** for zero or more choices (set `required: true` when at least one is necessary).
- Use **`text`** for freeform or nuanced input. Text ignores `options` and `allowOther` during normalization.

Group related questions in one `questions` array. Do not turn a simple confirmation into a large form.

## Keep navigation short and content complete

`label` is only a short tab/navigation name. Put the actual question in `prompt`. Put surrounding explanation in `body`, decision context in `context`, a suggested choice and rationale in `recommendation`, and constraints or risks in `caveats`.

For options, keep `label` scannable while putting explanations in `description` and extra detail in `details`. `note` is a **presenter note** supplied by the form author. During the form, the user can add a separate **response note** to a predefined option with `N`; the result preserves both.

## Stable values and behavior

Every question needs an `id`. IDs are normalized to lowercase snake-like text, limited to 80 characters, and deduplicated with numeric suffixes. Supply unique, stable IDs yourself rather than relying on cleanup.

Every option should have a stable `value`. When omitted, its `label` becomes the value. Renaming that label can therefore change downstream semantics.

Defaults are matched against option values or labels:

- `radio`: a string
- `checkbox`: an array of strings
- `text`: a string

Choice questions default to `allowOther: true`; set it to `false` for a closed set. All questions default to `required: true`; set `required: false` to permit an empty answer. `recommended: true` is display metadata, not an automatic selection.

## Good example

```json
{
  "title": "Deployment decision",
  "description": "Choose a target and the checks required before rollout.",
  "questions": [
    {
      "id": "deployment_target",
      "type": "radio",
      "label": "Target",
      "prompt": "Where should the release be deployed first?",
      "context": "Production serves customer traffic; staging mirrors the production configuration.",
      "recommendation": "Start in staging so the smoke test runs before customer exposure.",
      "caveats": "A staging pass does not replace production monitoring.",
      "options": [
        {
          "value": "staging",
          "label": "Staging",
          "description": "Deploy to the internal validation environment.",
          "details": "Run smoke tests and verify migrations before promotion.",
          "recommended": true
        },
        {
          "value": "production",
          "label": "Production",
          "description": "Deploy directly to customer traffic.",
          "note": "Requires the release approver to be present."
        }
      ],
      "allowOther": false,
      "default": "staging"
    },
    {
      "id": "required_checks",
      "type": "checkbox",
      "label": "Checks",
      "prompt": "Which checks must pass before promotion?",
      "options": [
        { "value": "smoke", "label": "Smoke tests", "description": "Exercise critical user paths." },
        { "value": "metrics", "label": "Metrics", "description": "Confirm baseline error and latency rates." }
      ],
      "default": ["smoke", "metrics"],
      "allowOther": true
    },
    {
      "id": "rollback_signal",
      "type": "text",
      "label": "Rollback",
      "prompt": "What observable signal should trigger rollback?",
      "placeholder": "Describe a metric, threshold, and observation window."
    }
  ]
}
```

## Bad example

```json
{
  "questions": [
    {
      "id": "q",
      "type": "radio",
      "label": "Which architecture should we choose given all of the constraints described earlier?",
      "prompt": "Choose one",
      "options": [
        { "label": "A - the recommended architecture with many hidden tradeoffs" },
        { "label": "B" }
      ]
    }
  ]
}
```

This hides the real question in a navigation label, discards the earlier constraints, makes labels do the work of descriptions, omits stable values, and does not explain the recommendation. A self-contained form should remain answerable even when the original chat is not visible.

## Complete input shape

```ts
{
  title?: string;
  description?: string;
  questions: Array<{
    id: string;
    type: "radio" | "checkbox" | "text";
    label?: string;
    prompt: string;
    body?: string;
    context?: string;
    recommendation?: string;
    caveats?: string;
    options?: Array<{
      value?: string;
      label: string;
      description?: string;
      details?: string;
      note?: string;
      recommended?: boolean;
    }>;
    allowOther?: boolean;
    required?: boolean;
    placeholder?: string;
    default?: string | string[];
  }>;
}
```

The TypeBox schema is defined in `extensions/pi-rich-questions/schema.ts`; runtime normalization is in `normalize.ts`. See [Result contract](result-contract.md) before consuming returned indexes or values.
