Takes the /questions and turns them into this years survey using the flow defined in /questions/survey.yaml.
A respondent-style render of the questions with hot reload for editing the survey.
npm run dev -w surveyNetlify builds this package, with branch deploys on e.g., https://<branch>.questions.survey.stackoverflow.co.
packages/results uses surveyPreview in packages/results/config.ts to link every question to https://releases-${year}.questions.survey.stackoverflow.co, so we always have a copy of that year.
Validate every question YAML and the survey.yaml flow.
npm run validate -w surveyThree sub-steps run in order; the first failure short-circuits:
| Script | What it checks | How |
|---|---|---|
validate:questions |
Every questions/**/*.yaml against the per-question schema |
ajv-cli |
validate:survey |
questions/survey.yaml against the flow schema |
ajv-cli |
validate:cross |
Filename matches question.id (extends to show_if and flow reference resolution later) |
scripts/validate.ts |
The schemas live alongside what they validate:
questions/question.schema.json— per-question shape.questions/survey.schema.json— survey flow shape.
Final output of the survey is done via the Qualtrics API.
packages/survey/scripts/qualtrics-sync.ts pushes this YAML into an existing Qualtrics survey via the Survey Definitions API. It transforms each question with the type mapping and reconciles idempotently keyed on DataExportTag (which equals question.id / the filename):
- in YAML, not in the survey → created,
- in both → updated only when the content changed,
- in the survey, not in YAML → reported as an orphan, deleted only with
--prune.
Qualtrics QIDs are preserved across runs, so a re-run with no edits is a no-op. New questions are created with a minimal payload and then immediately updated with the full payload (DataExportTag, recodes, randomization) — the create-then-update sequence avoids the API's create-time validation errors.
Configure via environment variables (set in a .env file at root):
QUALTRICS_API_TOKEN=... # X-API-TOKEN
QUALTRICS_DATACENTER=... # e.g. iad1, fra1, syd1
QUALTRICS_SURVEY_ID=SV_... # the target existing surveyThen, from the repo root:
npm run sync:qualtrics:dry -w survey # read-only GET + reconcile, prints WOULD-write actions
npm run sync:qualtrics -w survey # apply changes
npm run sync:qualtrics -w survey -- --prune # also delete orphaned questions/blocks
npm run sync:qualtrics -w survey -- --verbose # per-question decisionsWith no QUALTRICS_API_TOKEN, the script runs offline and just prints the transformed payloads — handy for eyeballing the transforms without credentials.
Phase 1 scope: questions, blocks, page breaks, and ensuring every block is reachable from the flow (new blocks are appended; existing flow is never rewritten). The flow-level if/then branching and the randomize: N BlockRandomizer are printed as "not synced yet" and left for a later phase.
See Qualtrics docs for more info.
- Visit the Oauth credentials page from the dashboard
- Create client
- Add a name
- Scopes required:
openid,manage:surveys,write:surveys,read:surveys - Grant Type:
Authorization code - Redirect URLs:
http://localhost:8080 - Create the client
- For Claude add your generated
clientIdbelow and enter into the terminal for MCP setup
claude mcp add-json qualtrics \
'{"type":"http","url":"https://stackoverflow.pdx1.qualtrics.com/API/mcp/survey-definitions","oauth":{"clientId":"your-client-id","callbackPort":8080}}' \
--client-secret