Understood. Given that all of the strong reasons apply, you should treat a jsPsych-first survey path as a strategic capability, but implement it in a way that preserves (a) speed of delivery, (b) auditability, and (c) interoperability with ODM. The key is to avoid building a second full-featured form builder unless you must.
Recommendation¶
Adopt a ** two-track survey strategy :**
- SurveyJS remains the primary authoring UX for study teams (because it is mature and you already have it).
- Build a jsPsych Survey Runtime + Builder Lite only to the degree required for “single battery” participant workflows and unified instrumentation.
In practice, you should prioritize a SurveyJS → jsPsych transpilation pipeline first, then add “Builder Lite” only for edge cases that are truly jsPsych-specific.
Proposed Implementation Path (lowest risk, highest ROI)¶
Phase A: Unified “Activity” model (1 week of engineering-level scope)¶
Create one scheduling and delivery abstraction that works for both surveys and tasks.
Activity types
- survey_surveyjs
- survey_jspsych
- task_jspsych
Shared fields
- activity_id, study_id, version, released_at/by
- runtime: surveyjs or jspsych
- definition_json (schema/config)
- question_map (local keys → ItemOID, datatype, codelist)
- qc_policy (what QC signals to compute)
- audit_policy (draft/submitted/amend rules)
Why this matters
It prevents architecture fragmentation: schedules, reminders, exports, audit reports work identically regardless of runtime.
Phase B: jsPsych survey capture → your audit + ODM pipeline (must-have)¶
Even before any builder work, ensure jsPsych survey data flows through the same regulated core:
- Store raw jsPsych result payload as activity_response.raw_json
- Normalize to item-level facts keyed by ItemOID
- Write append-only item_audit_event rows for each item create/update/amend
- Export to ODM ClinicalData from normalized items
This ensures jsPsych surveys are first-class, auditable clinical data.
Phase C: SurveyJS → jsPsych transpiler (primary lever)¶
Implement a constrained transpiler that converts “SurveyJS-authored surveys” into a jsPsych timeline for a defined subset.
Supported subset (start here)¶
- Question types: text, integer/decimal, boolean, single-choice, multi-choice, date
- Required fields
- Pages → jsPsych “screens” (one page per screen)
- Simple branching (optional for v1; can be added later)
Output¶
- jsPsych timeline JSON that uses:
- survey-multi-choice, survey-text, etc., or your own consolidated jsPsych survey component
- stable question keys (derived from SurveyJS name)
- consistent data packaging
Benefits¶
- Study teams author once (SurveyJS)
- Runtime is unified (jsPsych battery)
- You avoid building a complex builder immediately
Phase D: “Builder Lite” for jsPsych-only surveys (only if needed)¶
Once transpilation is working, you’ll discover what still requires native jsPsych survey authoring. This is usually:
- micro-surveys embedded mid-task (very short, task-adaptive)
- special presentation constraints
- certain offline/UX patterns
- tightly coupled conditional logic based on task performance
Builder Lite scope
- A minimal UI that creates a restricted “jsPsych survey definition” format
- Strong guardrails:
- limited question types
- simple branching only
- no matrix/panel complexity
- It writes out your internal definition_json + mapping
This avoids reproducing SurveyJS features.
Key architectural rules (to keep this safe and maintainable)¶
1) One auditability model across SurveyJS and jsPsych¶
Regardless of runtime:
- items are keyed to stable ItemOID
- item changes produce item_audit_event
- post-submission changes require reason
- responses are immutable documents linked by amendment chains
2) One normalization pipeline¶
Normalize both SurveyJS and jsPsych responses into the same response_item store:
- typed columns + JSON for complex values
- indexes by (study_id, participant_id, item_oid)
3) One ODM export layer¶
ODM metadata and ClinicalData generation should never depend on runtime:
- both SurveyJS and jsPsych question maps output ItemDefs/CodeLists
- scheduled visits output StudyEventDefs
- responses output ItemData
4) Use “release snapshots”¶
When you “release” a study battery:
- hash the SurveyJS schema and/or transpiled jsPsych timeline
- hash the QuestionMap / ItemOID mapping
- lock versions for data collection
This prevents silent flips in mixed-runtime studies.
What to tell the team (positioning)¶
- We are not “replacing SurveyJS.” We are using SurveyJS as our authoring surface and jsPsych as our unified participant runtime, because our workflow demands single-battery execution and unified instrumentation.
- We will minimize custom UI build by transpiling SurveyJS → jsPsych for most surveys.
- A jsPsych builder will be developed only for surveys that cannot be expressed cleanly in SurveyJS or cannot be reliably transpiled.
- Auditability and ODM interoperability remain first-class; jsPsych surveys will be held to the same standard as CRFs.
Below is an agent-coder-ready specification for a SurveyJS → jsPsych transpiler that preserves your auditability + ODM goals and supports a mobile-first, single-battery runtime.
Specification: SurveyJS → jsPsych Survey Transpiler (Subset v1)¶
1) Objective¶
Enable study teams to author surveys in SurveyJS Creator and deliver them within a jsPsych battery (timeline) while maintaining:
- Stable identifiers (QuestionMap → ItemOID)
- Item-level auditability
- Deterministic exports (ODM ClinicalData)
- Unified participant instrumentation (timing/QC/device meta)
Non-goals (v1):
- Full SurveyJS feature parity
- Complex panels/matrices/dynamic repeats
- Arbitrary SurveyJS expression logic execution
2) Inputs and Outputs¶
Input¶
- surveyjs_schema (JSON) – SurveyJS form definition (Creator output)
- form_version_id – the immutable version used for runtime
- question_map – mapping table for stable item identity and typing (SurveyJS name → ItemOID, datatype, codelist)
- Optional: transpile_profile – e.g. "jspsych_v1_minimal"
Output¶
A jsPsych timeline JSON representing the survey, plus a manifest:
- timeline: jsPsych trials (pages/questions)
- metadata: stable IDs, version hashes, mapping hash, build info
- instrumentation: data packaging rules and QC metrics configuration
Required deterministic behavior: the same inputs must produce the same output (ordering, IDs, structure).
3) Supported SurveyJS Subset (v1)¶
Survey structure¶
- title (optional)
- pages[]
- Each page supports:
- name (recommended; else derive)
- title (optional)
- elements[]
Supported question types (¶
elements[].type¶
)¶
- text (single line)
- comment (multiline)
- boolean
- radiogroup (single choice)
- dropdown (single choice)
- checkbox (multi choice)
- rating (integer scale; limited)
- number (if present in schema; treat similarly to text + numeric validation)
- text with inputType: date, datetime-local, number (map accordingly)
Constraints supported¶
- isRequired
- min, max (numeric)
- minLength, maxLength
- choices (for choice questions)
- placeholder (optional)
Explicitly unsupported (v1)¶
- panel, paneldynamic, matrix, *multipletext, **html
- complex visibleIf, enableIf, calculatedValues, expression
- file upload, signature, image picker (handle later)
- nested questions / dynamic repeats
Behavior for unsupported elements: transpiler must emit warnings and fail closed (do not generate partial runtime without explicit override).
4) Identifier and Mapping Rules (Critical)¶
Question identity¶
- SurveyJS question identity is elements[].name (mandatory).
- If missing, transpiler fails with E_MISSING_NAME.
Stable ItemOID¶
- Each name must map to QuestionMap.item_oid.
- If mapping missing:
- Option A (recommended): fail with E_MISSING_MAPPING
- Option B (dev mode): auto-generate mapping + warning W_AUTOGEN_ITEMOID and persist it
Page identity¶
- Use page.name if present, else derive P{index+1}.
- Derive stable page_oid for instrumentation only (not ODM).
Timeline stable IDs¶
Every jsPsych trial produced must include:
- trial_id: deterministic string based on }:{form_version}:{page}:{question_name
5) Output Data Packaging (jsPsych → backend)¶
Data envelope format¶
Each jsPsych trial must emit a standardized data payload containing:
- study_id
- participant_id (or token reference)
- visit_instance_id / scheduled_task_id
- form_version_id
- form_oid, form_version
- runtime: "jspsych"
- surveyjs_source_hash (sha256 of canonicalized SurveyJS schema)
- transpiler_hash (sha256 of transpiler config/profile)
- question_items[]: list of
- qc: per-trial QC metrics (see below)
- client_meta: device/browser/app build, timezone
Submission strategy¶
Two viable options; pick one:
Option 1 (recommended): submit once at end
- Accumulate answers client-side
- Submit a single payload per survey activity
- Pros: fewer network calls, simpler audit unit
- Cons: if crash, you lose partial unless you checkpoint
Option 2: checkpoint per page
- Submit after each page trial
- Pros: resilience on mobile/offline
- Cons: more server events
Recommendation: implement end-submit + optional local persistence; add checkpoint later if needed.
6) Validation and Requiredness¶
jsPsych survey plugins may not enforce all constraints equivalently. Therefore:
Client-side (best effort)¶
- Required: do not allow page advance if required unanswered.
- Simple numeric bounds: enforce if question typed numeric/date.
Server-side (authoritative)¶
On submission, backend validates:
- all required surveyjs_name are present (per schema)
- types match mapping profile
- values are in codelist when applicable
If invalid:
- reject with 422 + structured error list
- do not write clinical record
- log attempt in audit log (security + traceability)
7) QC / Instrumentation Requirements (Because you want unified runtime)¶
Per trial/page, compute and include:
- rt_ms (reaction/response time)
- focus_lost_count (page visibility changes)
- blur_count (window blur)
- input_edits_count (if trackable)
- device_class (mobile/tablet/desktop)
- network_state (online/offline if available)
Define a minimal QC schema:
Backend stores QC alongside response metadata and can surface it for monitoring.
8) Transpilation Strategy (How to construct the jsPsych timeline)¶
Per page¶
Create a jsPsych trial representing that page.
Two approaches:
Approach A: use jsPsych survey plugins
- Use survey-multi-choice, survey-text, survey-likert where applicable
- Map each SurveyJS question into the plugin’s question list
Approach B (recommended for consistency): create one custom jsPsych “survey page” plugin
- Render questions with a shared component
- Uniform validation + QC collection
- Easier to guarantee mobile UX and consistent data format
Given your need for tight UX and unified instrumentation, Approach B is recommended even if it is a small up-front lift.
Page trial output¶
The page trial returns an object like:
- { [question_name]: value, ... } Then the transpiler wraps into the standard data envelope at the end.
9) Type Mapping Rules (SurveyJS → internal types → ODM)¶
Define a type mapping table:
| SurveyJS type | Internalvalue_type | Storage | ODM DataType (target) |
|---|---|---|---|
| text | string | value_text | text/string |
| comment | text | value_text | text |
| boolean | boolean | value_bool | boolean |
| radiogroup/dropdown | code | value_text | text + CodeListRef |
| checkbox | codeset | value_json (array) | text (policy) or repeated ItemData |
| rating | integer | value_int | integer |
| number | decimal | value_decimal | float/decimal |
| date | date | value_date | date |
| datetime-local | datetime | value_datetime | datetime |
Checkbox policy (v1): store as JSON array in one item row (codeset). ODM export can serialize as a delimited string or repeated values; choose one and document.
10) Error and Warning Model (must be structured)¶
Transpiler produces:
- errors[]: hard-stop issues (missing name, unsupported element, missing mapping)
- warnings[]: non-fatal issues (auto-generated mapping in dev, truncated label length)
Return payload:
{
"status": "failed|success",
"errors": [{"code":"E_UNSUPPORTED_TYPE","path":"pages[0].elements[2]"}],
"warnings": [{"code":"W_DEFAULT_PAGE_NAME","detail":"page.name missing; used P1"}],
"timeline": {... only if success ...},
"manifest": {...}
}
11) Required Backend Endpoints (transpiler integration)¶
Study/design endpoints¶
- POST /forms/{form_version_id}/transpile/jspsych
- Input: profile + options
- Output: timeline + manifest + warnings/errors
- RBAC: study_designer/admin
- POST /forms/{form_version_id}/transpile/jspsych/release
- Stores transpiled timeline as an immutable artifact (JSONB) with hash
- Logs audit event: FORM_RUNTIME_RELEASED
Participant runtime endpoints¶
- GET /participants/me/activities/{activity_instance_id}/runtime
- Returns either:
- SurveyJS schema (runtime=surveyjs)
- jsPsych timeline (runtime=jspsych)
- POST /participants/me/activities/{activity_instance_id}/submit
- Accepts standard data envelope from jsPsych or SurveyJS
- Normalizes, audits at item-level, stores raw + normalized
12) Auditability Integration (must be identical across runtimes)¶
On jsPsych survey submission:
- Create form_response row with:
- raw payload (full envelope)
- response hash
- metadata + QC
- For each question:
- write/update response_item
- append item_audit_event (create on first entry; update on amendment)
- enforce reason-for-change if prior submitted
This ensures jsPsych-derived surveys are indistinguishable from SurveyJS-derived surveys in downstream audit and ODM export.
13) Test Plan (non-negotiable)¶
Golden-file tests¶
- Given a fixed SurveyJS schema input, transpiler output must match expected timeline JSON (canonicalized).
Drift tests¶
- Rename a question:
- ensure transpiler fails if name changes without mapping update
- or ensures ItemOID remains stable if mapping says it’s same semantic item
Unsupported feature tests¶
- Add a panel/matrix → transpiler must fail with E_UNSUPPORTED_TYPE.
End-to-end test¶
- Simulate participant completion of transpiled jsPsych survey:
- verify submission accepted
- verify response_item rows created
- verify item_audit_event rows created
- verify ODM ClinicalData export includes those items
Implementation Notes (pragmatic)¶
Canonical JSON hashing¶
To compute schema and payload hashes deterministically:
- sort object keys
- normalize whitespace
- consistent float formatting
Storage of transpiled artifacts¶
Store per form_version_id:
- jspsych_timeline_json
- jspsych_timeline_sha256
- transpile_profile
- released_at/by
This aligns with your “release snapshot” approach and prevents silent flips.
Agent-Coder Prompt (copy/paste)¶
Implement a SurveyJS → jsPsych transpiler (subset v1) as specified:
- Input: SurveyJS schema JSON + form_version_id + QuestionMap mapping (surveyjs_name → item_oid, datatype, codelist_oid)
- Output: deterministic jsPsych timeline JSON + manifest; fail closed on unsupported SurveyJS features.
Scope:
- Support SurveyJS: pages/elements, types {text, comment, boolean, radiogroup, dropdown, checkbox, rating, number, date/datetime-local}
- Enforce required fields in runtime (best effort) and server-side validation (authoritative)
- Package jsPsych results into a standard submission envelope containing question_items with item_oid/value/value_type
- Store raw payload as form_response, normalize to response_item, and append item_audit_event per item
- Add endpoints:
- POST /forms/{form_version_id}/transpile/jspsych
- POST /forms/{form_version_id}/transpile/jspsych/release
- GET /participants/me/activities/{activity_instance_id}/runtime
- POST /participants/me/activities/{activity_instance_id}/submit
- Add golden-file tests + drift tests + unsupported feature tests
- Ensure stable deterministic IDs and sha256 hashes for schema and timeline artifacts
If you share which jsPsych survey plugin(s) you currently use (or whether you’d prefer the “custom survey page plugin” approach), I can tighten sections 8–9 into a concrete JSON output example (one page, three questions) and an exact expected submission envelope.