Offline SurveyToGo deployment guide
Create a questionnaire in voice, download its complete offline package, then install that package in SurveyToGo. Collection runs locally on the tablet.
Use the internet to prepare and download the package. No voice connection is needed during collection. Publishing and integration codes are unnecessary. Answers stay in SurveyToGo: no voice results, automatic upload or built-in offline analysis.
Unverified on SurveyToGo devices
Open the questionnaire editorHow answers reach the host
One voice module saves one JSON text value in one host question, even when the module contains several questions. JSON is structured text: its reports list holds the individual recorded answers. The integration does not automatically create a separate host column for each voice question.
- Install the offline package on one SurveyToGo custom question. For this example, call its exported answer column strudl_module. No voice server participates in collection or in the handoff.
- The local runtime prepares the recorded answer, then HTML/adapter.js calls STG.AnswerChanged() with the JSON for the current module question. It checks the saved value by reading it back before allowing progress. This happens during collection, without internet.
- The same JSON answer contains state for resuming and result for recorded data. During collection result.completed is false; after the final successful save it is true. Save the host interview and use SurveyToGo’s normal synchronization/export workflow when available. Read JSON.parse(value).result.reports.
Once installed and configured, the supplied adapter performs these writes automatically. The enumerator does not copy answers between surveys. Native storage and export still need the device/account acceptance test described below.
The recorded result contains garbled reports for protected questions and ordinary recorded values for other questions, plus identifiers and completion status. Intended protected answers and garbling randomness are never returned. Host-provided enumerator/respondent IDs are not echoed in the result: keep those native host fields beside the module answer in the export.
See the saved result and extraction example1. Export your questionnaire from voice
- Create or import the questionnaire in the editor. For a first test, download the example below and load it using Upload or download Markdown → Upload questionnaire Markdown. Download example questionnaire
- Open Offline SurveyToGo package in the editor. Choose Respondent confirmation for the first simulator test, enter your consent script version and complete study information, then click Download offline SurveyToGo ZIP.
- The consent text must explain the study contact, voluntary participation, answer protection, host identifiers, storage, retention and deletion. For external consent, choose Consent collected externally and map the actual host confirmation in step 2.
This ZIP contains the questionnaire and its fixed consent script. To change either, edit in voice and export a new package before starting new interviews. Editing questionnaire.md inside an existing ZIP does not rebuild the runtime. Never replace a package during an unfinished interview.
2. Map host fields in HTML/config.js
Extract the ZIP. Replace HTML/config.js with the following example, adapting the SurveyToGo question numbers: interview ID in question 1, enumerator ID in question 2 and respondent ID in question 3. The partner must verify these native field reads. Use strings for IDs.
globalThis.STRUDL_OFFLINE_CONFIG = {
hostData: () => ({
interview_id: String(STG.getUL().Answer(1)),
enumerator_id: String(STG.getUL().Answer(2)),
respondent_id: String(STG.getUL().Answer(3))
}),
consent: () => ({ confirmed: false, version: "" })
};
This keeps the package’s respondent-confirmation mode. If you exported in external-consent mode, replace only the consent property below; question 4 must record consent for this interview, and study-v1 must match the version entered before export. Offline uses version, whereas the online wrapper uses script_version.
consent: () => ({
confirmed: String(STG.getUL().Answer(4)) === "1",
version: "study-v1"
})
Host inputs are stored with the offline resume state. The recorded result does not echo them. Keep the same identifiers in native SurveyToGo fields so you can join exported answers to interviews. The host controls encryption and access for this offline data.
3. Check the files and repack
strudl-offline-surveytogo.zip
├── HTML/
│ ├── page1.html
│ ├── config.js
│ ├── adapter.js
│ ├── package.js
│ ├── runtime.js
│ ├── catalog.js
│ ├── style.css
│ ├── offline.css
│ ├── simulator.html
│ └── simulator.js
├── manifest.json
├── questionnaire.md
└── README.md
Only HTML/config.js needs host-field edits. package.js contains the runnable questionnaire; manifest.json and questionnaire.md describe that exact package. Keep all supplied files and the uppercase HTML/ folder at the ZIP root.
Keep HTML/ directly at the ZIP root, with uppercase letters. Do not zip the enclosing download folder. Run these commands from the folder that contains HTML/; the new ZIP is written one folder above it.
You can also create the ZIP with your file manager: select its contents, not the enclosing folder. The commands use Python 3; on Windows replace python3 with py -3.
cd "/path/to/extracted/package"
python3 -m zipfile -c ../strudl-offline-surveytogo.zip HTML manifest.json questionnaire.md README.md
python3 -m zipfile -l ../strudl-offline-surveytogo.zip
4. Test locally, then install on a device
- Verify the entire handoff: finish a synthetic module, save/submit the host interview, export the receiving field, parse its JSON and locate both first and last by question ID. Check recorded values against the module result, not against the protected choices clicked by the respondent.
- On your computer, open HTML/simulator.html in Chromium. Start with a package exported in Respondent confirmation mode. The simulator supplies synthetic IDs and deliberately does not auto-confirm external consent.
- Disconnect the computer from the internet, complete a synthetic interview, reload and reopen it with the same interview key. Test a second key and the failed-save control. The simulator demonstrates local persistence; it does not test your native field mappings.
- In SurveyToGo Studio, attach the configured ZIP and configure the module question as a custom question. Select that attachment, set the entry/class path to HTML/page1.html, and enable the custom rendering package. Your partner must confirm the exact controls for the Studio/app version in use.
- Install the host survey and package on the tablet before entering airplane mode. Test an actual interview without a network, including app restart, two separate interviews, consent, routing and full exported answers. Require durable saving and no truncation before fieldwork.
5. Configure completion, exports and deletion
What the recorded result looks like
The example below shows only the result object inside the saved SurveyToGo answer. The full answer also contains operational state; do not pass that full answer on to analysts. Extract result.reports as shown below.
{
"format": "strudl/offline-result",
"version": 1,
"package_id": "example-package-id",
"interview_id": "example-module-id",
"completed": true,
"reports": [
{"question_id": "first", "reported": 1, "event_id": "example-module-id:first"},
{"question_id": "last", "reported": 0, "event_id": "example-module-id:last"}
]
}
These are illustrative recorded values for the downloadable two-question example, not predictions of what a respondent will report. Both questions list Yes first and No second: reported: 0 means recorded Yes; reported: 1 means recorded No. For the protected first question, this does not reveal the intended answer.
Offline reports use question_id; online reports use question. The offline result.interview_id is generated by the module and is not your host-provided interview ID. Join to your study using the native host IDs exported on the same row.
| Report field | How to read it |
|---|---|
reported | One recorded choice index, starting at 0. Match it to the answer order in the exact questionnaire used. |
text / number | The ordinary recorded text or number. |
multiple | A list of recorded choice indices, each starting at 0. |
skipped: true | An explicit skip. A question absent from reports has no recorded answer; do not convert it to No or zero. |
event_id | A stable answer identifier. Use it to remove duplicates if you combine repeated callbacks or exports. |
Offline reports can also contain explanation: true for a completed information card. It is not an answer. Online results omit these cards.
A required SurveyToGo field is not enough: the wrapper saves resume state before the interview finishes. Your partner must block Next until result.completed is true. This JavaScript shows the condition; it is not a ready-to-paste SurveyToGo Studio validation expression. Bind equivalent logic to the validation hook supported by your app version.
function hasCompletedStrudl(answerJson) {
try {
return JSON.parse(String(answerJson)).result?.completed === true;
} catch {
return false;
}
}
Turn the host answer into analysis columns
Run this JavaScript after exporting the host data, in your export-processing code. exportedRow means one parsed CSV row. Replace the column names if your host export uses different names. This is not code to paste into the voice questionnaire or the host question editor.
Here strudl_module means the exported answer of the SurveyToGo question that runs the package. If your export calls it Q5 or another name, replace exportedRow.strudl_module below. The destination is the module question itself, not the host ID or consent questions.
const envelope = JSON.parse(exportedRow.strudl_module);
const result = envelope.result;
if (result?.completed !== true) throw new Error("Incomplete module");
const byQuestion = Object.fromEntries(
result.reports.map(report => [report.question_id, report])
);
const analysisRow = {
interview_id: exportedRow.interview_id,
enumerator_id: exportedRow.enumerator_id,
respondent_id: exportedRow.respondent_id,
first_recorded: byQuestion.first?.reported ?? null,
last_recorded: byQuestion.last?.reported ?? null
};
For the example result above, first_recorded is 1 and last_recorded is 0. The IDs come from the host’s existing columns, not from the result. Keep first and last for the supplied example; replace them with your questionnaire’s question IDs for your study. The code preserves zero and leaves missing/skipped choices as null.
This creates an analysisRow object; it does not modify the host survey or write a CSV file by itself. Save those objects using your own export workflow. To use a recorded answer in a later host question during the interview, add platform-specific parsing/routing after module completion; the supplied adapters do not create those extra host fields.
The host saves a separate resume state and recorded-only result. Host inputs and fixed variable values stay in state; analyst outputs use result.reports. Require result.completed before onward routing. Failed saves pause progress; retry reuses the same report.
Export result.reports, package_id and your native host identifiers. Keep manifest.json and questionnaire.md with the export as its data dictionary. Protected choices contain the already-randomized report; this package provides no debiasing or other analysis.
There is no server deadline, remote code revocation or voice withdrawal link in this package. Local withdrawal removes the module’s local data and leaves a marker preventing restart; it does not delete earlier exports or other host copies. Follow the study’s host-side deletion procedure.
Offline scope and limits
The offline runtime supports single or multiple questions, recorded-answer routing, text, numbers, multiple selections, explanation cards, scalar formulas and IID treatment draws. Array formulas and vector draws are rejected at export. Vignette practice answers are not stored.
Online and offline interviews cannot be converted into one another. A failed online connection is handled by reconnecting and resuming; planned offline fieldwork requires this separate package before collection starts.