Migrating Qualtrics and SurveyMonkey surveys to EDSL
Capture a rendered survey, recover its questions and routing, translate its visual language into Humanize CSS, audit the result, and hand off a portable EDSL Survey.
01 What Zugunruhe does
A hosted survey is more than a list of prompts. It is a small interactive program: pages, controls, validation rules, skip logic, randomized blocks, quotas, embedded fields, completion behavior, typography, spacing, colors, and assets all contribute to what a respondent experiences. Platform exports are useful when available, but researchers often receive only a respondent URL—or need an independent, inspectable reconstruction.
Zugunruhe drives the rendered instrument in Chromium and records what it can observe. It then converts that evidence into EDSL source, a JSON survey specification, synthetic respondent scaffolding, Humanize styling, and a side-by-side audit report. The output is designed for inspection and revision. It is not a claim that browser observation perfectly recovers proprietary survey logic.
What is observed
Visible questions, controls, answer labels, page transitions, alternate answer paths, computed visual properties, HTML, and screenshots.
What remains uncertain
Unvisited server-side branches, quotas, hidden embedded data, validation edge cases, randomization probabilities, and platform bookkeeping.
What is generated
Readable Python, portable JSON, EDSL question objects, rules, persona scaffolding, CSS, theme tokens, and an audit report.
What requires judgment
Semantic fidelity, acceptable visual differences, whether inferred routing is correct, and whether publication is authorized.
02 Install and diagnose
This tutorial assumes Python 3.10 or newer, a checkout of Zugunruhe, and Chromium installed through Playwright. The safe example is a local HTML file, so it does not send responses to Qualtrics, SurveyMonkey, or any other service.
git clone https://github.com/expectedparrot/zugunruhe.git
cd zugunruhe
python -m venv .venv
.venv/bin/pip install -e .
.venv/bin/playwright install chromium
.venv/bin/zugunruhe --version
.venv/bin/zugunruhe doctor
doctor is bounded and non-mutating. It reports whether the Playwright Python package and the ep executable are visible. It intentionally does not open a browser or test Expected Parrot authentication; those checks could be slower or involve external systems.
Representative doctor response
{
"schema_version": "1.0",
"command": "zugunruhe doctor",
"status": "ok",
"data": {
"capabilities": {
"playwright_python": true,
"playwright_browser": "not_checked",
"edsl_cli": "/path/to/ep",
"publishing_authentication": "not_checked"
},
"mutated": false
},
"warnings": ["Browser installation and Expected Parrot authentication are checked only when their commands run."],
"errors": [],
"next_steps": []
}ep and authentication are needed only for Humanize preview or publication.03 Let the package sequence the work
Zugunruhe has a package-owned agent control surface. A coding agent should not reproduce the workflow from memory. It asks for the guide, checks durable state, and executes the recommended structured action only after respecting its approval flag.
zugunruhe agent guide
zugunruhe agent docs list
zugunruhe agent docs show workflow
zugunruhe agent next -C work/safe-migration
zugunruhe agent status -C work/safe-migration
zugunruhe agent history -C work/safe-migration
For identical durable state, agent next is deterministic, local, cheap, and read-only. It returns a stable action ID, a working-directory array, an argument vector, inputs, outputs, reasons, warnings, and explicit booleans for local mutation, external effects, and required approval. It never launches a browser, performs inference, publishes, or appends history.
First recommendation in an empty project
{
"terminal": false,
"recommended_action": {
"id": "inspect_source",
"stage": "inspect",
"cwd": ["/absolute/path/work/safe-migration"],
"argv": ["zugunruhe", "inspect", "SURVEY_URL", "--output", "/absolute/path/work/safe-migration"],
"mutates_local_state": true,
"external_side_effect": true,
"requires_user_approval": true,
"reason": "No source capture exists.",
"input_artifacts": [],
"resulting_artifacts": ["manifest.json", "page screenshots and HTML", "route evidence"],
"safety_warnings": ["Live survey navigation may persist partial responses or submit a final page; inspection requires explicit user approval."]
},
"alternative_actions": []
}The placeholder SURVEY_URL is deliberate. Resolving it requires task context and, for a live source, explicit user approval. The top-level zugunruhe guide and zugunruhe next commands remain compatibility aliases.
04 Capture the safe local fixture
The repository includes examples/safe-survey.html, a two-option survey that exercises the real browser capture path without contacting a third party. Use an absolute file:// URL so Chromium can open it regardless of the output directory.
mkdir -p work
FIXTURE="file://$PWD/examples/safe-survey.html"
zugunruhe inspect "$FIXTURE" --output work/safe-migration
Capture has no default page cap and no default branch cap. Zugunruhe maintains a traversal queue, replays answer paths from the beginning, and checkpoints the manifest as it works. Diagnostic limits exist for development, but a capped run is intentionally marked incomplete.
--max-pages and --max-branches are diagnostic escape hatches. If either limit prevents queue exhaustion, the resulting manifest cannot represent migration completion.Each distinct observed page receives a stable fingerprint. Alongside the manifest, the capture stores a screenshot, rendered HTML, and collected inline CSS and JavaScript. These are evidence: keep them even after the EDSL reconstruction looks correct.
05 Audit capture completeness
Before converting anything, inspect state rather than inferring success from the presence of manifest.json.
zugunruhe agent status -C work/safe-migration
python -m json.tool work/safe-migration/manifest.json | less
| Manifest field | Required state | Why it matters |
|---|---|---|
traversal_complete | true | The path queue was exhausted rather than interrupted. |
failed_paths | Empty | A repeatedly failing replay can conceal a branch. |
diagnostic_limits.max_pages | null | A page cap turns the run into a diagnostic sample. |
diagnostic_limits.max_branches | null | A branch cap prevents an exhaustive answer probe. |
pages | Nonempty for a real survey | Contains the actual observed page evidence. |
If any completion criterion fails, agent next returns repair_capture, not migrate_to_edsl. Its alternative is a read-only status audit. This prevents an incomplete capture from becoming “complete” merely because later artifacts happen to exist.
06 Build the EDSL reconstruction
zugunruhe migrate work/safe-migration/manifest.json \
--output work/safe-migration/edsl
zugunruhe agent next -C work/safe-migration
The migration writes both executable Python and inspectable JSON. This dual representation matters: Python is convenient for EDSL, while JSON is easier for audits, diffs, coding agents, and other tools.
survey.py defines build_survey() and a module-level survey. It selects an EDSL class based on the observed control: QuestionMultipleChoice, QuestionCheckBox, QuestionNumerical, QuestionFreeText, or QuestionMatrix. Names are sanitized and made unique, while source_name preserves the original control identifier.
from pathlib import Path
import importlib.util
path = Path("work/safe-migration/edsl/survey.py")
spec = importlib.util.spec_from_file_location("migrated_survey", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
survey = module.build_survey()
print(survey)
The generated personas are scaffolding for synthetic testing, not survey-derived population estimates. Read persona_spec.json before using them and replace generic dimensions with study-appropriate traits when needed.
07 Preserve matrices and inspect routing
A grid should remain one semantic question. Zugunruhe recognizes matrix structure, records row items separately from column options, and emits QuestionMatrix. Turning every row into an unrelated multiple-choice question would lose the instrument’s grouping and produce a poorer respondent experience.
{
"name": "session_rating",
"text": "Please rate each part of the program",
"type": "matrix",
"items": ["Presentations", "Discussion", "Logistics"],
"options": ["Poor", "Fair", "Good", "Excellent"]
}
Routing requires more caution. Zugunruhe infers a rule only when alternate answers observed from the same parent page reach different destinations. Forward branch exits can rejoin after sibling branches; backward jumps are discarded because EDSL rejects them. A single observed transition is ordinary navigation, not enough evidence for skip logic.
survey_spec.json with the source instrument. Check display logic, termination, embedded data, quotas, randomization, and validation separately; some of these behaviors may never appear in rendered HTML.08 Recreate the survey’s visual language
zugunruhe style work/safe-migration/manifest.json \
--output work/safe-migration/edsl
During capture, Zugunruhe samples computed properties for semantic roles: body, container, question, question text, option label, input, primary button, and progress indicator. Styling selects representative values across pages and maps only a safe property allowlist. Potentially executable CSS values such as url(...), expression(...), and javascript: are rejected.
| Output | Purpose |
|---|---|
theme.json | Readable representative tokens and captured branding metadata. |
humanize.css | Generated CSS targeting EDSL Humanize selectors. |
humanize.json | Humanize schema containing the same CSS under survey.custom_css. |
The NBER/SurveyMonkey case
The motivating migration was the 2026 Summer Institute participant survey. Its source pages use a restrained NBER visual system: a muted gray surround, white survey sheet, dark text, a blue primary accent, generous spacing, and organization branding. Zugunruhe reads SurveyMonkey CSS variables when available and applies the captured title color, font size, weight, page background, and primary accent. It also generates a compact NBER-style header when branding metadata is present.
Assets are a separate problem. Logos and background images can involve expiring URLs, access controls, licensing, content hashing, and upload APIs. The current Humanize path handles CSS but does not claim to migrate binary assets. Preserve discovered asset URLs in evidence and resolve asset publication deliberately rather than embedding unstable links.
Button hover behavior
The browser may inherit a Humanize default hover rule even when the source button is blue. CSS can control it explicitly. Add a hover and focus-visible rule to humanize.css, then ensure humanize.json contains the revised CSS before previewing:
.edsl-survey-container form button:hover,
.edsl-survey-container form button:focus-visible {
background-color: rgb(0, 92, 185);
border-color: rgb(0, 92, 185);
filter: brightness(.94);
}
Keep focus visible for keyboard users. Matching a source design does not justify removing accessibility cues.
09 Build the audit report
zugunruhe report \
work/safe-migration/manifest.json \
work/safe-migration/edsl/survey_spec.json \
--output work/safe-migration/edsl/survey-report.html
The report is the principal human-review surface. Open it locally and compare captured source screenshots with reconstructed question metadata and inferred routing. The report should make omissions visible; it is not a celebratory “conversion succeeded” page.
- Read every prompt aloud and compare punctuation, interpolation, and instructional text.
- Count answer options and verify labels, order, exclusive choices, comments, and “other” behavior.
- Check matrix row items and scale columns as a single structured question.
- Trace each branch using concrete answers and verify where paths rejoin or terminate.
- Record validation that the browser could not infer: required fields, ranges, formats, and cross-question constraints.
- Compare typography, spacing, controls, progress, and hover/focus states on desktop and mobile.
10 Save the portable .ep artifact
The durable EDSL interchange format is produced with survey.git.save(), not the older generic survey.save(). Run this after reviewing or editing the generated Python:
cd work/safe-migration/edsl
python - <<'PY'
from survey import build_survey
survey = build_survey()
survey.git.save("survey.ep")
print("saved survey.ep")
PY
Inspect the resulting object with the EDSL CLI before sending it downstream:
ep inspect survey.ep
survey.py is a readable, reviewable source representation. survey.ep is the portable serialized artifact used by Expected Parrot workflows. Retain both, along with the JSON specification that explains how the source capture became EDSL.11 Preview and tune Humanize CSS
Previewing lets you compare the rendered EDSL instrument against source screenshots without treating it as an authorized publication.
zugunruhe preview \
--survey work/safe-migration/edsl/survey.ep \
--schema work/safe-migration/edsl/humanize.json
A successful preview also audits generated selectors and representative computed styles against Zugunruhe's versioned Humanize respondent-DOM contract. It writes .zugunruhe/preview-verification.json and a local audit screenshot, binding the result to hashes of the current survey and schema. Publication is blocked if this verification is missing, failed, or stale; --allow-unverified-preview is reserved for an explicit reviewed override.
Open the returned URL at a wide and narrow viewport. Iterate in small, explainable changes. When an existing Humanize survey needs only a CSS update, patch it and reload:
zugunruhe patch-css HUMAN_SURVEY_UUID \
--file work/safe-migration/edsl/humanize.css
Common visual differences include browser-default form controls, selector specificity, hover/focus styles, container widths, inherited line heights, and mobile padding. Prefer a narrow selector adjustment over global !important rules. Verify contrast and keyboard navigation after every visual change.
12 Record review and reach terminal handoff
Once capture, migration, styling, and report artifacts exist, agent next returns review_migration with a JSON schema and valid example. Review wording, options, validation, routing, embedded data, randomization, quotas, and termination behavior before publication.
mkdir -p work/safe-migration/.zugunruhe
cat > work/safe-migration/.zugunruhe/review.json <<'JSON'
{
"approved": true,
"reviewed_at": "2026-08-10T12:00:00Z",
"checks": [
"wording", "options", "validation", "routing",
"embedded_data", "randomization", "quotas", "termination"
]
}
JSON
zugunruhe agent next -C work/safe-migration
A valid review produces the explicit terminal action handoff_edsl_survey. Its exact input artifact is edsl/survey.py; Zugunruhe does not invent optional cleanup, run models, or perform downstream survey testing. If the review omits a required check, state reports INVALID_REVIEW and remains nonterminal.
13 Move from the fixture to a live survey
After the local workflow is familiar, substitute an authorized respondent URL and a fresh output directory:
zugunruhe agent next -C work/participant-survey
# Run only after explicit approval for live navigation:
zugunruhe inspect 'https://www.surveymonkey.com/r/EXAMPLE' \
--output work/participant-survey
Use a new capture directory for every substantive attempt. Do not overwrite the only evidence from a previous traversal. If the survey requires authentication, invitation tokens, CAPTCHA, uploads, payments, or personally identifying responses, stop and agree on a safe plan before automation.
Qualtrics and SurveyMonkey differences
Both platforms render familiar form controls, but markup, accessibility roles, progress indicators, and navigation conventions differ. SurveyMonkey often exposes theme variables that improve branding recovery. Qualtrics may combine question text and answer labels in containers that require cleanup. Neither platform’s DOM should be treated as a stable public API, so always inspect the artifacts and keep extraction tests anchored in semantics rather than opaque class names.
14 Artifact contract and provenance
Durable evidence produced by a migration.
| Artifact | Role | Keep? |
|---|---|---|
manifest.json | Traversal state, pages, paths, failures, limits, and source URL. | Always |
| Page screenshots and HTML | Rendered evidence for visual and semantic comparison. | Always |
| Inline CSS and JavaScript | Source evidence and debugging context; never execute blindly. | Always |
edsl/survey_spec.json | Normalized questions and inferred rules. | Always |
edsl/survey.py | Readable EDSL source and downstream handoff. | Always |
edsl/survey.ep | Portable EDSL serialization created with survey.git.save(). | For delivery |
| Humanize schema, CSS, and theme | Visual translation and editable design tokens. | Always |
edsl/survey-report.html | Human audit surface. | Always |
.zugunruhe/history.jsonl | Successful mutation ledger. | Always |
.zugunruhe/review.json | Explicit completion gate. | Always |
.zugunruhe/preview-verification.json | Hash-bound selector and computed-style audit used by the publication gate. | For publication |
.zugunruhe/humanize-preview-audit.png | Rendered current-DOM contract screenshot for visual regression review. | For publication |
Preserve the source manifest, screenshots, HTML, route evidence, EDSL source, styling artifacts, and audit report as provenance. Avoid committing live response tokens, cookies, credentials, personally identifiable data, or proprietary assets without permission.
15 Troubleshooting and recovery
Chromium is missing
python -m playwright install chromium
zugunruhe doctorIf Chromium exists but cannot launch in a restricted environment, run the browser step where OS sandbox and IPC services are available. Read-only agent commands should continue working.
The capture never becomes complete
Inspect remaining_paths and failed_paths. Repeated failures can arise from expiring collector tokens, randomization, rate limits, validation, or a path that depends on prior free text. Preserve the failed manifest, diagnose the cause, and recapture into a new directory. Do not edit traversal_complete by hand.
The matrix became separate questions
Open the captured HTML and confirm that rows and column headers have accessible grid semantics or stable grouping. Add a focused extraction fixture before changing selectors. The desired output is one QuestionMatrix with row items and column options.
The styling looks like the EDSL default
Check that the manifest pages contain theme data, then inspect theme.json. If the source uses CSS variables, confirm they were present in captured HTML. Compare selectors in browser developer tools; Humanize markup may require a more specific rule.
The Next button changes color on hover
Add explicit :hover and :focus-visible declarations, update both the CSS file and schema, then patch or recreate the preview. Do not remove the focus indicator.
survey.ep is missing
migrate generates source; serialize the reviewed object explicitly with survey.git.save("survey.ep"). This makes the handoff visible and avoids confusing it with legacy survey.save().
Expected Parrot preview fails
Validate that ep is installed and authenticated, the survey path is a real .ep artifact, and humanize.json contains valid JSON. The CLI returns expected configuration failures as structured errors without a traceback.
16 Safety boundaries
Rules that agents must preserve.
- Live survey navigation may persist partial responses or submit a final page; inspection requires explicit user approval.
- A capped capture, failed path, or manifest with traversal_complete=false is not a completed migration.
- Review wording, options, validation, routing, embedded data, randomization, quotas, and termination behavior before publication.
- Preview is distinct from publication; publication is externally visible and requires explicit user approval.
- Preserve the source manifest, screenshots, HTML, route evidence, EDSL source, styling artifacts, and audit report as provenance.
Review readiness
A human or supervising agent records review without Zugunruhe publishing anything.
Publication is intentionally absent from the agent’s terminal migration recommendation. If a user separately authorizes publication, use the reviewed .ep file and Humanize schema:
zugunruhe publish \
--survey work/participant-survey/edsl/survey.ep \
--schema work/participant-survey/edsl/humanize.json \
--name "Reviewed participant survey"
17 Completion checklist
- □ Live navigation was explicitly approved and used a safe collector or test path.
- □ The capture is uncapped,
traversal_completeis true, andfailed_pathsis empty. - □ Every prompt, option, matrix, input type, and validation rule was compared with source evidence.
- □ Every inferred branch, rejoin, and termination path was reviewed.
- □ Embedded data, randomization, quotas, and platform-only behavior were checked separately.
- □ Humanize CSS matches typography, spacing, color, controls, hover/focus behavior, and mobile layout closely enough.
- □ Asset limitations are recorded; no unstable or unauthorized asset was silently embedded.
- □ The audit report and provenance artifacts are retained.
- □
survey.pyis readable andsurvey.epwas created withsurvey.git.save(). - □
.zugunruhe/review.jsontruthfully records completed checks. - □
zugunruhe agent nextreturns terminalhandoff_edsl_surveywithedsl/survey.py. - □ Any publication has separate, explicit authorization.
At this point the migration is complete. Downstream testing can import the exact EDSL Survey artifact, construct appropriate agents or scenarios, and validate behavior without Zugunruhe broadening its role.