Stagebook
July 9, 2026 · View on GitHub
Executable study protocols for small group conversation studies. A stagebook defines everything that happens in an experiment - what gets shown to whom, when, and under what conditions - to enable complete documentation and perfect replication.
What is Stagebook?
Stagebook defines a declarative language for specifying interactive group experiments: stages, elements (prompts, surveys, timers, discussion windows), conditional logic, templates, and participant positioning.
This repository provides supporting infrastructure for translating stagebook manifests into automated experiments:
- Zod schemas that validate treatment files and prompt files
- A template engine for parameterized experiment designs with broadcast expansion
- Shared utilities for condition evaluation and reference resolution
- React components that render Stagebook elements into participant-facing UI
Stagebook is platform-agnostic. Define your study protocol once, then run it on any compatible platform.
Try the Viewer — walk through any study from the participant's perspective. Paste a GitHub URL to a treatment file, or explore the built-in examples.
Installation
From GitHub (builds automatically on install):
npm install talkbench/stagebook
Peer dependencies: zod >= 3.23, js-yaml >= 4. React components additionally peer-depend on react >= 18 and react-dom >= 18.
Usage
Validating treatment + prompt files from the command line
The fastest way to check a file is the bundled CLI, which works in any directory — even study repos that have no JS toolchain at all (Node is the only requirement):
# One file
npx --package=stagebook stagebook validate study.stagebook.yaml
# Mixed inputs (treatments + prompts) + globs
npx --package=stagebook stagebook validate \
'stagebook/**/*.stagebook.yaml' prompts/intro.prompt.md
# Stdin (for agents validating buffered content before writing)
cat study.stagebook.yaml | \
npx --package=stagebook stagebook validate --type=treatment -
# Machine-readable for CI / agents
npx --package=stagebook stagebook validate --format=json study.stagebook.yaml
Default is expand-and-validate: the validator expands templates + resolves imports: before checking the schema, so errors that only appear after template substitution are caught. --no-expand skips that for faster pre-expansion checks.
Exit codes: 0 clean (warnings OK), 1 schema errors, 2 couldn't read a file / YAML unparseable / glob matched nothing (use --allow-empty to opt out of the last).
Diagnostics match what the VS Code extension shows in its Problems panel — same text, positions, severities — so an editor user and a CI bot see the same errors. The extension isn't on the Marketplace yet; install it from the latest release with a one-line curl — see apps/vscode/README.md.
Validating from TypeScript
For programmatic access (e.g. building tooling), import the validators directly:
import { treatmentFileSchema } from "stagebook";
import { load as loadYaml } from "js-yaml";
const config = loadYaml(yamlString);
const result = treatmentFileSchema.safeParse(config);
if (!result.success) {
console.error(result.error.issues);
}
For rich diagnostics with source positions (the format used by the editor and CLI), import from the validate subpath:
import {
validateTreatmentSource,
validatePromptSource,
type Diagnostic,
} from "stagebook/validate";
const { diagnostics } = validateTreatmentSource(yamlString);
for (const d of diagnostics) {
console.log(`${d.severity}: ${d.message} (line ${d.range?.startLine})`);
}
The same subpath exports checkPairing(file, { introSequenceName }, treatmentNames) — a launch-time guard hosts call where batch config pairs an intro sequence with treatments (pass introSequenceName: null for intro-less launches) — and getRequiredServices(file, { loadPrompt }), a provisioning primitive that walks an expanded treatment and reports which external services it needs (coedit, video, textChat, externalSurvey) — keyed by arm (overall, byTreatment, byIntroSequence, byConsent) so a host can provision exactly what the selected launch uses. See the API reference and integration guide.
Validating a prompt file
promptFileSchema takes raw markdown, parses it, and validates structure, metadata, response format, and slider labels in a single pass:
import { promptFileSchema } from "stagebook";
const result = promptFileSchema.safeParse(markdownString);
if (result.success) {
const { metadata, body, responseItems } = result.data;
// metadata: parsed and validated YAML frontmatter
// body: the prompt text
// responseItems: parsed response options (prefix-stripped)
} else {
console.error(result.error.issues);
}
Evaluating conditions
import { compare } from "stagebook";
compare(5, "isAbove", 3); // true
compare("hello", "includes", "ell"); // true
compare(undefined, "exists"); // false
compare(undefined, "doesNotEqual", "x"); // true
The 16 canonical comparators: exists, doesNotExist, equals, doesNotEqual, isAbove, isBelow, isAtLeast, isAtMost, hasLengthAtLeast, hasLengthAtMost, includes, doesNotInclude, matches, doesNotMatch, isOneOf, isNotOneOf.
Parsing reference strings
Every reference begins with a position selector — self, shared, all, or a non-negative integer slot index (#298). The selector becomes part of the parsed ReferenceType; getReferenceKeyAndPath strips it to return the storage key and path:
import { getReferenceKeyAndPath } from "stagebook";
getReferenceKeyAndPath("self.survey.bigFive.result.score");
// { referenceKey: "survey_bigFive", path: ["result", "score"] }
getReferenceKeyAndPath("self.prompt.myQuestion");
// { referenceKey: "prompt_myQuestion", path: ["value"] }
Un-prefixed strings ("survey.bigFive.result.score") throw at parse time with an error suggesting the migration.
Supported namespaces: survey, submitButton, qualtrics, prompt, trackedLink, timeline, discussion, entryUrl, attributes. (urlParams was renamed to entryUrl in #246; connectionInfo / browserInfo / participantInfo were merged into a single flat attributes source in #473.) entryUrl references must use the params subpath, e.g. getReferenceKeyAndPath("self.entryUrl.params.foo").
Expanding templates
import { fillTemplates } from "stagebook";
const result = fillTemplates({
obj: treatmentConfig,
templates: treatmentConfig.templates,
});
The template engine supports field substitution (${fieldName}), nested templates, and multi-dimensional broadcast expansion.
API Reference
Schemas
| Export | Description |
|---|---|
treatmentFileSchema | Top-level schema for a treatment YAML file (templates, consent, introSequences, treatments) |
treatmentSchema | Single treatment with playerCount, compatibleIntroSequences, gameStages, exitSequence |
consentArmSchema | Single named consent arm with its own locale and steps (#481) |
consentSchema | Top-level consent: array of arms — the host selects one by name |
stageSchema | Game stage with name, duration, elements, discussion; validates element time bounds against duration |
elementSchema | Any DSL element (prompt, display, survey, timer, etc.) with conditional rendering support |
promptSchema | Prompt element with file reference and optional shared flag |
discussionSchema | Discussion config (chat type, layout, rooms, visibility) |
conditionSchema | Condition with reference, comparator, value, and position |
referenceSchema | DSL reference string validator |
promptFileSchema | Parses and validates a complete prompt markdown file |
metadataTypeSchema | Prompt metadata field types and constraints |
metadataRefineSchema | Cross-field metadata validation (e.g., slider requires min/max/interval) |
templateContextSchema | Template reference with fields and broadcast dimensions |
templateSchema | Named template definition with content type |
All schemas export corresponding TypeScript types (e.g., TreatmentType, StageType, ElementType).
Utilities
| Export | Description |
|---|---|
compare(lhs, comparator, rhs?) | Evaluate a condition. Returns boolean | undefined |
Comparator | String literal union type of the 16 canonical comparator names |
getReferenceKeyAndPath(reference) | Parse a DSL reference string into storage key + nested path |
getNestedValueByPath(obj, path?) | Traverse a nested object by path array |
Templates
| Export | Description |
|---|---|
fillTemplates({ obj, templates }) | Expand all template references and validate no placeholders remain |
expandTemplate({ templates, context }) | Expand a single template context with fields and broadcast |
substituteFields({ content, fields }) | Replace ${key} placeholders with values |
Documentation
For Researchers (designing experiments)
- Treatment Files — how to structure a
.stagebook.yamlfile - Page Elements — all element types and their options
- Prompt Files — markdown format for prompts, sliders, surveys
- Conditions & References — conditional display and data references
- Dispatchers — assigning groups to treatments (uniform, weighted, urn, softmax-knockdown)
- Discussions — text chat, video calls, breakout rooms, custom layouts
- Templates — reusable structures with field substitution and broadcast
- Syntax Reference — compact cheat sheet for the full language
For Engineers (integrating Stagebook)
- Integration Guide — implementing a StagebookProvider backend
- Platform Requirements — what the host platform must provide (state, orchestration, group formation, services)
- API Reference — all exports, types, and component props
- Architecture — StagebookProvider design, three-layer component model, render slots, CSS theming
Styling and fonts
Stagebook components ship their visual styling as inline styles backed by CSS
custom properties, so they render correctly on any host. Importing the optional
stylesheet adds the --stagebook-* variable defaults at :root and registers
the Inter webfont:
import "stagebook/styles";
Inter is bundled inside the package (OFL-1.1) rather than fetched from a
CDN, so every participant gets the same font regardless of network conditions —
the measurement-instrument guarantee. When you import stagebook/styles through
a bundler with built-in asset handling (Vite, webpack), the font's relative
url() is resolved automatically; no consumer action is needed.
esbuild: esbuild has no preconfigured loader for
.woff2, so an app that bundlesstagebook/styleswith plain esbuild fails on the font url until you add a font loader, e.g.--loader:.woff2=file(ordataurl/copy). This is the one bundler that needs a one-line config to consume the bundled font.
If your host serves the stylesheet statically (a raw <link> to the file,
or a copy step into a public/ dir) the relative url() won't resolve, because
nothing rewrites it. For that case — or if you maintain your own typography and
don't import our CSS at all — resolve the bundled file from the stable subpath
export, copy it into your own static assets, and point your @font-face at
your path:
// In a build/copy step: resolve the bundled file through package exports,
// then copy it into, e.g., public/fonts/InterVariable.woff2.
// ESM — import.meta.resolve() consults the package "exports" map:
import.meta.resolve("stagebook/assets/InterVariable.woff2");
// ESM without import.meta.resolve (older runtimes):
import { createRequire } from "node:module";
createRequire(import.meta.url).resolve("stagebook/assets/InterVariable.woff2");
// CommonJS:
require.resolve("stagebook/assets/InterVariable.woff2");
Note:
new URL("stagebook/assets/…", import.meta.url)does not work here —new URLdoes plain URL resolution and treats the bare specifier as a path relative to the current module, ignoring packageexports. Use a real resolver (import.meta.resolve/require.resolve) as above.
/* @font-face pointing at the copy you placed in your own static dir. */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("/fonts/InterVariable.woff2") format("woff2");
}
A bare package specifier inside a CSS url() — url("stagebook/assets/…") —
is not a portable shortcut: browsers and raw <link> serving don't resolve
it, and only some bundlers (Vite, webpack) rewrite specifiers in url() while
others (esbuild) don't. Resolve the path in JS as above and serve your own copy.
To override the font entirely, set --stagebook-font on any parent element (or
:root) — the components fall back through "Inter", ui-sans-serif, system-ui, sans-serif.
License
MIT — see LICENSE.
The bundled Inter font (packages/stagebook/src/assets/InterVariable.woff2)
is © The Inter Project Authors and licensed under the SIL Open Font License 1.1;
see Inter-OFL.txt.