Documentation source
Exercises
Workshop-style ranking, voting, scoring, assessment, generating, answering — and BuzzFeed-style quizzes — composed onto sessions + responses.
Sprinter exercises are the unified primitive for any structured, multi-step interactive flow that captures responses. The seven kinds today: _ranking, voting, scoring, assessment, generating, answering_, and the newer BuzzFeed-style _quiz_ (cover → multi-step questions → matching loader → archetype reveal). All seven compose onto Sprinter's `sessions`, `views`, `criteria_sets`, and `entity_responses` substrate. No new tables, no new `session_type`. The discriminator is `sessions.metadata.exercise_kind`.
> **Spec & plan:** `documents/work/2026-05-03-exercises-module/spec.md` and `plan.md`. PR1 ships the foundation; PR2 ships the runtime; PR5-slim ships the agent-facilitated demo.
## Experience unification (2026-06-12 update)
> **The ExperienceManifest engine (`features/experiences/`) was removed in PR #2399 (H-PR-6a).** Existing manifest-based data falls back to `mode:"url"` routing via the exercises gallery (`/exercises/`). All new structured input flows — including agent-initiated feedback, quizzes, and intake forms — use the canonical View Designer path: `views.definition` → `BlockHost` → unified submit path.
The unified stack: any interactive flow authors a view in the View Designer (or via `publish_view`), submits through the single `submitInteractionView` path, and materializes responses into the entity graph via `executeViewOnSubmit`. Agent feedback requests use the `request_feedback` tool, which compiles a `FeedbackSpec` into an input view on the fly. See [Tool System — request_feedback](/docs/features/tool-system#request_feedback--agent-initiated-human-input) and [Sessions — Agent Feedback Resume](/docs/features/sessions#agent-feedback-resume).
## Designer and quick generation
The canonical quiz and rich-experience authoring path is now the View Designer. Quiz/experience templates create `exercise-block` views whose authoritative `Quiz` config lives at `exercise-block.config.sourceQuiz`; the dedicated quiz authoring mode reuses the mature step rail, canvas preview, graph mapping, and logic inspectors while saving through normal `views.definition` persistence. Built-in starts include blank quiz, intake-to-protocol, agent feedback, ROI calculator, and lead-magnet assessment patterns.
Data-type admin also has a rich-experience tab at `/admin/data-types/[slug]?tab=experiences`. It quick-generates intake, assessment, and agent-feedback drafts from entity field metadata, shows destination diagnostics so authors can see which answers become fields, criteria, relations, or action inputs, and saves generated drafts as canonical designer views that can open at `/view/<id>/designer`.
The legacy `/exercises/builder` route now redirects authors to the Views hub so
quiz and rich-experience authoring stay on the canonical View Designer path.
## Agent feedback requests
Agent feedback now flows through the `request_feedback` tool: the agent's FeedbackSpec (questions / choose / markup / approve) compiles to a published input View built from standard blocks, and member submissions resume the agent session through the `view_submit` resume executor (`session-elicitation-resume`). See [Tool System — request_feedback](/docs/features/tool-system#request_feedback--agent-initiated-human-input). The legacy `AgentFeedbackRunner` manifest path survives only for the admin rich-experience authoring tab pending its migration (see the convergence work folder's followups).
## DOC360 protocol demonstration
`/demo/rich-experiences` is the public local preflight for the unified loop:
1. generate or select a DOC360-style intake/protocol experience;
2. complete it in the real quiz runner;
3. inspect the response-session graph payload derived from the manifest;
4. view the agent-built patient protocol deliverable;
5. submit a clinician ranking micro-experience that produces an agent resume payload.
The work folder `documents/work/2026-05-24-rich-experiences-unification/` includes the before-system index, after-architecture summary, and repeatable Playwright QA artifact for the desktop/mobile demo.
## Architecture
```
Facilitator (wizard surface) Participants (rank/swipe/form surface)
│ │
▼ ▼
exercise-setup wizard ──────────► Exercise (mixed session)
1. pick kind metadata.exercise_kind
2. attach criteria metadata.target_entity_ids[]
3. select target entities metadata.invited_user_ids[]
4. invite users metadata.timer
5. set timer metadata.target_view_id
│
▼
Per-user response child sessions
(session_type='response', status='draft')
│
▼
entity_responses (source='workshop')
│
▼
computeResponseAggregate / leaderboard
```
The diagram above is the entire module. Boxes on the right side are already-shipped Sprinter primitives — `features/sessions`, `features/views`, `features/responses`. The new code is the LEFT side (wizard + the kind→surface mapping + the registered tool) plus PR4's Inngest timer. Everything else is composition.
## Platform-native pattern: one tool, two callers
`createExercise` is registered as a first-class AI tool in `features/tools/exercise-tools.ts`. The setup wizard's "Create" button and an in-chat agent delegation both invoke that same tool — neither uses a custom code path. The only observable downstream difference is `session_events.metadata.origin`:
| Caller class | `role` | `metadata.origin` | `metadata.agent_slug` |
| ------------------------------- | ------- | ----------------- | --------------------- |
| Wizard / `createExerciseAction` | `user` | `"user"` | `null` |
| Agent in chat / heartbeat | `agent` | `"agent"` | `<slug>` |
This keeps humans + agents truly interchangeable. The PR5 demo ("Run a 10-minute scoring exercise on these 5 concepts with the team") is a tool-call away from PR1's foundation.
## Seven exercise kinds → view surfaces
| Kind | Surface | Participant action |
| ------------ | ------------- | ----------------------------------------------------------- |
| `ranking` | `rank` | Drag the target entity list into preferred order |
| `voting` | `swipe` | Swipe / multi-select yes-no |
| `scoring` | `form` | Score each entity on each criteria dimension |
| `assessment` | `form` | Single-entity multi-criteria scoring |
| `generating` | `form` | Open-ended brainstorm — produce new concepts |
| `answering` | `form` | Free-text answers to facilitator-set questions |
| `quiz` | `quiz-runner` | BuzzFeed-style cover → questions → matching loader → reveal |
The `voting` → `swipe` mapping delivers the OG Trinder pattern as a free byproduct.
## Quiz kind (BuzzFeed-style multi-step flow)
The seventh kind, `quiz`, ships a richer step-type registry than the OG kinds. Where ranking/voting/scoring assume one input shape per session, a quiz walks a sequence of typed steps with their own renderer per type — modeled on lead-magnet "find your X" experiences.
**Where the config lives.** Quiz config (cover, steps, scoring rules, archetypes, reveal template) is stored at `sessions.metadata.quiz_config` JSONB and validated against `QuizSchema` from `features/exercises/lib/quiz/types.ts`. The `ExerciseMetadataSchema.refine()` enforces that `quiz_config` is present when `exercise_kind === "quiz"`.
**Step type catalog (10 types):**
| Step type | Composite | Answer shape | Notes |
| ----------------- | ------------------------------------------------ | ----------------------- | -------------------------------------------------- |
| `cover` | `GlowingOrbHero` | (none) | Hero + Begin CTA. Self-controls Continue. |
| `multi-choice` | `ChoiceCardGrid` (multi) | `string[]` | min/max selection enforcement. |
| `single-choice` | `ChoiceCardGrid` (single) or `PillGroup` (pills) | `string \| null` | Layout-driven composite swap. |
| `slider` | `EmojiSlider` or `SpectrumSlider` | `number` | Emoji morph by bucket when `emojiScale` set. |
| `body-map` | `BodyMapPicker` + chip removal list | `string[]` (zone ids) | Optional by default per design. |
| `lifestyle-grid` | N × `PillGroup` in a responsive grid | `Record<string,string>` | Continue gates on every sub-question answered. |
| `screening` | `ScreeningList` with mutex + severity | `string[]` | "None of the above" mutex; ok/warn/block badges. |
| `matching-loader` | `MatchingLoader` (staged dashes + glowing orb) | (none) | Auto-advances on completion. |
| `reveal` | `ArchetypeReveal` + protocol stats / score card | (none) | Reads `revealResult` (computed by scoring engine). |
| `custom-block` | Placeholder (block-registry lookup is v2) | (depends on block) | Escape hatch for tenant-specific UX. |
**Scoring engine.** `evaluateScoring(config, answers)` is a pure function (`features/exercises/lib/quiz/scoring.ts`). Rules attach to either the quiz (global) or to individual steps (`step.rules`). Each rule is `{ if: AnswerPredicate, then: ScoringEffect[] }`. Effects: `score(archetype, weight)`, `flag(name)`, `branch(toStepId)`, `set(key, value)`. Resolvers: `highest-score` (alphabetical tiebreak), `first-match`, `weighted-mix`. Archetypes map to protocols via `protocolMap` for the canonical DOC'S reveal. Optional `scoring.calculations[]` entries compute calculator outputs from numeric step answers, scenario multipliers, and low/base/high range multipliers without exposing raw answers on the reveal payload.
**Graph mapping.** Each step may carry a `graphMapping` `{ entityType, field, transform }`. On `finalizeQuizSession`, the `promoteAnswers` server action writes the mapped answer back to the linked entity (e.g., `patient.training_load`). This keeps quiz answers inside the entity graph rather than living in an isolated sidecar.
**Runner + frame.** `features/exercises/components/quiz-runner.tsx` is the orchestrator. It picks the right renderer from `features/exercises/lib/quiz/step-renderers/registry.ts`, computes a progress label for visible steps, debounces persistence (`updateQuizSessionAnswer`, 300ms), and on the last non-reveal step evaluates scoring and finalizes. The renderer is mounted inside `QuizFrame` which scopes brand tokens (`docs-light`, `docs360-dark`, `neutral`) as CSS custom properties.
**Runtime palette.** The 10 underlying composites (`GlowingOrbHero`, `BodyMapPicker`, `EmojiSlider`, `SpectrumSlider`, `DualSliderCard`, `ChoiceCardGrid`, `PillGroup`, `ScreeningList`, `MatchingLoader`, `ArchetypeReveal`) live in `lib/runtime/composites/quiz/`. Each is a pure-prop component with a bare-render contract (no provider wrapping required), so they're available to BOTH code-resident step renderers AND agent-authored components via `manageComponent.compile` (per `.claude/rules/agent-components.md`).
**Authoring.** v1 shipped code-resident seeds via the `features/exercises/lib/quiz/seeds/registry.ts`. The DOC'S "Find Your Light" reference seed (`features/custom/tenants/docs/quizzes/find-your-light.ts`) registers under `docs/find-your-light` and is reachable in the browser at `/exercises/quiz-preview/docs/find-your-light` (no-persistence preview mode). Tenant-authored quizzes now author in the View Designer as `exercise-block` views: the designer exposes the same 3-column step rail / canvas / inspector with Graph Mapping + Logic surfaces and persists the source quiz in `views.definition`. The reveal inspector edits result cards and calculator formulas, including output labels, numeric input step ids, scenario multipliers, low/base/high ranges, and display format. The rail Export action serializes the current `sourceQuiz` as a typed code seed module for teams that still need code-resident seeds. Authenticated designer Run creates a quiz response session from `sourceQuiz` and opens `/exercises/[sessionId]`; public Publish redirects to `/quiz/<tenant>/v/<token>`, resolves the full quiz server-side, and sends a sanitized quiz payload to anonymous browsers.
### Reveal CTAs and the lead-capture target
The reveal step renders a row of CTAs declared on `Quiz.reveal.ctaRow`. Each CTA carries an opaque `actionId` resolved per-tenant against the action registry (`features/exercises/lib/quiz/quiz-action-registry.ts`). Four target arms:
| `kind` | Shape | Behavior |
| -------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `navigate` | `{ href }` | Soft Next-router push. |
| `external` | `{ url }` | `window.open` to an external URL. |
| `platform` | `{ actionId }` | Delegate to a platform default (`retake-quiz`, `share-plan`, `copy-link`). |
| `lead-capture` | `{ headline, subhead?, submitLabel?, marketingOptIn?, afterSubmit? }` | Opens `<QuizLeadCaptureSheet>` for name + email + consent capture. |
The lead-capture arm is the bridge between an anonymous quiz attempt and the tenant's entity graph. On submit, `captureQuizLead` (`features/exercises/server/quiz-lead.ts`) — **deprecated, remove-by 2026-07-15; use `captureViewLead` for new views** — reads `session.metadata.quiz_config.lead.entityType` to determine which entity type to create — defaulting to `patient` when absent. The new entity receives `intake_source: "quiz"`, the scoring archetype, the protocol id, consent flag, and the answer flags (via `promoteAnswers`). Idempotent on `content.intake_quiz_session_id` so a refresh or double-tap does not duplicate the row.
**Configurable lead entity type.** Declare `lead: { entityType: "intake" }` at the top of a `Quiz` config to redirect lead capture to a different entity type. The value is server-controlled — it is read from `session.metadata.quiz_config` (a snapshot produced at quiz-session creation from code-resident seeds or designer-published views). Any client-supplied entity type slug is ignored.
After capture, the configured `afterSubmit` target dispatches through the same path as any other CTA, with `{{email}}`, `{{firstName}}`, `{{lastName}}` substitutions URL-encoded in. For DOC'S' `book-first-session`, the `afterSubmit` external URL is sourced from the `NEXT_PUBLIC_DOCS_BOOK_FIRST_SESSION_URL` env var at module load — when set to a valid `https://...` template, it hands the user off to that URL with everything pre-filled; when unset (or set to a non-https URL), the action degrades to capture-only with the inline success card (the lead entity is still created). For `email-plan`, no `afterSubmit` is set — the sheet shows an inline success card and a follow-up Inngest job (planned) emails the matched protocol.
The sheet is themed via the `--quiz-accent` CSS custom property the `QuizFrame` already plumbs through `theme.accent`. DOC'S quizzes' warm-clinical accent reaches the submit button, success check, and form chrome without any tenant-specific overrides on the platform component.
### Reveal template context (public routes)
Public quiz routes (`/quiz/[tenantSlug]/v/[token]`) cannot prop-drill tenant-specific reveal components through `AnonymousQuizClient`. The `RevealTemplateProvider` (`features/exercises/lib/quiz/reveal-template-context.tsx`) solves this by providing a React context that the public route page can populate with a tenant-registered `RevealTemplateRenderer`.
```typescript
// In the public quiz page (or a layout wrapping it):
<RevealTemplateProvider template={tenantRevealTemplate}>
<AnonymousQuizClient ... />
</RevealTemplateProvider>
// Inside the reveal step renderer:
const template = useRevealTemplate(); // null = default ArchetypeReveal
```
Tenant modules register reveal templates under a string key and declare which key a quiz uses via `Quiz.revealTemplateKey`. The quiz runner's reveal step renderer calls `useRevealTemplate()` and delegates to the tenant component when non-null, falling back to the platform `ArchetypeReveal` otherwise.
### Quiz body-map: quiz-authored zone subsets
The `quiz-body-map` step type now accepts a `zones` array in its step config. When present, the `BodyMapPicker` restricts interactive zones to the declared subset rather than rendering all zones from the block manifest. This allows different quizzes to surface different body regions without a new block variant or manifest change. When `zones` is absent the block manifest's full zone list is used, preserving backward compatibility.
### Quiz completion hooks
`features/exercises/server/quiz-completion-hooks.ts` provides a lightweight registry for `onQuizComplete` callbacks keyed by quiz slug. Tenant modules register hooks in their server bootstrap:
```typescript
registerQuizCompletionHook("my-quiz-slug", async (payload) => {
// payload: { sessionId, tenantId, quizConfig, answers, revealResult, leadEntityId? }
});
```
The runner calls all registered hooks for the quiz slug after `finalizeQuizSession`. Hooks run sequentially; a failing hook logs to Sentry but does not block the completion response. This is the recommended seam for post-quiz side-effects (entity creation, status transitions, Inngest dispatch) that belong to custom tenant logic rather than the platform quiz runner.
## PR1 surface
Shipped:
- `features/exercises/types.ts` — `EXERCISE_KINDS`, `ExerciseTimerSchema`, `ExerciseMetadataSchema`, `parseExerciseMetadata`, `EXERCISE_SURFACE_BY_KIND`, `assertNever`.
- `features/exercises/server/create-exercise.ts` — manual-rollback transactional insert (view + session + `session.created` event).
- `features/exercises/server/list-exercises-for-user.ts` — RLS-respecting gallery filter.
- `features/tools/exercise-tools.ts` — `createExercise` registered as an AI tool with `requiredPermission: "entities.team.create"`.
- `app/exercises/page.tsx` — invitee gallery.
- `app/exercises/new/page.tsx` — minimal setup form (the polished 5-step wizard is the iteration-2 follow-up).
## Cross-references
- `content/docs/features/tool-system.mdx` — tool registry and dispatch.
- `content/docs/features/sessions.mdx` — session lifecycle and `session_events`.
- `content/docs/features/view-system.mdx` — surface types (`rank`, `swipe`, `form`).
- `content/docs/features/response-system.mdx` — `entity_responses`, criteria sets, aggregation.